[389-ds-base] branch 389-ds-base-1.4.2 updated: Issue 50545 - Add the new replication monitor functionality to UI
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
mreynolds pushed a commit to branch 389-ds-base-1.4.2
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.2 by this push:
new 32c1505 Issue 50545 - Add the new replication monitor functionality to UI
32c1505 is described below
commit 32c1505beaf77a29abd7e9a95f45c98aea2df298
Author: Simon Pichugin <spichugi(a)redhat.com>
AuthorDate: Tue Oct 8 22:59:03 2019 +0200
Issue 50545 - Add the new replication monitor functionality to UI
Description: As we ported repl-monitor.pl to dscon CLI
we should add the functionality to WebUI.
It is important to keep in mind that we shouldn't expose
user's password so the interactive option should be carried out.
Improve replication monitor CLI JSON output consistency.
Add Full Replication report functionality with ability of
continuous refresh.
https://pagure.io/389-ds-base/issue/50545
Reviewed by: mreynolds (Thanks!)
---
src/cockpit/389-console/.eslintrc.json | 2 +-
src/cockpit/389-console/src/css/ds.css | 4 +
.../389-console/src/lib/monitor/monitorModals.jsx | 762 +++++++++++++----
.../389-console/src/lib/monitor/monitorTables.jsx | 944 ++++++++++++++++++++-
.../389-console/src/lib/monitor/replMonitor.jsx | 873 ++++++++++++++++---
src/cockpit/389-console/src/lib/tools.jsx | 8 +-
src/cockpit/389-console/src/monitor.jsx | 3 +
src/lib389/lib389/agreement.py | 30 +-
src/lib389/lib389/cli_conf/backend.py | 2 +-
src/lib389/lib389/cli_conf/replication.py | 95 ++-
src/lib389/lib389/replica.py | 46 +-
11 files changed, 2403 insertions(+), 366 deletions(-)
diff --git a/src/cockpit/389-console/.eslintrc.json b/src/cockpit/389-console/.eslintrc.json
index 03ff3c4..2f3d88f 100644
--- a/src/cockpit/389-console/.eslintrc.json
+++ b/src/cockpit/389-console/.eslintrc.json
@@ -21,7 +21,7 @@
"ObjectExpression": "first",
"CallExpression": { "arguments": "first" },
"MemberExpression": 2,
- "ignoredNodes": ["JSXAttribute"]
+ "ignoredNodes": ["JSXAttribute", "JSXElement", "JSXAttribute *", "JSXElement *"]
}
],
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
diff --git a/src/cockpit/389-console/src/css/ds.css b/src/cockpit/389-console/src/css/ds.css
index e6bb1da..662d8de 100644
--- a/src/cockpit/389-console/src/css/ds.css
+++ b/src/cockpit/389-console/src/css/ds.css
@@ -885,6 +885,10 @@ option {
padding-left: 5px;
}
+.ds-raise-field {
+ margin-top: -3px;
+}
+
.content-view-pf-pagination > div > span:last-child {
position: relative;
}
diff --git a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
index 96007ea..e48809a 100644
--- a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
+++ b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
@@ -8,157 +8,16 @@ import {
Button,
Form,
noop,
+ FormGroup,
+ FormControl,
Spinner,
+ Checkbox
} from "patternfly-react";
import PropTypes from "prop-types";
import { get_date_string } from "../tools.jsx";
-import { LagReportTable } from "./monitorTables.jsx";
+import { ReportSingleTable, ReportConsumersTable } from "./monitorTables.jsx";
import "../../css/ds.css";
-class ReplLoginModal extends React.Component {
- render() {
- const {
- showModal,
- closeHandler,
- handleChange,
- doReport,
- spinning,
- error
- } = this.props;
-
- let spinner = "";
- if (spinning) {
- spinner =
- <Row className="ds-margin-top">
- <hr />
- <div className="ds-modal-spinner">
- <Spinner loading inline size="lg" />Authenticating to all the replicas ...
- </div>
- </Row>;
- }
-
- return (
- <Modal show={showModal} onHide={closeHandler}>
- <div className="ds-no-horizontal-scrollbar">
- <Modal.Header>
- <button
- className="close"
- onClick={closeHandler}
- aria-hidden="true"
- aria-label="Close"
- >
- <Icon type="pf" name="close" />
- </button>
- <Modal.Title>
- Replication Login Credentials
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <Form horizontal autoComplete="off">
- <p>
- In order to get the replication agreement lag times and state the
- authentication credentials to the remote replicas must be provided.
- This only works if the bind credentials used are valid on all the
- replicas.
- </p>
- <hr />
- <Row>
- <Col sm={3}>
- <ControlLabel>
- Bind DN
- </ControlLabel>
- </Col>
- <Col sm={9}>
- <input
- className={error.binddn ? "ds-input-auto-bad" : "ds-input-auto"}
- onChange={handleChange} defaultValue="cn=Directory Manager"
- type="text" id="binddn"
- />
- </Col>
- </Row>
- <Row className="ds-margin-top">
- <Col sm={3}>
- <ControlLabel>
- Password
- </ControlLabel>
- </Col>
- <Col sm={9}>
- <input
- className={error.bindpw ? "ds-input-auto-bad" : "ds-input-auto"}
- onChange={handleChange} type="password" id="bindpw"
- />
- </Col>
- </Row>
- {spinner}
- </Form>
- </Modal.Body>
- <Modal.Footer>
- <Button
- bsStyle="default"
- className="btn-cancel"
- onClick={closeHandler}
- >
- Close
- </Button>
- <Button
- bsStyle="primary"
- onClick={doReport}
- >
- Get Report
- </Button>
- </Modal.Footer>
- </div>
- </Modal>
- );
- }
-}
-
-class ReplLagReportModal extends React.Component {
- render() {
- const {
- showModal,
- closeHandler,
- agmts,
- pokeAgmt,
- viewAgmt
- } = this.props;
-
- return (
- <Modal backdrop="static" contentClassName="ds-lag-report" show={showModal} onHide={closeHandler}>
- <Modal.Header>
- <button
- className="close"
- onClick={closeHandler}
- aria-hidden="true"
- aria-label="Close"
- >
- <Icon type="pf" name="close" />
- </button>
- <Modal.Title>
- Replication Lag Report
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <LagReportTable
- agmts={agmts}
- pokeAgmt={pokeAgmt}
- viewAgmt={viewAgmt}
- />
- </Modal.Body>
- <Modal.Footer>
- <Button
- bsStyle="default"
- className="btn-cancel"
- onClick={closeHandler}
- >
- Close
- </Button>
- </Modal.Footer>
- </Modal>
- );
- }
-}
-
class TaskLogModal extends React.Component {
render() {
const {
@@ -224,6 +83,16 @@ class AgmtDetailsModal extends React.Component {
convertedDate[attr] = get_date_string(agmt[attr]);
}
}
+ let initButton = null;
+ if (!this.props.isRemoteAgmt) {
+ initButton = <Button
+ bsStyle="default"
+ className="btn-primary ds-float-left"
+ onClick={this.props.initAgmt}
+ >
+ Initialize Agreement
+ </Button>;
+ }
return (
<Modal show={showModal} onHide={closeHandler}>
@@ -327,13 +196,7 @@ class AgmtDetailsModal extends React.Component {
</Form>
</Modal.Body>
<Modal.Footer>
- <Button
- bsStyle="default"
- className="btn-primary ds-float-left"
- onClick={this.props.initAgmt}
- >
- Initialize Agreement
- </Button>
+ {initButton}
<Button
bsStyle="default"
className="btn-cancel"
@@ -645,12 +508,494 @@ class ConflictCompareModal extends React.Component {
}
}
+class ReportCredentialsModal extends React.Component {
+ render() {
+ const {
+ handleFieldChange,
+ showModal,
+ closeHandler,
+ newEntry,
+ hostname,
+ port,
+ binddn,
+ pwInputInterractive,
+ bindpw,
+ addConfig,
+ editConfig
+ } = this.props;
+
+ return (
+ <Modal show={showModal} onHide={closeHandler}>
+ <div className="ds-no-horizontal-scrollbar">
+ <Modal.Header>
+ <button
+ className="close"
+ onClick={closeHandler}
+ aria-hidden="true"
+ aria-label="Close"
+ >
+ <Icon type="pf" name="close" />
+ </button>
+ <Modal.Title>{newEntry ? "Add" : "Edit"} Report Credentials</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Row>
+ <Col sm={12}>
+ <Form horizontal autoComplete="off">
+ <FormGroup controlId="credsHostname">
+ <Col sm={3}>
+ <ControlLabel title="A regex for hostname">
+ Hostname
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="text"
+ value={hostname}
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="credsPort">
+ <Col sm={3}>
+ <ControlLabel title="A regex for port">
+ Port
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="text"
+ value={port}
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="credsBinddn">
+ <Col sm={3}>
+ <ControlLabel title="Bind DN for the specified instances">
+ Bind DN
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="text"
+ value={binddn}
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="credsBindpw">
+ <Col sm={3}>
+ <ControlLabel title="Bind password for the specified instances">
+ Password
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="password"
+ value={bindpw}
+ onChange={handleFieldChange}
+ disabled={pwInputInterractive}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="interractiveInput">
+ <Col sm={3}>
+ <ControlLabel title="Input the password interactively">
+ Interractive Input
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <Checkbox
+ checked={pwInputInterractive}
+ id="pwInputInterractive"
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ </Form>
+ </Col>
+ </Row>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button bsStyle="default" className="btn-cancel" onClick={closeHandler}>
+ Cancel
+ </Button>
+ <Button bsStyle="primary" onClick={newEntry ? addConfig : editConfig}>
+ Save
+ </Button>
+ </Modal.Footer>
+ </div>
+ </Modal>
+ );
+ }
+}
+
+class ReportAliasesModal extends React.Component {
+ render() {
+ const {
+ handleFieldChange,
+ showModal,
+ closeHandler,
+ newEntry,
+ hostname,
+ port,
+ alias,
+ addConfig,
+ editConfig
+ } = this.props;
+
+ return (
+ <Modal show={showModal} onHide={closeHandler}>
+ <div className="ds-no-horizontal-scrollbar">
+ <Modal.Header>
+ <button
+ className="close"
+ onClick={closeHandler}
+ aria-hidden="true"
+ aria-label="Close"
+ >
+ <Icon type="pf" name="close" />
+ </button>
+ <Modal.Title>{newEntry ? "Add" : "Edit"} Report Credentials</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Row>
+ <Col sm={12}>
+ <Form horizontal>
+ <FormGroup controlId="aliasName">
+ <Col sm={3}>
+ <ControlLabel title="Alias name for the instance">
+ Alias
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="text"
+ value={alias}
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="aliasHostname">
+ <Col sm={3}>
+ <ControlLabel title="An instance hostname">
+ Hostname
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="text"
+ value={hostname}
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="aliasPort">
+ <Col sm={3}>
+ <ControlLabel title="An instance port">
+ Port
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="number"
+ value={port}
+ onChange={handleFieldChange}
+ />
+ </Col>
+ </FormGroup>
+ </Form>
+ </Col>
+ </Row>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button bsStyle="default" className="btn-cancel" onClick={closeHandler}>
+ Cancel
+ </Button>
+ <Button bsStyle="primary" onClick={newEntry ? addConfig : editConfig}>
+ Save
+ </Button>
+ </Modal.Footer>
+ </div>
+ </Modal>
+ );
+ }
+}
+
+class ReportLoginModal extends React.Component {
+ render() {
+ const {
+ showModal,
+ closeHandler,
+ handleChange,
+ processCredsInput,
+ instanceName,
+ disableBinddn,
+ loginBinddn,
+ loginBindpw
+ } = this.props;
+
+ return (
+ <Modal show={showModal} onHide={closeHandler}>
+ <div className="ds-no-horizontal-scrollbar">
+ <Modal.Header>
+ <button
+ className="close"
+ onClick={closeHandler}
+ aria-hidden="true"
+ aria-label="Close"
+ >
+ <Icon type="pf" name="close" />
+ </button>
+ <Modal.Title>Replication Login Credentials for {instanceName}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Form horizontal autoComplete="off">
+ <p>
+ In order to get the replication agreement lag times and state the
+ authentication credentials to the remote replicas must be provided.
+ </p>
+ <hr />
+ <p>
+ Bind DN was acquired from <b>Replica Credentials</b> table. If you want
+ to bind as another user, change or remove the Bind DN there.
+ </p>
+ <br />
+ <FormGroup controlId="loginBinddn">
+ <Col sm={3}>
+ <ControlLabel title="Bind DN for the instance">
+ Bind DN
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="text"
+ value={loginBinddn}
+ onChange={handleChange}
+ disabled={disableBinddn}
+ />
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="loginBindpw">
+ <Col sm={3}>
+ <ControlLabel title="Password for the Bind DN">
+ Password
+ </ControlLabel>
+ </Col>
+ <Col sm={9}>
+ <FormControl
+ type="password"
+ value={loginBindpw}
+ onChange={handleChange}
+ />
+ </Col>
+ </FormGroup>
+ </Form>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button bsStyle="default" className="btn-cancel" onClick={closeHandler}>
+ Close
+ </Button>
+ <Button bsStyle="primary" onClick={processCredsInput}>
+ Confirm Credentials Input
+ </Button>
+ </Modal.Footer>
+ </div>
+ </Modal>
+ );
+ }
+}
+
+class FullReportContent extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ oneTableReport: false,
+ showDisabledAgreements: false
+ };
+
+ this.handleSwitchChange = this.handleSwitchChange.bind(this);
+ }
+
+ handleSwitchChange(e) {
+ if (typeof e === "boolean") {
+ // Handle Switch object
+ this.setState({
+ oneTableReport: e
+ });
+ } else {
+ this.setState({
+ [e.target.id]: e.target.checked
+ });
+ }
+ }
+
+ render() {
+ const {
+ reportData,
+ handleRefresh,
+ reportRefreshing,
+ reportLoading
+ } = this.props;
+
+ let suppliers = [];
+ let supplierName;
+ let supplierData;
+ let resultRows = [];
+ let spinner = <ControlLabel />;
+ if (reportLoading) {
+ spinner = (
+ <div>
+ <ControlLabel title="Do the refresh every few seconds">
+ {reportRefreshing ? "Refreshing" : "Loading"} the report...
+ </ControlLabel>
+ <Spinner inline loading size="sm" />
+ </div>
+ );
+ }
+ let reportHeader = "";
+ if (reportData.length > 0) {
+ reportHeader = (
+ <Form horizontal autoComplete="off">
+ <FormGroup controlId="showDisabledAgreements">
+ <Col sm={8}>
+ <Checkbox
+ checked={this.state.showDisabledAgreements}
+ id="showDisabledAgreements"
+ onChange={this.handleSwitchChange}
+ title="Display all agreements including the disabled ones and the ones we failed to connect to"
+ >
+ Show All (Including Disabled Agreements)
+ </Checkbox>
+ </Col>
+ </FormGroup>
+ <FormGroup controlId="oneTableReport">
+ <Col sm={6} title="Show all data in one table (it makes it easier to check lag times)">
+ <Checkbox
+ checked={this.state.oneTableReport}
+ onChange={this.handleSwitchChange}
+ id="oneTableReport"
+ title="Display all agreements including the disabled ones and the ones we failed to connect to"
+ >
+ Table View
+ </Checkbox>
+ </Col>
+ </FormGroup>
+ <Button
+ className="ds-margin-top"
+ bsStyle="default"
+ onClick={handleRefresh}
+ >
+ Refresh Report
+ </Button>
+ <hr />
+ </Form>
+ );
+ } else {
+ reportHeader = spinner;
+ }
+ if (this.state.oneTableReport) {
+ for (let supplier of reportData) {
+ for (let replica of supplier.data) {
+ resultRows = resultRows.concat(replica.agmts_status);
+ }
+ suppliers.push(supplierData);
+ }
+ suppliers = [(<div>
+ <ReportSingleTable
+ rows={resultRows}
+ viewAgmt={this.props.viewAgmt}
+ />
+ </div>
+ )];
+ } else {
+ for (let supplier of reportData) {
+ let s_data = supplier.data;
+ if (s_data.length === 1 && s_data[0].replica_status.startsWith("Unavailable")) {
+ supplierData = (
+ <div>
+ <h4>
+ <b>Can not get replication information from Replica</b>
+ </h4>
+ <h4 title="Supplier availability status">
+ <b>Replica Status:</b> {s_data[0].replica_status}
+ </h4>
+ </div>
+ );
+ } else {
+ supplierData = supplier.data.map(replica => (
+ <div key={replica.replica_root + replica.replica_id}>
+ <h4 title="Replica Root suffix">
+ <b>Replica Root:</b> {replica.replica_root}
+ </h4>
+ <h4 title="Replica ID">
+ <b>Replica ID:</b> {replica.replica_id}
+ </h4>
+ <h4 title="Replica Status">
+ <b>Replica Status:</b> {replica.replica_status}
+ </h4>
+ <h4 title="Max CSN">
+ <b>Max CSN:</b> {replica.maxcsn}
+ </h4>
+ {"agmts_status" in replica &&
+ replica.agmts_status.length > 0 &&
+ "agmt-name" in replica.agmts_status[0] ? (
+ <ReportConsumersTable
+ rows={replica.agmts_status}
+ viewAgmt={this.props.viewAgmt}
+ />
+ ) : (
+ <h4>
+ <b>No Agreements Were Found</b>
+ </h4>
+ )}
+ </div>
+ ));
+ }
+ supplierName = (
+ <div key={supplier.name}>
+ <center>
+ <h2 title="Supplier host:port (and alias if applicable)">
+ <b>Supplier:</b> {supplier.name}
+ </h2>
+ </center>
+ <hr />
+ {supplierData}
+ </div>
+ );
+ suppliers.push(supplierName);
+ }
+ }
+
+ let report = suppliers.map(supplier => (
+ <div key={supplier.key}>
+ {supplier}
+ <hr />
+ </div>
+ ));
+ if (reportLoading) {
+ report =
+ <Col sm={12} className="ds-center ds-margin-top">
+ {spinner}
+ </Col>;
+ }
+
+ return (
+ <div>
+ {reportHeader}
+ {report}
+ </div>
+ );
+ }
+}
// Prototypes and defaultProps
AgmtDetailsModal.propTypes = {
showModal: PropTypes.bool,
closeHandler: PropTypes.func,
agmt: PropTypes.object,
initAgmt: PropTypes.func,
+ isRemoteAgmt: PropTypes.bool
};
AgmtDetailsModal.defaultProps = {
@@ -658,6 +1003,7 @@ AgmtDetailsModal.defaultProps = {
closeHandler: noop,
agmt: {},
initAgmt: noop,
+ isRemoteAgmt: false
};
WinsyncAgmtDetailsModal.propTypes = {
@@ -686,24 +1032,6 @@ TaskLogModal.defaultProps = {
agreement: "",
};
-ReplLoginModal.propTypes = {
- showModal: PropTypes.bool,
- closeHandler: PropTypes.func,
- handleChange: PropTypes.func,
- doReport: PropTypes.func,
- spinning: PropTypes.bool,
- error: PropTypes.object,
-};
-
-ReplLoginModal.defaultProps = {
- showModal: false,
- closeHandler: noop,
- handleChange: noop,
- doReport: noop,
- spinning: false,
- error: {},
-};
-
ConflictCompareModal.propTypes = {
showModal: PropTypes.bool,
conflictEntry: PropTypes.object,
@@ -722,11 +1050,101 @@ ConflictCompareModal.defaultProps = {
closeHandler: noop,
};
+ReportCredentialsModal.propTypes = {
+ showModal: PropTypes.bool,
+ closeHandler: PropTypes.func,
+ handleFieldChange: PropTypes.func,
+ hostname: PropTypes.string,
+ port: PropTypes.string,
+ binddn: PropTypes.string,
+ bindpw: PropTypes.string,
+ pwInputInterractive: PropTypes.bool,
+ newEntry: PropTypes.bool,
+ addConfig: PropTypes.func,
+ editConfig: PropTypes.func
+};
+
+ReportCredentialsModal.defaultProps = {
+ showModal: false,
+ closeHandler: noop,
+ handleFieldChange: noop,
+ hostname: "",
+ port: "",
+ binddn: "",
+ bindpw: "",
+ pwInputInterractive: false,
+ newEntry: false,
+ addConfig: noop,
+ editConfig: noop,
+};
+
+ReportAliasesModal.propTypes = {
+ showModal: PropTypes.bool,
+ closeHandler: PropTypes.func,
+ handleFieldChange: PropTypes.func,
+ hostname: PropTypes.string,
+ port: PropTypes.number,
+ alias: PropTypes.string,
+ newEntry: PropTypes.bool,
+ addConfig: PropTypes.func,
+ editConfig: PropTypes.func
+};
+
+ReportAliasesModal.defaultProps = {
+ showModal: false,
+ closeHandler: noop,
+ handleFieldChange: noop,
+ hostname: "",
+ port: 389,
+ alias: "",
+ newEntry: false,
+ addConfig: noop,
+ editConfig: noop,
+};
+
+ReportLoginModal.propTypes = {
+ showModal: PropTypes.bool,
+ closeHandler: PropTypes.func,
+ handleChange: PropTypes.func,
+ processCredsInput: PropTypes.func,
+ instanceName: PropTypes.string,
+ disableBinddn: PropTypes.bool,
+ loginBinddn: PropTypes.string,
+ loginBindpw: PropTypes.string
+};
+
+ReportLoginModal.defaultProps = {
+ showModal: false,
+ closeHandler: noop,
+ handleChange: noop,
+ processCredsInput: noop,
+ instanceName: "",
+ disableBinddn: false,
+ loginBinddn: "",
+ loginBindpw: ""
+};
+
+FullReportContent.propTypes = {
+ reportData: PropTypes.array,
+ handleRefresh: PropTypes.func,
+ reportRefreshing: PropTypes.bool
+};
+
+FullReportContent.defaultProps = {
+ handleFieldChange: noop,
+ reportData: [],
+ handleRefresh: noop,
+ reportRefreshTimeout: 5,
+ reportRefreshing: false
+};
+
export {
TaskLogModal,
AgmtDetailsModal,
- ReplLagReportModal,
- ReplLoginModal,
WinsyncAgmtDetailsModal,
ConflictCompareModal,
+ ReportCredentialsModal,
+ ReportAliasesModal,
+ ReportLoginModal,
+ FullReportContent
};
diff --git a/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx b/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
index 21c0128..d38e2fc 100644
--- a/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
+++ b/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
@@ -11,7 +11,7 @@ import {
import { DSTable, DSShortTable } from "../dsTable.jsx";
import PropTypes from "prop-types";
import "../../css/ds.css";
-import { get_date_string } from "../tools.jsx";
+import { get_date_string, searchFilter } from "../tools.jsx";
class AbortCleanALLRUVTable extends React.Component {
constructor(props) {
@@ -542,16 +542,16 @@ class WinsyncAgmtTable extends React.Component {
formatters: [
(value, { rowData }) => {
return [
- <td key={rowData['agmt-name']}>
- <DropdownButton id={rowData['agmt-name']}
+ <td key={rowData['agmt-name'][0]}>
+ <DropdownButton id={rowData['agmt-name'][0]}
bsStyle="default" title="Actions">
<MenuItem eventKey="1" onClick={() => {
- this.props.viewAgmt(rowData['agmt-name']);
+ this.props.viewAgmt(rowData['agmt-name'][0]);
}}>
View Agreement Details
</MenuItem>
<MenuItem eventKey="2" onClick={() => {
- this.props.pokeAgmt(rowData['agmt-name']);
+ this.props.pokeAgmt(rowData['agmt-name'][0]);
}}>
Poke Agreement
</MenuItem>
@@ -760,16 +760,16 @@ class AgmtTable extends React.Component {
formatters: [
(value, { rowData }) => {
return [
- <td key={rowData['agmt-name']}>
- <DropdownButton id={rowData['agmt-name']}
+ <td key={rowData['agmt-name'][0]}>
+ <DropdownButton id={rowData['agmt-name'][0]}
bsStyle="default" title="Actions">
<MenuItem eventKey="1" onClick={() => {
- this.props.viewAgmt(rowData['agmt-name']);
+ this.props.viewAgmt(rowData['agmt-name'][0]);
}}>
View Agreement Details
</MenuItem>
<MenuItem eventKey="2" onClick={() => {
- this.props.pokeAgmt(rowData['agmt-name']);
+ this.props.pokeAgmt(rowData['agmt-name'][0]);
}}>
Poke Agreement
</MenuItem>
@@ -1175,16 +1175,16 @@ class LagReportTable extends React.Component {
formatters: [
(value, { rowData }) => {
return [
- <td key={rowData['agmt-name']}>
- <DropdownButton id={rowData['agmt-name']}
+ <td key={rowData['agmt-name'][0]}>
+ <DropdownButton id={rowData['agmt-name'][0]}
bsStyle="default" title="Actions">
<MenuItem eventKey="1" onClick={() => {
- this.props.viewAgmt(rowData['agmt-name']);
+ this.props.viewAgmt(rowData['agmt-name'][0]);
}}>
View Agreement Details
</MenuItem>
<MenuItem eventKey="2" onClick={() => {
- this.props.pokeAgmt(rowData['agmt-name']);
+ this.props.pokeAgmt(rowData['agmt-name'][0]);
}}>
Poke Agreement
</MenuItem>
@@ -1662,6 +1662,770 @@ class DiskTable extends React.Component {
header: {
label: "Available Space",
props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 3
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+
+ ]
+ };
+ this.getColumns = this.getColumns.bind(this);
+ }
+
+ getColumns() {
+ return this.state.columns;
+ }
+
+ render() {
+ return (
+ <div className="ds-margin-top-xlg">
+ <DSShortTable
+ getColumns={this.getColumns}
+ rowKey={this.state.rowKey}
+ rows={this.props.disks}
+ disableLoadingSpinner
+ />
+ </div>
+ );
+ }
+}
+
+class ReportAliasesTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getColumns = this.getColumns.bind(this);
+ this.getSingleColumn = this.getSingleColumn.bind(this);
+
+ this.state = {
+ searchField: "Aliases",
+ fieldsToSearch: ["alias", "connData"],
+
+ columns: [
+ {
+ property: "alias",
+ header: {
+ label: "Alias",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "connData",
+ header: {
+ label: "Connection Data",
+ props: {
+ index: 1,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 1
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "actions",
+ header: {
+ props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1
+ },
+ formatters: [actionHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 2
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.alias}>
+ <DropdownButton
+ id={rowData.alias}
+ bsStyle="default"
+ title="Actions"
+ >
+ <MenuItem
+ eventKey="1"
+ onClick={() => {
+ this.props.editConfig(rowData);
+ }}
+ >
+ Edit Alias
+ </MenuItem>
+ <MenuItem divider />
+ <MenuItem
+ eventKey="2"
+ onClick={() => {
+ this.props.deleteConfig(rowData);
+ }}
+ >
+ Delete Alias
+ </MenuItem>
+ </DropdownButton>
+ </td>
+ ];
+ }
+ ]
+ }
+ }
+ ]
+ };
+ }
+
+ getColumns() {
+ return this.state.columns;
+ }
+
+ getSingleColumn () {
+ return [
+ {
+ property: "msg",
+ header: {
+ label: "Instance Aliases",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ ];
+ }
+
+ render() {
+ let reportAliasTable;
+ if (this.props.rows.length < 1) {
+ reportAliasTable = (
+ <DSShortTable
+ getColumns={this.getSingleColumn}
+ rowKey={"msg"}
+ rows={[{ msg: "No alias entries" }]}
+ disableLoadingSpinner
+ />
+ );
+ } else {
+ reportAliasTable = (
+ <DSShortTable
+ getColumns={this.getColumns}
+ rowKey="alias"
+ rows={this.props.rows}
+ disableLoadingSpinner
+ />
+ );
+ }
+
+ return <div className="ds-margin-top-xlg">{reportAliasTable}</div>;
+ }
+}
+
+class ReportCredentialsTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getColumns = this.getColumns.bind(this);
+ this.getSingleColumn = this.getSingleColumn.bind(this);
+
+ this.state = {
+ columns: [
+ {
+ property: "connData",
+ header: {
+ label: "Connection Data",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "credsBinddn",
+ header: {
+ label: "Bind DN",
+ props: {
+ index: 1,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 1
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.connData}>
+ {value == "" ? <i>Edit To Add a Bind DN Data</i> : value }
+ </td>
+ ];
+ }
+ ]
+ }
+ },
+ {
+ property: "credsBindpw",
+ header: {
+ label: "Password",
+ props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 2
+ },
+ formatters: [
+ (value, { rowData }) => {
+ let pwField = <i>Interractive Input is set</i>;
+ if (!rowData.pwInputInterractive) {
+ if (value == "") {
+ pwField = <i>Both Password or Interractive Input flag are not set</i>;
+ } else {
+ pwField = "********";
+ }
+ }
+ return [
+ <td key={rowData.connData}>
+ {pwField}
+ </td>
+ ];
+ }
+ ]
+ }
+ },
+ {
+ property: "actions",
+ header: {
+ props: {
+ index: 3,
+ rowSpan: 1,
+ colSpan: 1
+ },
+ formatters: [actionHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 3
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.connData}>
+ <DropdownButton
+ id={rowData.connData}
+ bsStyle="default"
+ title="Actions"
+ >
+ <MenuItem
+ eventKey="1"
+ onClick={() => {
+ this.props.editConfig(rowData);
+ }}
+ >
+ Edit Connection
+ </MenuItem>
+ <MenuItem divider />
+ <MenuItem
+ eventKey="2"
+ onClick={() => {
+ this.props.deleteConfig(rowData);
+ }}
+ >
+ Delete Connection
+ </MenuItem>
+ </DropdownButton>
+ </td>
+ ];
+ }
+ ]
+ }
+ }
+ ]
+ };
+ }
+
+ getColumns() {
+ return this.state.columns;
+ }
+
+ getSingleColumn () {
+ return [
+ {
+ property: "msg",
+ header: {
+ label: "Replica Credentials Table",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ ];
+ }
+
+ render() {
+ let reportConnTable;
+ if (this.props.rows.length < 1) {
+ reportConnTable = (
+ <DSShortTable
+ getColumns={this.getSingleColumn}
+ rowKey={"msg"}
+ rows={[{ msg: "No connection entries" }]}
+ disableLoadingSpinner
+ />
+ );
+ } else {
+ reportConnTable = (
+ <DSShortTable
+ getColumns={this.getColumns}
+ rowKey="connData"
+ rows={this.props.rows}
+ disableLoadingSpinner
+ />
+ );
+ }
+
+ return <div className="ds-margin-top-xlg">{reportConnTable}</div>;
+ }
+}
+
+class ReportSingleTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getColumns = this.getColumns.bind(this);
+ this.getSingleColumn = this.getSingleColumn.bind(this);
+
+ this.state = {
+ searchField: "Replica",
+ fieldsToSearch: [
+ "supplierName",
+ "replicaName",
+ "replicaStatus",
+ "agmt-name",
+ "replica",
+ "replicaStatus",
+ "replica-enabled",
+ "replication-lag-time"
+ ],
+
+ columns: [
+ {
+ property: "supplierName",
+ header: {
+ label: "Supplier",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replicaName",
+ header: {
+ label: "Suffix:ReplicaID",
+ props: {
+ index: 1,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 1
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replicaStatus",
+ header: {
+ label: "Replica Status",
+ props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 2
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "agmt-name",
+ header: {
+ label: "Agreement",
+ props: {
+ index: 3,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 3
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.rowKey}>
+ {value || <i>No Agreements Were Found</i>}
+ </td>
+ ];
+ }
+ ]
+ }
+ },
+ {
+ property: "replica",
+ header: {
+ label: "Consumer",
+ props: {
+ index: 4,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 4
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replica-enabled",
+ header: {
+ label: "Is Enabled",
+ props: {
+ index: 5,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 5
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replication-lag-time",
+ header: {
+ label: "Lag Time",
+ props: {
+ index: 6,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 6
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "actions",
+ header: {
+ props: {
+ index: 7,
+ rowSpan: 1,
+ colSpan: 1
+ },
+ formatters: [actionHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 7
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.rowKey}>
+ <Button
+ onClick={() => {
+ this.props.viewAgmt(rowData['supplierName'][0],
+ rowData['replicaName'][0],
+ rowData['agmt-name'][0]);
+ }}
+ >
+ View Data
+ </Button>
+ </td>
+ ];
+ }
+ ]
+ }
+ }
+ ]
+ };
+ }
+
+ getColumns() {
+ return this.state.columns;
+ }
+
+ getSingleColumn () {
+ return [
+ {
+ property: "msg",
+ header: {
+ label: "All In One Report",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ ];
+ }
+
+ render() {
+ let reportSingleTable;
+ let filteredRows = this.props.rows;
+ if (!this.props.showDisabledAgreements) {
+ filteredRows = searchFilter("on", ["replica-enabled"], filteredRows);
+ }
+ if (filteredRows.length < 1) {
+ reportSingleTable = (
+ <DSShortTable
+ getColumns={this.getSingleColumn}
+ rowKey={"msg"}
+ rows={[{ msg: "No replica entries" }]}
+ disableLoadingSpinner
+ noSearchBar
+ />
+ );
+ } else {
+ reportSingleTable = (
+ <DSShortTable
+ getColumns={this.getColumns}
+ rowKey="rowKey"
+ rows={filteredRows}
+ disableLoadingSpinner
+ noSearchBar
+ />
+ );
+ }
+
+ return <div>{reportSingleTable}</div>;
+ }
+}
+
+class ReportConsumersTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getColumns = this.getColumns.bind(this);
+ this.getSingleColumn = this.getSingleColumn.bind(this);
+
+ this.state = {
+ searchField: "Agreements",
+ fieldsToSearch: [
+ "agmt-name",
+ "replica-enabled",
+ "replication-status",
+ "replication-lag-time"
+ ],
+
+ columns: [
+ {
+ property: "agmt-name",
+ header: {
+ label: "Agreement Name",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replica-enabled",
+ header: {
+ label: "Is Enabled",
+ props: {
+ index: 1,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 1
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replication-status",
+ header: {
+ label: "Replication Status",
+ props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 2
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "replication-lag-time",
+ header: {
+ label: "Replication Lag Time",
+ props: {
index: 3,
rowSpan: 1,
colSpan: 1,
@@ -1678,30 +2442,104 @@ class DiskTable extends React.Component {
formatters: [tableCellFormatter]
}
},
-
+ {
+ property: "actions",
+ header: {
+ props: {
+ index: 4,
+ rowSpan: 1,
+ colSpan: 1
+ },
+ formatters: [actionHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 4
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.rowKey}>
+ <Button
+ onClick={() => {
+ this.props.viewAgmt(rowData['supplierName'][0],
+ rowData['replicaName'][0],
+ rowData['agmt-name'][0]);
+ }}
+ >
+ View Data
+ </Button>
+ </td>
+ ];
+ }
+ ]
+ }
+ }
]
};
- this.getColumns = this.getColumns.bind(this);
}
getColumns() {
return this.state.columns;
}
+ getSingleColumn () {
+ return [
+ {
+ property: "msg",
+ header: {
+ label: "Report Consumers",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ ];
+ }
+
render() {
- return (
- <div className="ds-margin-top-xlg">
+ let reportConsumersTable;
+ let filteredRows = this.props.rows;
+ if (!this.props.showDisabledAgreements) {
+ filteredRows = searchFilter("on", ["replica-enabled"], filteredRows);
+ }
+ if (filteredRows.length < 1) {
+ reportConsumersTable = (
+ <DSShortTable
+ getColumns={this.getSingleColumn}
+ rowKey={"msg"}
+ rows={[{ msg: "No agreement entries" }]}
+ disableLoadingSpinner
+ noSearchBar
+ />
+ );
+ } else {
+ reportConsumersTable = (
<DSShortTable
getColumns={this.getColumns}
- rowKey={this.state.rowKey}
- rows={this.props.disks}
+ rowKey="rowKey"
+ rows={filteredRows}
disableLoadingSpinner
+ noSearchBar
/>
- </div>
- );
+ );
+ }
+
+ return <div>{reportConsumersTable}</div>;
}
}
-
// Proptypes and defaults
LagReportTable.propTypes = {
@@ -1790,6 +2628,62 @@ GlueTable.defaultProps = {
deleteGlue: noop,
};
+ReportCredentialsTable.propTypes = {
+ rows: PropTypes.array,
+ editConfig: PropTypes.func,
+ deleteConfig: PropTypes.func
+};
+
+ReportCredentialsTable.defaultProps = {
+ rows: [],
+ editConfig: noop,
+ deleteConfig: noop
+};
+
+ReportAliasesTable.propTypes = {
+ rows: PropTypes.array,
+ editConfig: PropTypes.func,
+ deleteConfig: PropTypes.func
+};
+
+ReportAliasesTable.defaultProps = {
+ rows: [],
+ editConfig: noop,
+ deleteConfig: noop
+};
+
+ReportConsumersTable.propTypes = {
+ showDisabledAgreements: PropTypes.bool,
+ rows: PropTypes.array,
+ viewAgmt: PropTypes.func
+};
+
+ReportConsumersTable.defaultProps = {
+ showDisabledAgreements: false,
+ rows: [],
+ viewAgmt: noop
+};
+
+ReportSingleTable.propTypes = {
+ showDisabledAgreements: PropTypes.bool,
+ rows: PropTypes.array,
+ viewAgmt: PropTypes.func
+};
+
+ReportSingleTable.defaultProps = {
+ showDisabledAgreements: false,
+ rows: [],
+ viewAgmt: noop
+};
+
+DiskTable.defaultProps = {
+ rows: PropTypes.array
+};
+
+DiskTable.defaultProps = {
+ rows: []
+};
+
export {
ConnectionTable,
AgmtTable,
@@ -1799,5 +2693,9 @@ export {
AbortCleanALLRUVTable,
ConflictTable,
GlueTable,
- DiskTable,
+ ReportCredentialsTable,
+ ReportAliasesTable,
+ ReportConsumersTable,
+ ReportSingleTable,
+ DiskTable
};
diff --git a/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx b/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
index ffbb5cc..482834b 100644
--- a/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
+++ b/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
@@ -5,6 +5,8 @@ import PropTypes from "prop-types";
import "../../css/ds.css";
import { ConfirmPopup } from "../notifications.jsx";
import {
+ ReportCredentialsTable,
+ ReportAliasesTable,
AgmtTable,
WinsyncAgmtTable,
CleanALLRUVTable,
@@ -13,11 +15,13 @@ import {
GlueTable,
} from "./monitorTables.jsx";
import {
+ FullReportContent,
+ ReportLoginModal,
+ ReportCredentialsModal,
+ ReportAliasesModal,
TaskLogModal,
AgmtDetailsModal,
WinsyncAgmtDetailsModal,
- ReplLagReportModal,
- ReplLoginModal,
ConflictCompareModal,
} from "./monitorModals.jsx";
import {
@@ -29,39 +33,151 @@ import {
Button,
noop
} from "patternfly-react";
+import CustomCollapse from "../customCollapse.jsx";
+
+const _ = cockpit.gettext;
export class ReplMonitor extends React.Component {
+ componentDidUpdate(prevProps, prevState) {
+ if (!(prevState.showReportLoginModal) && (this.state.showReportLoginModal)) {
+ // When the login modal turned on
+ // We set timeout to close it and stop the report
+ if (this.timer) window.clearTimeout(this.timer);
+
+ this.timer = window.setTimeout(() => {
+ this.setState({
+ showFullReportModal: false
+ });
+ this.timer = null;
+ }, 300);
+ }
+ if ((prevState.showReportLoginModal) && !(this.state.showReportLoginModal)) {
+ // When the login modal turned off
+ // We clear the timeout
+ if (this.timer) window.clearTimeout(this.timer);
+ }
+ }
+
+ componentWillUnmount() {
+ // It's important to do so we don't get the error
+ // on the unmounted component
+ if (this.timer) window.clearTimeout(this.timer);
+ }
+
+ componentDidMount() {
+ if (this.state.initCreds) {
+ let cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "config", "get", "nsslapd-port", "nsslapd-localhost", "nsslapd-rootdn"];
+ log_cmd("ReplMonitor", "add credentials during componentDidMount", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ let config = JSON.parse(content);
+ this.setState(prevState => ({
+ credentialsList: [
+ ...prevState.credentialsList,
+ {
+ connData: `${config.attrs["nsslapd-localhost"]}:${config.attrs["nsslapd-port"]}`,
+ credsBinddn: config.attrs["nsslapd-rootdn"],
+ credsBindpw: "",
+ pwInputInterractive: true
+ }
+ ]
+ }));
+ for (let agmt of this.props.data.replAgmts) {
+ this.setState(prevState => ({
+ credentialsList: [
+ ...prevState.credentialsList,
+ {
+ connData: `${agmt.replica}`,
+ credsBinddn: config.attrs["nsslapd-rootdn"],
+ credsBindpw: "",
+ pwInputInterractive: true
+ }
+ ],
+ initCreds: false
+ }));
+ }
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to get config nsslapd-port, nsslapd-localhost and nasslapd-rootdn: ${errMsg.desc}`
+ );
+ });
+ }
+ this.props.enableTree();
+ }
+
constructor (props) {
super(props);
this.state = {
activeKey: 1,
+ activeReportKey: 1,
logData: "",
showBindModal: false,
showLogModal: false,
showAgmtModal: false,
+ isRemoteAgmt: false,
+ showFullReportModal: false,
+ showReportLoginModal: false,
+ showCredentialsModal: false,
+ showAliasesModal: false,
showWinsyncAgmtModal: false,
showInitWinsyncConfirm: false,
showInitConfirm: false,
- showLoginModal: false,
- showLagReport: false,
showCompareModal: false,
showConfirmDeleteGlue: false,
showConfirmConvertGlue: false,
showConfirmSwapConflict: false,
showConfirmConvertConflict: false,
showConfirmDeleteConflict: false,
- reportLoading: false,
lagAgmts: [],
+ credsData: [],
+ aliasData: [],
+ reportData: [],
agmt: "",
convertRDN: "",
glueEntry: "",
conflictEntry: "",
binddn: "cn=Directory Manager",
bindpw: "",
- errObj: {}
+ errObj: {},
+ aliasList: [],
+ newEntry: false,
+ initCreds: true,
+
+ fullReportProcess: {},
+ interruptLoginCredsInput: false,
+ doFullReportCleanup: false,
+ reportRefreshing: false,
+ reportLoading: false,
+
+ credsInstanceName: "",
+ disableBinddn: false,
+ loginBinddn: "",
+ loginBindpw: "",
+ inputLoginData: false,
+
+ credsHostname: "",
+ credsPort: "",
+ credsBinddn: "cn=Directory Manager",
+ credsBindpw: "",
+ pwInputInterractive: false,
+
+ aliasHostname: "",
+ aliasPort: 389,
+ aliasName: "",
+
+ credentialsList: [],
+ dynamicCredentialsList: [],
+ aliasesList: []
};
+ this.handleFieldChange = this.handleFieldChange.bind(this);
this.handleNavSelect = this.handleNavSelect.bind(this);
+ this.handleReportNavSelect = this.handleReportNavSelect.bind(this);
this.pokeAgmt = this.pokeAgmt.bind(this);
this.initAgmt = this.initAgmt.bind(this);
this.initWinsyncAgmt = this.initWinsyncAgmt.bind(this);
@@ -71,17 +187,37 @@ export class ReplMonitor extends React.Component {
this.closeInitWinsyncConfirm = this.closeInitWinsyncConfirm.bind(this);
this.pokeWinsyncAgmt = this.pokeWinsyncAgmt.bind(this);
this.showAgmtModal = this.showAgmtModal.bind(this);
+ this.showAgmtModalRemote = this.showAgmtModalRemote.bind(this);
this.closeAgmtModal = this.closeAgmtModal.bind(this);
this.showWinsyncAgmtModal = this.showWinsyncAgmtModal.bind(this);
this.closeWinsyncAgmtModal = this.closeWinsyncAgmtModal.bind(this);
- this.getLagReportCreds = this.getLagReportCreds.bind(this);
- this.doLagReport = this.doLagReport.bind(this);
- this.closeLagReport = this.closeLagReport.bind(this);
this.viewCleanLog = this.viewCleanLog.bind(this);
this.viewAbortLog = this.viewAbortLog.bind(this);
this.closeLogModal = this.closeLogModal.bind(this);
- this.handleLoginModal = this.handleLoginModal.bind(this);
- this.closeLoginModal = this.closeLoginModal.bind(this);
+ this.closeReportLoginModal = this.closeReportLoginModal.bind(this);
+
+ // Replication report functions
+ this.addCreds = this.addCreds.bind(this);
+ this.editCreds = this.editCreds.bind(this);
+ this.removeCreds = this.removeCreds.bind(this);
+ this.openCredsModal = this.openCredsModal.bind(this);
+ this.showAddCredsModal = this.showAddCredsModal.bind(this);
+ this.showEditCredsModal = this.showEditCredsModal.bind(this);
+ this.closeCredsModal = this.closeCredsModal.bind(this);
+
+ this.addAliases = this.addAliases.bind(this);
+ this.editAliases = this.editAliases.bind(this);
+ this.removeAliases = this.removeAliases.bind(this);
+ this.openAliasesModal = this.openAliasesModal.bind(this);
+ this.showAddAliasesModal = this.showAddAliasesModal.bind(this);
+ this.showEditAliasesModal = this.showEditAliasesModal.bind(this);
+ this.closeAliasesModal = this.closeAliasesModal.bind(this);
+
+ this.doFullReport = this.doFullReport.bind(this);
+ this.processCredsInput = this.processCredsInput.bind(this);
+ this.closeReportModal = this.closeReportModal.bind(this);
+ this.refreshFullReport = this.refreshFullReport.bind(this);
+
// Conflict entry functions
this.convertConflict = this.convertConflict.bind(this);
this.swapConflict = this.swapConflict.bind(this);
@@ -105,8 +241,18 @@ export class ReplMonitor extends React.Component {
this.closeConfirmSwapConflict = this.closeConfirmSwapConflict.bind(this);
}
- componentDidMount() {
- this.props.enableTree();
+ handleFieldChange(e) {
+ let value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
+ if (e.target.type === 'number') {
+ if (e.target.value) {
+ value = parseInt(e.target.value);
+ } else {
+ value = 1;
+ }
+ }
+ this.setState({
+ [e.target.id]: value
+ });
}
convertConflict (dn) {
@@ -341,6 +487,12 @@ export class ReplMonitor extends React.Component {
});
}
+ handleReportNavSelect(key) {
+ this.setState({
+ activeReportKey: key
+ });
+ }
+
closeLogModal() {
this.setState({
showLogModal: false
@@ -422,6 +574,7 @@ export class ReplMonitor extends React.Component {
if (agmt['agmt-name'] == name) {
this.setState({
showAgmtModal: true,
+ isRemoteAgmt: false,
agmt: agmt
});
break;
@@ -429,6 +582,34 @@ export class ReplMonitor extends React.Component {
}
}
+ showAgmtModalRemote (supplierName, replicaName, agmtName) {
+ if (!agmtName) {
+ this.props.addNotification(
+ "error",
+ `The agreement doesn't exist!`
+ );
+ } else {
+ for (let supplier of this.state.reportData) {
+ if (supplier.name == supplierName) {
+ for (let replica of supplier.data) {
+ if (`${replica.replica_root}:${replica.replica_id}` == replicaName) {
+ for (let agmt of replica.agmts_status) {
+ if (agmt['agmt-name'][0] == agmtName) {
+ this.setState({
+ showAgmtModal: true,
+ isRemoteAgmt: true,
+ agmt: agmt
+ });
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
closeAgmtModal() {
this.setState({
showAgmtModal: false,
@@ -533,108 +714,507 @@ export class ReplMonitor extends React.Component {
});
}
- getLagReportCreds () {
- if (this.props.data.replAgmts.length == 0) {
- // No agreements, don't proceed...
- this.props.addNotification(
- "error", "There are no replication agreements to report on"
- );
+ handleConvertChange(e) {
+ const value = e.target.value;
+ this.setState({
+ convertRDN: value,
+ });
+ }
+
+ changeCreds(action) {
+ const { credentialsList, oldCredsHostname, oldCredsPort, credsHostname,
+ credsPort, credsBinddn, credsBindpw, pwInputInterractive } = this.state;
+
+ if (credsHostname === "" || credsPort === "" || credsBinddn === "") {
+ this.props.addNotification("warning", "Host, Port, and Bind DN are required.");
+ } else if (credsBindpw === "" && !pwInputInterractive) {
+ this.props.addNotification("warning", "Password field can't be empty, if Interractive Input is not selected");
} else {
- this.setState({
- showLoginModal: true,
- errObj: {
- bindpw: true
- }
- });
+ let credsExist = false;
+ if ((action == "add") && (credentialsList.some(row => row.connData === `${credsHostname}:${credsPort}`))) {
+ credsExist = true;
+ }
+ if ((action == "edit") && (credentialsList.some(row => row.connData === `${oldCredsHostname}:${oldCredsPort}`))) {
+ this.setState({
+ credentialsList: credentialsList.filter(
+ row => row.connData !== `${oldCredsHostname}:${oldCredsPort}`
+ )
+ });
+ }
+
+ if (!credsExist) {
+ this.setState(prevState => ({
+ credentialsList: [
+ ...prevState.credentialsList,
+ {
+ connData: `${credsHostname}:${credsPort}`,
+ credsBinddn: credsBinddn,
+ credsBindpw: credsBindpw,
+ pwInputInterractive: pwInputInterractive
+ }
+ ]
+ }));
+ } else {
+ this.props.addNotification(
+ "error",
+ `Credentials "${credsHostname}:${credsPort}" already exists`
+ );
+ }
+ this.closeCredsModal();
}
}
- closeLoginModal () {
+ addCreds() {
+ this.changeCreds("add");
+ }
+
+ editCreds() {
+ this.changeCreds("edit");
+ }
+
+ removeCreds(rowData) {
this.setState({
- showLoginModal: false,
+ credentialsList: this.state.credentialsList.filter(
+ row => row.connData !== rowData.connData
+ )
});
}
- handleLoginModal(e) {
- const value = e.target.value.trim();
- let valueErr = false;
- let errObj = this.state.errObj;
- if (value == "") {
- valueErr = true;
- }
- errObj[e.target.id] = valueErr;
+ openCredsModal() {
this.setState({
- [e.target.id]: value,
- errObj: errObj
+ showCredentialsModal: true
});
}
- closeLagReport() {
+ showAddCredsModal() {
+ this.openCredsModal();
this.setState({
- showLagReport: false
+ newEntry: true,
+ oldCredsHostname: "",
+ oldCredsPort: "",
+ credsHostname: "",
+ credsPort: "",
+ credsBinddn: "cn=Directory Manager",
+ credsBindpw: "",
+ pwInputInterractive: false
});
}
- doLagReport() {
- // Get agmts but this time with with bind credentials, then clear
- // out bind credentials after we use them
+ showEditCredsModal(rowData) {
+ this.openCredsModal();
+ this.setState({
+ newEntry: false,
+ oldCredsHostname: rowData.connData.split(':')[0],
+ oldCredsPort: rowData.connData.split(':')[1],
+ credsHostname: rowData.connData.split(':')[0],
+ credsPort: rowData.connData.split(':')[1],
+ credsBinddn: rowData.credsBinddn,
+ credsBindpw: rowData.credsBindpw,
+ pwInputInterractive: rowData.pwInputInterractive
+ });
+ }
- if (this.state.binddn == "" || this.state.bindpw == "") {
- return;
+ closeCredsModal() {
+ this.setState({
+ showCredentialsModal: false
+ });
+ }
+
+ changeAlias(action) {
+ const { aliasesList, aliasHostname, aliasPort, oldAliasName, aliasName } = this.state;
+
+ if (aliasPort === "" || aliasHostname === "" || aliasName === "") {
+ this.props.addNotification("warning", "Host, Port, and Alias are required.");
+ } else {
+ let aliasExists = false;
+ if ((action == "add") && (aliasesList.some(row => row.alias === aliasName))) {
+ aliasExists = true;
+ }
+ if ((action == "edit") && (aliasesList.some(row => row.alias === oldAliasName))) {
+ this.setState({
+ aliasesList: aliasesList.filter(row => row.alias !== oldAliasName)
+ });
+ }
+
+ if (!aliasExists) {
+ this.setState(prevState => ({
+ aliasesList: [
+ ...prevState.aliasesList,
+ {
+ connData: `${aliasHostname}:${aliasPort}`,
+ alias: aliasName
+ }
+ ]
+ }));
+ } else {
+ this.props.addNotification("error", `Alias "${aliasName}" already exists`);
+ }
+ this.closeAliasesModal();
}
+ }
+ addAliases() {
+ this.changeAlias("add");
+ }
+
+ editAliases() {
+ this.changeAlias("edit");
+ }
+
+ removeAliases(rowData) {
this.setState({
- loginSpinning: true,
+ aliasesList: this.state.aliasesList.filter(row => row.alias !== rowData.alias)
});
+ }
- let cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
- "replication", "status", "--suffix=" + this.props.suffix,
- "--bind-dn=" + this.state.binddn, "--bind-passwd=" + this.state.bindpw];
- log_cmd("doLagReport", "Get agmts for lag report", cmd);
- cockpit
- .spawn(cmd, { superuser: true, err: "message" })
- .done(content => {
- let config = JSON.parse(content);
+ openAliasesModal() {
+ this.setState({
+ showAliasesModal: true,
+ });
+ }
+
+ showAddAliasesModal() {
+ this.openAliasesModal();
+ this.setState({
+ newEntry: true,
+ aliasHostname: "",
+ aliasPort: 389,
+ oldAliasName: "",
+ aliasName: ""
+ });
+ }
+
+ showEditAliasesModal(rowData) {
+ this.openAliasesModal();
+ this.setState({
+ newEntry: false,
+ aliasHostname: rowData.connData.split(':')[0],
+ aliasPort: parseInt(rowData.connData.split(':')[1]),
+ oldAliasName: rowData.alias,
+ aliasName: rowData.alias
+ });
+ }
+
+ closeAliasesModal() {
+ this.setState({
+ showAliasesModal: false
+ });
+ }
+
+ refreshFullReport() {
+ this.doFullReport();
+ this.setState({
+ reportRefreshing: true
+ });
+ }
+
+ doFullReport() {
+ // Initiate the report and continue the processing in the input window
+ this.setState({
+ reportLoading: true,
+ activeReportKey: 2
+ });
+
+ let password = "";
+ let credentials = [];
+ let printCredentials = [];
+ for (let row of this.state.credentialsList) {
+ if (row.pwInputInterractive) {
+ password = "*";
+ } else {
+ password = `${row.credsBindpw}`;
+ }
+ credentials.push(`${row.connData}:${row.credsBinddn}:${password}`);
+ printCredentials.push(`${row.connData}:${row.credsBinddn}:********`);
+ }
+
+ let aliases = [];
+ for (let row of this.state.aliasesList) {
+ aliases.push(`${row.alias}=${row.connData}`);
+ }
+
+ let buffer = "";
+ let cmd = [
+ "dsconf",
+ "-j",
+ "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "replication",
+ "monitor"
+ ];
+
+ if (aliases.length != 0) {
+ cmd = [...cmd, "-a"];
+ for (let value of aliases) {
+ cmd = [...cmd, value];
+ }
+ }
+
+ // We should not print the passwords to console.log
+ let printCmd = cmd;
+ if (credentials.length != 0) {
+ cmd = [...cmd, "-c"];
+ for (let value of credentials) {
+ cmd = [...cmd, value];
+ }
+ printCmd = [...printCmd, "-c"];
+ for (let value of printCredentials) {
+ printCmd = [...printCmd, value];
+ }
+ }
+
+ log_cmd("doFullReport", "Get the report for the current instance topology", printCmd);
+ // We need to set it here because 'input' will be run from inside
+ let proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], superuser: true, err: "message", directory: self.path });
+ // We use it in processCredsInput
+ this.setState({
+ fullReportProcess: proc
+ });
+ proc
+ .done(data => {
+ // Use the buffer from stream. 'data' is empty
+ let report = JSON.parse(buffer);
+ // We need to reparse the report data because agmts json wasn't parsed correctly because it was too nested
+ let agmts_reparsed = [];
+ let replica_reparsed = [];
+ let supplier_reparsed = [];
+ for (let supplier of report.items) {
+ replica_reparsed = [];
+ for (let replica of supplier.data) {
+ agmts_reparsed = [];
+ let agmts_done = false;
+ if (replica.hasOwnProperty("agmts_status")) {
+ for (let agmt of replica.agmts_status) {
+ // We need this for Agreement View Modal
+ agmt["supplierName"] = [supplier.name];
+ agmt["replicaName"] = [`${replica.replica_root}:${replica.replica_id}`];
+ agmt["replicaStatus"] = [`${replica.replica_status}`];
+ agmt["rowKey"] = [`${supplier.name}:${replica.replica_root}:${replica.replica_id}:${agmt["agmt-name"]}`];
+ agmts_reparsed.push(agmt);
+ agmts_done = true;
+ }
+ }
+ if (!agmts_done) {
+ let agmt_empty = {};
+ agmt_empty["supplierName"] = [supplier.name];
+ if (replica.replica_root || replica.replica_id) {
+ agmt_empty["replicaName"] = [`${replica.replica_root || ""}:${replica.replica_id || ""}`];
+ } else {
+ agmt_empty["replicaName"] = [""];
+ }
+ agmt_empty["replicaStatus"] = [`${replica.replica_status}`];
+ agmt_empty["rowKey"] = [`${supplier.name}:${replica.replica_root}:${replica.replica_id}:None`];
+ agmts_reparsed.push(agmt_empty);
+ }
+ replica_reparsed.push({...replica, agmts_status: agmts_reparsed});
+ }
+ supplier_reparsed.push({...supplier, data: replica_reparsed});
+ }
+ const report_reparsed = {...report, items: supplier_reparsed};
this.setState({
- lagAgmts: config.items,
- showLagReport: true,
- showLoginModal: false,
- loginSpinning: false,
- bindpw: ""
+ reportData: report_reparsed.items,
+ showFullReportModal: true,
+ reportLoading: false,
+ doFullReportCleanup: true
});
})
- .fail(err => {
- let errMsg = JSON.parse(err);
+ .fail(_ => {
+ let errMsg = JSON.parse(buffer);
this.props.addNotification(
"error",
- `Failed to get replication status - ${errMsg.desc}`
+ `Sync report has failed - ${errMsg.desc}`
);
this.setState({
- showLoginModal: false,
- loginSpinning: false,
- bindpw: ""
+ dynamicCredentialsList: [],
+ reportLoading: false,
+ doFullReportCleanup: true,
+ activeReportKey: 1
});
+ })
+ // Stream is run each time as a new character arriving
+ .stream(data => {
+ buffer += data;
+ let lines = buffer.split("\n");
+ let last_line = lines[lines.length - 1];
+ let found_creds = false;
+
+ // Interractive Input is required
+ // Check for Bind DN first
+ if (last_line.startsWith("Enter a bind DN") && last_line.endsWith(": ")) {
+ buffer = "";
+ // Get the instance name. We need it for fetching the creds data from stored state list
+ this.setState({
+ credsInstanceName: data.split("a bind DN for ")[1].split(": ")[0]
+ });
+ // First check if DN is in the list already (either from previous run or during this execution)
+ for (let creds of this.state.dynamicCredentialsList) {
+ if (creds.credsInstanceName == this.state.credsInstanceName) {
+ found_creds = true;
+ proc.input(`${creds.binddn}\n`, true);
+ }
+ }
+
+ // If we don't have the creds - open the modal window and ask the user for input
+ if (!found_creds) {
+ this.setState({
+ showReportLoginModal: true,
+ binddnRequired: true,
+ disableBinddn: false,
+ credsInstanceName: this.state.credsInstanceName,
+ loginBinddn: "",
+ loginBindpw: ""
+ });
+ }
+
+ // Check for password
+ } else if (last_line.startsWith("Enter a password") && last_line.endsWith(": ")) {
+ buffer = "";
+ // Do the same logic for password but the string parsing is different
+ this.setState({
+ credsInstanceName: data.split(" on ")[1].split(": ")[0]
+ });
+ for (let creds of this.state.dynamicCredentialsList) {
+ if (creds.credsInstanceName == this.state.credsInstanceName) {
+ found_creds = true;
+ proc.input(`${creds.bindpw}\n`, true);
+ this.setState({
+ credsInstanceName: ""
+ });
+ }
+ }
+
+ if (!found_creds) {
+ this.setState({
+ showReportLoginModal: true,
+ bindpwRequired: true,
+ credsInstanceName: this.state.credsInstanceName,
+ disableBinddn: true,
+ loginBinddn: data.split("Enter a password for ")[1].split(" on")[0],
+ loginBindpw: ""
+ });
+ }
+ }
});
}
- handleConvertChange(e) {
- const value = e.target.value;
+ closeReportLoginModal() {
this.setState({
- convertRDN: value,
+ showReportLoginModal: false,
+ reportLoading: false,
+ activeReportKey: 1
+ });
+ }
+
+ processCredsInput() {
+ const {
+ loginBinddn,
+ loginBindpw,
+ credsInstanceName,
+ fullReportProcess
+ } = this.state;
+
+ if (loginBinddn == "" || loginBindpw == "") {
+ this.props.addNotification("warning", "Bind DN and password are required.");
+ } else {
+ this.setState({
+ showReportLoginModal: false,
+ reportLoading: false
+ });
+
+ // Store the temporary data in state
+ this.setState(prevState => ({
+ dynamicCredentialsList: [
+ ...prevState.dynamicCredentialsList,
+ {
+ binddn: loginBinddn,
+ bindpw: loginBindpw,
+ credsInstanceName: credsInstanceName
+ }
+ ]
+ }));
+
+ // We wait for some input - put the right one here
+ if (this.state.binddnRequired) {
+ fullReportProcess.input(`${loginBinddn}\n`, true);
+ this.setState({
+ binddnRequired: false
+ });
+ } else if (this.state.bindpwRequired) {
+ fullReportProcess.input(`${loginBindpw}\n`, true);
+ this.setState({
+ bindpwRequired: false
+ });
+ }
+ }
+ }
+
+ closeReportModal() {
+ this.setState({
+ showFullReportModal: false,
+ reportLoading: false
});
}
render() {
+ let reportData = this.state.reportData;
+ let credentialsList = this.state.credentialsList;
+ let aliasesList = this.state.aliasesList;
let replAgmts = this.props.data.replAgmts;
let replWinsyncAgmts = this.props.data.replWinsyncAgmts;
let cleanTasks = this.props.data.cleanTasks;
let abortTasks = this.props.data.abortTasks;
let conflictEntries = this.props.data.conflicts;
let glueEntries = this.props.data.glues;
+ let fullReportModal = "";
+ let reportLoginModal = "";
+ let reportCredentialsModal = "";
+ let reportAliasesModal = "";
let agmtDetailModal = "";
let winsyncAgmtDetailModal = "";
let compareConflictModal = "";
+ if (this.state.showReportLoginModal) {
+ reportLoginModal =
+ <ReportLoginModal
+ showModal={this.state.showReportLoginModal}
+ closeHandler={this.closeReportLoginModal}
+ handleChange={this.handleFieldChange}
+ processCredsInput={this.processCredsInput}
+ instanceName={this.state.credsInstanceName}
+ disableBinddn={this.state.disableBinddn}
+ loginBinddn={this.state.loginBinddn}
+ loginBindpw={this.state.loginBindpw}
+ />;
+ }
+ if (this.state.showCredentialsModal) {
+ reportCredentialsModal =
+ <ReportCredentialsModal
+ showModal={this.state.showCredentialsModal}
+ closeHandler={this.closeCredsModal}
+ handleFieldChange={this.handleFieldChange}
+ newEntry={this.state.newEntry}
+ hostname={this.state.credsHostname}
+ port={this.state.credsPort}
+ binddn={this.state.credsBinddn}
+ bindpw={this.state.credsBindpw}
+ pwInputInterractive={this.state.pwInputInterractive}
+ addConfig={this.addCreds}
+ editConfig={this.editCreds}
+ />;
+ }
+ if (this.state.showAliasesModal) {
+ reportAliasesModal =
+ <ReportAliasesModal
+ showModal={this.state.showAliasesModal}
+ closeHandler={this.closeAliasesModal}
+ handleFieldChange={this.handleFieldChange}
+ newEntry={this.state.newEntry}
+ hostname={this.state.aliasHostname}
+ port={this.state.aliasPort}
+ alias={this.state.aliasName}
+ addConfig={this.addAliases}
+ editConfig={this.editAliases}
+ />;
+ }
if (this.state.showAgmtModal) {
agmtDetailModal =
<AgmtDetailsModal
@@ -642,6 +1222,7 @@ export class ReplMonitor extends React.Component {
closeHandler={this.closeAgmtModal}
agmt={this.state.agmt}
initAgmt={this.confirmInit}
+ isRemoteAgmt={this.state.isRemoteAgmt}
/>;
}
@@ -668,6 +1249,105 @@ export class ReplMonitor extends React.Component {
/>;
}
+ let reportContent =
+ <div>
+ <Nav bsClass="nav nav-tabs nav-tabs-pf">
+ <NavItem className="ds-nav-med" eventKey={1}>
+ {_("Prepare")}
+ </NavItem>
+ <NavItem className="ds-nav-med" eventKey={2}>
+ {_("Result")}
+ </NavItem>
+ </Nav>
+ <TabContent>
+ <TabPane eventKey={1}>
+ <div className="ds-indent ds-margin-top-lg">
+ <CustomCollapse textClosed="Show Help" textOpened="Hide Help" className="h3">
+ <h3>How To Use Replication Sync Report</h3>
+ <ol className="ds-left-indent">
+ <li>
+ Fill in <b>Replica Credentials</b>;
+ <ul>
+ <li>• Initially, the list is populated with existing instance agreements and the active instance itself;</li>
+ <li>• You can use regular expressions for the <b>Connection Data</b> field;</li>
+ <li>• It is advised to use an <b>Interactive Input</b> option for a password because it's more secure.</li>
+ </ul>
+ </li>
+ <li>
+ Add <b>Instance Aliases</b> if needed;
+ <ul>
+ <li>• Adding the aliases will make the report more readable;</li>
+ <li>• Each instance can have one alias. For example, you can give names like this:
+ <b> Alias</b>=Main Master, <b>Hostname</b>=192.168.122.01, <b>Port</b>=38901;</li>
+ <li>• In a result, the report will have an entry like this:
+ <b> Supplier: Main Master (192.168.122.01:38901)</b>.</li>
+ </ul>
+ </li>
+ <li>
+ Press <b>Generate Report</b> button;
+ <ul>
+ <li>• It will initiate the report creation;</li>
+ <li>• You may be asked for the credentials while the process is running through the agreements.</li>
+ </ul>
+ </li>
+ <li>
+ Once report is generated you can review it and enable continuous refreshing.
+ <ul>
+ <li>• More consumer replication data is available under the 'View Data' button;</li>
+ <li>• You can set the timeout and the new report will be created by that;</li>
+ <li>• It will use the specified credentials (both preset and from interactive input).</li>
+ </ul>
+ </li>
+ </ol>
+ </CustomCollapse>
+ <ReportCredentialsTable
+ rows={credentialsList}
+ deleteConfig={this.removeCreds}
+ editConfig={this.showEditCredsModal}
+ />
+ <Button
+ className="ds-margin-top"
+ bsStyle="default"
+ onClick={this.showAddCredsModal}
+ >
+ Add Credentials
+ </Button>
+ <ReportAliasesTable
+ rows={aliasesList}
+ deleteConfig={this.removeAliases}
+ editConfig={this.showEditAliasesModal}
+ />
+ <Button
+ className="ds-margin-top"
+ bsStyle="default"
+ onClick={this.showAddAliasesModal}
+ >
+ Add Alias
+ </Button>
+ <p />
+ <Button
+ className="ds-margin-top"
+ bsStyle="primary"
+ onClick={this.doFullReport}
+ title="Use the specified credentials and display full topology report"
+ >
+ Generate Report
+ </Button>
+ </div>
+ </TabPane>
+ <TabPane eventKey={2}>
+ <div className="ds-indent ds-margin-top-lg">
+ <FullReportContent
+ reportData={reportData}
+ viewAgmt={this.showAgmtModalRemote}
+ handleRefresh={this.refreshFullReport}
+ reportRefreshing={this.state.reportRefreshing}
+ reportLoading={this.state.reportLoading}
+ />
+ </div>
+ </TabPane>
+ </TabContent>
+ </div>;
let cleanNavTitle = 'CleanAllRUV Tasks <font size="1">(' + cleanTasks.length + ')</font>';
let abortNavTitle = 'Abort CleanAllRUV Tasks <font size="1">(' + abortTasks.length + ')</font>';
let taskContent =
@@ -750,8 +1430,9 @@ export class ReplMonitor extends React.Component {
</TabContent>
</div>;
- let replAgmtNavTitle = 'Replication Agreements <font size="1">(' + replAgmts.length + ')</font>';
- let winsyncNavTitle = 'Winsync Agreements <font size="1">(' + replWinsyncAgmts.length + ')</font>';
+ let fullReportTitle = 'Sync Report';
+ let replAgmtNavTitle = 'Agreements <font size="1">(' + replAgmts.length + ')</font>';
+ let winsyncNavTitle = 'Winsync <font size="1">(' + replWinsyncAgmts.length + ')</font>';
let tasksNavTitle = 'Tasks <font size="1">(' + (cleanTasks.length + abortTasks.length) + ')</font>';
let conflictsNavTitle = 'Conflicts <font size="1">(' + (conflictEntries.length + glueEntries.length) + ')</font>';
@@ -761,37 +1442,44 @@ export class ReplMonitor extends React.Component {
<div>
<Nav bsClass="nav nav-tabs nav-tabs-pf">
<NavItem eventKey={1}>
- <div dangerouslySetInnerHTML={{__html: replAgmtNavTitle}} />
+ <div dangerouslySetInnerHTML={{__html: fullReportTitle}} />
</NavItem>
<NavItem eventKey={2}>
- <div dangerouslySetInnerHTML={{__html: winsyncNavTitle}} />
+ <div dangerouslySetInnerHTML={{__html: replAgmtNavTitle}} />
</NavItem>
<NavItem eventKey={3}>
- <div dangerouslySetInnerHTML={{__html: tasksNavTitle}} />
+ <div dangerouslySetInnerHTML={{__html: winsyncNavTitle}} />
</NavItem>
<NavItem eventKey={4}>
+ <div dangerouslySetInnerHTML={{__html: tasksNavTitle}} />
+ </NavItem>
+ <NavItem eventKey={5}>
<div dangerouslySetInnerHTML={{__html: conflictsNavTitle}} />
</NavItem>
</Nav>
<TabContent>
<TabPane eventKey={1}>
<div className="ds-indent ds-tab-table">
+ <TabContainer
+ id="task-tabs"
+ defaultActiveKey={1}
+ onSelect={this.handleReportNavSelect}
+ activeKey={this.state.activeReportKey}
+ >
+ {reportContent}
+ </TabContainer>
+ </div>
+ </TabPane>
+ <TabPane eventKey={2}>
+ <div className="ds-indent ds-tab-table">
<AgmtTable
agmts={replAgmts}
pokeAgmt={this.pokeAgmt}
viewAgmt={this.showAgmtModal}
/>
- <Button
- className="ds-margin-top"
- bsStyle="primary"
- onClick={this.getLagReportCreds}
- title="Display report that shows the lag time and replication status of each agreement in relationship to its replica"
- >
- Get Lag Report
- </Button>
</div>
</TabPane>
- <TabPane eventKey={2}>
+ <TabPane eventKey={3}>
<div className="dds-indent ds-tab-table">
<WinsyncAgmtTable
agmts={replWinsyncAgmts}
@@ -800,14 +1488,14 @@ export class ReplMonitor extends React.Component {
/>
</div>
</TabPane>
- <TabPane eventKey={3}>
+ <TabPane eventKey={4}>
<div className="ds-indent ds-tab-table">
<TabContainer id="task-tabs" defaultActiveKey={1}>
{taskContent}
</TabContainer>
</div>
</TabPane>
- <TabPane eventKey={4}>
+ <TabPane eventKey={5}>
<div className="ds-indent ds-tab-table">
<TabContainer id="task-tabs" defaultActiveKey={1}>
{conflictContent}
@@ -838,21 +1526,6 @@ export class ReplMonitor extends React.Component {
msg="Are you really sure you want to reinitialize this replication winsync agreement?"
msgContent={this.state.agmt['agmt-name']}
/>
- <ReplLoginModal
- showModal={this.state.showLoginModal}
- closeHandler={this.closeLoginModal}
- handleChange={this.handleLoginModal}
- doReport={this.doLagReport}
- spinning={this.state.loginSpinning}
- error={this.state.errObj}
- />
- <ReplLagReportModal
- showModal={this.state.showLagReport}
- closeHandler={this.closeLagReport}
- agmts={this.state.lagAgmts}
- pokeAgmt={this.pokeAgmt}
- viewAgmt={this.showAgmtModal}
- />
<ConfirmPopup
showModal={this.state.showConfirmDeleteGlue}
closeHandler={this.closeConfirmDeleteGlue}
@@ -893,6 +1566,10 @@ export class ReplMonitor extends React.Component {
msg="Are you really sure you want to delete this conflict entry?"
msgContent={this.state.conflictEntry}
/>
+ {fullReportModal}
+ {reportLoginModal}
+ {reportCredentialsModal}
+ {reportAliasesModal}
{agmtDetailModal}
{winsyncAgmtDetailModal}
{compareConflictModal}
diff --git a/src/cockpit/389-console/src/lib/tools.jsx b/src/cockpit/389-console/src/lib/tools.jsx
index b37dd86..631135e 100644
--- a/src/cockpit/389-console/src/lib/tools.jsx
+++ b/src/cockpit/389-console/src/lib/tools.jsx
@@ -4,9 +4,11 @@ export function searchFilter(searchFilterValue, columnsToSearch, rows) {
rows.forEach(row => {
let rowToSearch = [];
if (columnsToSearch && columnsToSearch.length) {
- columnsToSearch.forEach(column =>
- rowToSearch.push(row[column])
- );
+ columnsToSearch.forEach(column => {
+ if (column in row) {
+ rowToSearch.push(row[column]);
+ }
+ });
} else {
rowToSearch = row;
}
diff --git a/src/cockpit/389-console/src/monitor.jsx b/src/cockpit/389-console/src/monitor.jsx
index fd2bc0d..1c97497 100644
--- a/src/cockpit/389-console/src/monitor.jsx
+++ b/src/cockpit/389-console/src/monitor.jsx
@@ -828,6 +828,9 @@ export class Monitor extends React.Component {
replLoading: false,
});
});
+ } else {
+ // We should enable it here because ReplMonitor never will be mounted
+ this.enableTree();
}
}
diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py
index 93fd728..dfffbed 100644
--- a/src/lib389/lib389/agreement.py
+++ b/src/lib389/lib389/agreement.py
@@ -372,21 +372,21 @@ class Agreement(DSLdapObject):
# Case sensitive?
if use_json:
result = {
- 'agmt-name': ensure_str(status_attrs_dict['cn'][0]),
- 'replica': consumer,
- 'replica-enabled': ensure_str(status_attrs_dict['nsds5replicaenabled'][0]),
- 'update-in-progress': ensure_str(status_attrs_dict['nsds5replicaupdateinprogress'][0]),
- 'last-update-start': ensure_str(status_attrs_dict['nsds5replicalastupdatestart'][0]),
- 'last-update-end': ensure_str(status_attrs_dict['nsds5replicalastupdateend'][0]),
- 'number-changes-sent': ensure_str(status_attrs_dict['nsds5replicachangessentsincestartup'][0]),
- 'number-changes-skipped:': ensure_str(status_attrs_dict['nsds5replicachangesskippedsince'][0]),
- 'last-update-status': ensure_str(status_attrs_dict['nsds5replicalastupdatestatus'][0]),
- 'last-init-start': ensure_str(status_attrs_dict['nsds5replicalastinitstart'][0]),
- 'last-init-end': ensure_str(status_attrs_dict['nsds5replicalastinitend'][0]),
- 'last-init-status': ensure_str(status_attrs_dict['nsds5replicalastinitstatus'][0]),
- 'reap-active': ensure_str(status_attrs_dict['nsds5replicareapactive'][0]),
- 'replication-status': status,
- 'replication-lag-time': lag_time
+ 'agmt-name': ensure_list_str(status_attrs_dict['cn']),
+ 'replica': [consumer],
+ 'replica-enabled': ensure_list_str(status_attrs_dict['nsds5replicaenabled']),
+ 'update-in-progress': ensure_list_str(status_attrs_dict['nsds5replicaupdateinprogress']),
+ 'last-update-start': ensure_list_str(status_attrs_dict['nsds5replicalastupdatestart']),
+ 'last-update-end': ensure_list_str(status_attrs_dict['nsds5replicalastupdateend']),
+ 'number-changes-sent': ensure_list_str(status_attrs_dict['nsds5replicachangessentsincestartup']),
+ 'number-changes-skipped': ensure_list_str(status_attrs_dict['nsds5replicachangesskippedsince']),
+ 'last-update-status': ensure_list_str(status_attrs_dict['nsds5replicalastupdatestatus']),
+ 'last-init-start': ensure_list_str(status_attrs_dict['nsds5replicalastinitstart']),
+ 'last-init-end': ensure_list_str(status_attrs_dict['nsds5replicalastinitend']),
+ 'last-init-status': ensure_list_str(status_attrs_dict['nsds5replicalastinitstatus']),
+ 'reap-active': ensure_list_str(status_attrs_dict['nsds5replicareapactive']),
+ 'replication-status': [status],
+ 'replication-lag-time': [lag_time]
}
return (json.dumps(result))
else:
diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
index dc482f4..b62446e 100644
--- a/src/lib389/lib389/cli_conf/backend.py
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -1111,7 +1111,7 @@ def create_parser(subparsers):
#######################################################
delete_parser = subcommands.add_parser('delete', help='Delete a backend database')
delete_parser.set_defaults(func=backend_delete)
- delete_parser.add_argument('be_name', help='The backend name or suffix to delete')
+ delete_parser.add_argument('be-name', help='The backend name or suffix to delete')
#######################################################
# Get Suffix Tree (for use in web console)
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index ede2529..c10e3ab 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -376,15 +376,15 @@ def get_repl_monitor_info(inst, basedn, log, args):
if connections:
for connection_str in connections:
- if len(connection_str.split(":")) != 4:
- raise ValueError(f"Connection string {connection_str} is in wrong format."
- "It should be host:port:binddn:bindpw")
- host_regex = connection_str.split(":")[0]
- port_regex = connection_str.split(":")[1]
+ connection = connection_str.split(":")
+ if (len(connection) != 4 or not all([len(str) > 0 for str in connection])):
+ raise ValueError(f"Please, fill in all Credential details. It should be host:port:binddn:bindpw")
+ host_regex = connection[0]
+ port_regex = connection[1]
if re.match(host_regex, host) and re.match(port_regex, port):
found = True
- binddn = connection_str.split(":")[2]
- bindpw = connection_str.split(":")[3]
+ binddn = connection[2]
+ bindpw = connection[3]
# Search for the password file or ask the user to write it
if bindpw.startswith("[") and bindpw.endswith("]"):
pwd_file_path = os.path.expanduser(bindpw[1:][:-1])
@@ -404,46 +404,63 @@ def get_repl_monitor_info(inst, basedn, log, args):
"bindpw": bindpw}
repl_monitor = ReplicationMonitor(inst)
- report_dict = repl_monitor.generate_report(get_credentials)
+ report_dict = repl_monitor.generate_report(get_credentials, args.json)
+ report_items = []
+
+ for instance, report_data in report_dict.items():
+ report_item = {}
+ found_alias = False
+ if args.aliases:
+ aliases = {al.split("=")[0]: al.split("=")[1] for al in args.aliases}
+ elif connection_data["aliases"]:
+ aliases = connection_data["aliases"]
+ else:
+ aliases = {}
+ if aliases:
+ for alias_name, alias_host_port in aliases.items():
+ if alias_host_port.lower() == instance.lower():
+ supplier_header = f"{alias_name} ({instance})"
+ found_alias = True
+ break
+ if not found_alias:
+ supplier_header = f"{instance}"
- if args.json:
- log.info(json.dumps({"type": "list", "items": report_dict}))
- else:
- for instance, report_data in report_dict.items():
- found_alias = False
- if args.aliases:
- aliases = {al.split("=")[0]: al.split("=")[1] for al in args.aliases}
- elif connection_data["aliases"]:
- aliases = connection_data["aliases"]
- else:
- aliases = {}
- if aliases:
- for alias_name, alias_host_port in aliases.items():
- if alias_host_port.lower() == instance.lower():
- supplier_header = f"Supplier: {alias_name} ({instance})"
- found_alias = True
- break
- if not found_alias:
- supplier_header = f"Supplier: {instance}"
+ if args.json:
+ report_item["name"] = supplier_header
+ else:
+ supplier_header = f"Supplier: {supplier_header}"
log.info(supplier_header)
- # Draw a line with the same length as the header
+
+ # Draw a line with the same length as the header
+ status = ""
+ if not args.json:
log.info("-".join(["" for _ in range(0, len(supplier_header)+1)]))
- if "status" in report_data and report_data["status"] == "Unavailable":
- status = report_data["status"]
- reason = report_data["reason"]
- log.info(f"Status: {status}")
- log.info(f"Reason: {reason}\n")
- else:
- for replica in report_data:
- replica_root = replica["replica_root"]
- replica_id = replica["replica_id"]
- maxcsn = replica["maxcsn"]
+ if report_data[0]["replica_status"].startswith("Unavailable"):
+ status = report_data[0]["replica_status"]
+ if not args.json:
+ log.info(f"Replica Status: {status}\n")
+ else:
+ for replica in report_data:
+ replica_root = replica["replica_root"]
+ replica_id = replica["replica_id"]
+ replica_status = replica["replica_status"]
+ maxcsn = replica["maxcsn"]
+ if not args.json:
log.info(f"Replica Root: {replica_root}")
log.info(f"Replica ID: {replica_id}")
+ log.info(f"Replica Status: {replica_status}")
log.info(f"Max CSN: {maxcsn}\n")
- for agreement_status in replica["agmts_status"]:
+ for agreement_status in replica["agmts_status"]:
+ if not args.json:
log.info(agreement_status)
+ if args.json:
+ report_item["data"] = report_data
+ report_items.append(report_item)
+
+ if args.json:
+ log.info(json.dumps({"type": "list", "items": report_items}))
+
def create_cl(inst, basedn, log, args):
cl = Changelog5(inst)
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index c026ff7..0fc1275 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -2492,12 +2492,16 @@ class ReplicationMonitor(object):
protocol = agmt.get_attr_val_utf8_l('nsds5replicatransportinfo')
# Supply protocol here because we need it only for connection
# and agreement status is already preformatted for the user output
- consumer = f"{host}:{port}:{protocol}"
+ consumer = f"{host}:{port}"
if consumer not in report_data:
- report_data[consumer] = None
- agmts_status.append(agmt.status(use_json))
+ report_data[f"{consumer}:{protocol}"] = None
+ if use_json:
+ agmts_status.append(json.loads(agmt.status(use_json=True)))
+ else:
+ agmts_status.append(agmt.status())
replicas_status.append({"replica_id": replica_id,
"replica_root": replica_root,
+ "replica_status": "Available",
"maxcsn": replica_maxcsn,
"agmts_status": agmts_status})
return replicas_status
@@ -2514,9 +2518,13 @@ class ReplicationMonitor(object):
"""
report_data = {}
- initial_inst_key = f"{self._instance.host.lower()}:{str(self._instance.port).lower()}"
+ initial_inst_key = f"{self._instance.config.get_attr_val_utf8_l('nsslapd-localhost')}:{self._instance.config.get_attr_val_utf8_l('nsslapd-port')}"
# Do this on an initial instance to get the agreements to other instances
- report_data[initial_inst_key] = self._get_replica_status(self._instance, report_data, use_json)
+ try:
+ report_data[initial_inst_key] = self._get_replica_status(self._instance, report_data, use_json)
+ except ldap.LDAPError as e:
+ self._log.debug(f"Connection to consumer ({supplier_hostname}:{supplier_port}) failed, error: {e}")
+ report_data[initial_inst_key] = [{"replica_status": f"Unavailable - {e.args[0]['desc']}"}]
# Check if at least some replica report on other instances was generated
repl_exists = False
@@ -2528,18 +2536,19 @@ class ReplicationMonitor(object):
except IndexError:
break
+ del report_data[supplier]
s_splitted = supplier.split(":")
supplier_hostname = s_splitted[0]
supplier_port = s_splitted[1]
supplier_protocol = s_splitted[2]
+ supplier_hostport_only = ":".join(s_splitted[:2])
# The function should be defined outside and
# it should have all the logic for figuring out the credentials.
# It is done for flexibility purpuses between CLI, WebUI and lib389 API applications
credentials = get_credentials(supplier_hostname, supplier_port)
if not credentials["binddn"]:
- report_data[supplier] = {"status": "Unavailable",
- "reason": "Bind DN was not specified"}
+ report_data[supplier_hostport_only] = [{"replica_status": "Unavailable - Bind DN was not specified"}]
continue
# Open a connection to the consumer
@@ -2557,21 +2566,30 @@ class ReplicationMonitor(object):
supplier_inst.open()
except ldap.LDAPError as e:
self._log.debug(f"Connection to consumer ({supplier_hostname}:{supplier_port}) failed, error: {e}")
- report_data[supplier] = {"status": "Unavailable",
- "reason": e.args[0]['desc']}
+ report_data[supplier_hostport_only] = [{"replica_status": f"Unavailable - {e.args[0]['desc']}"}]
continue
- report_data[supplier] = self._get_replica_status(supplier_inst, report_data, use_json)
+ report_data[supplier_hostport_only] = self._get_replica_status(supplier_inst, report_data, use_json)
repl_exists = True
+ # Get rid of the repeated items
+ report_data_parsed = {}
+ for key, value in report_data.items():
+ current_inst_rids = [val["replica_id"] for val in report_data[key] if "replica_id" in val.keys()]
+ report_data_parsed[key] = sorted(current_inst_rids)
+ report_data_filtered = {}
+ for key, value in report_data_parsed.items():
+ if value not in report_data_filtered.values():
+ report_data_filtered[key] = value
+
# Now remove the protocol from the name
report_data_final = {}
for key, value in report_data.items():
# We take the initial instance only if it is the only existing part of the report
- if key != initial_inst_key or not repl_exists:
+ if key in report_data_filtered.keys() or not repl_exists:
if not value:
- value = {"status": "Unavailable",
- "reason": "No replicas were found"}
- report_data_final[":".join(key.split(":")[:2])] = value
+ value = [{"replica_status": "Unavailable - No replicas were found"}]
+
+ report_data_final[key] = value
return report_data_final
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months
[389-ds-base] branch 389-ds-base-1.4.1 updated: Issue 50806 - Fix minor issues in lib389 health checks
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
mreynolds pushed a commit to branch 389-ds-base-1.4.1
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.1 by this push:
new 903a41f Issue 50806 - Fix minor issues in lib389 health checks
903a41f is described below
commit 903a41f224ca87b6ef9429ee8946c33c7621936e
Author: Mark Reynolds <mreynolds(a)redhat.com>
AuthorDate: Fri Jan 10 10:29:02 2020 -0500
Issue 50806 - Fix minor issues in lib389 health checks
Description: For permissions checks, add a list of permissions
that is acceptable instead of single value.
For RI plugin attribute indexing checks, we now check
if a container scope is specified. If it is set, we
skip all the other backends that are not in the scope.
This prevents false positives.
relates: https://pagure.io/389-ds-base/issue/50806
Reviewed by: mhonek(Thanks!)
---
src/lib389/lib389/dseldif.py | 40 ++++++++++++++++++++++++++++------------
src/lib389/lib389/plugins.py | 13 +++++++++++--
2 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
index b994103..81b9642 100644
--- a/src/lib389/lib389/dseldif.py
+++ b/src/lib389/lib389/dseldif.py
@@ -330,13 +330,27 @@ class FSChecks(object):
self.dirsrv = dirsrv
self._certdb = self.dirsrv.get_cert_dir()
self.ds_files = [
- ('/etc/resolv.conf', '644', DSPERMLE0001),
- (self._certdb + "/pin.txt", '600', DSPERMLE0002),
- (self._certdb + "/pwdfile.txt", '600', DSPERMLE0002),
+ {
+ 'name': '/etc/resolv.conf',
+ 'perms': [644],
+ 'report': DSPERMLE0001
+ },
+ {
+ 'name': self._certdb + "/pin.txt",
+ 'perms': [400, 600],
+ 'report': DSPERMLE0002
+ },
+ {
+ 'name': self._certdb + "/pwdfile.txt",
+ 'perms': [400, 600],
+ 'report': DSPERMLE0002
+ },
]
self._lint_functions = [self._lint_file_perms]
def lint(self):
+ """Run a lint/healthcheck for this class
+ """
results = []
for fn in self._lint_functions:
for result in fn():
@@ -345,15 +359,17 @@ class FSChecks(object):
return results
def _lint_file_perms(self):
- # Check file permissions are correct
+ """Test file permissions are safe
+ """
for ds_file in self.ds_files:
- perms = str(oct(os.stat(ds_file[0])[ST_MODE])[-3:])
- if perms != ds_file[1]:
- report = copy.deepcopy(ds_file[2])
- report['items'].append(ds_file[0])
- report['detail'] = report['detail'].replace('FILE', ds_file[0])
- report['detail'] = report['detail'].replace('PERMS', ds_file[1])
- report['fix'] = report['fix'].replace('FILE', ds_file[0])
- report['fix'] = report['fix'].replace('PERMS', ds_file[1])
+ perms = int(oct(os.stat(ds_file['name'])[ST_MODE])[-3:])
+ if perms not in ds_file['perms']:
+ perms = str(ds_file['perms'][0])
+ report = copy.deepcopy(ds_file['report'])
+ report['items'].append(ds_file['name'])
+ report['detail'] = report['detail'].replace('FILE', ds_file['name'])
+ report['detail'] = report['detail'].replace('PERMS', perms)
+ report['fix'] = report['fix'].replace('FILE', ds_file['name'])
+ report['fix'] = report['fix'].replace('PERMS', perms)
yield report
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
index 97c5d1d..0775e46 100644
--- a/src/lib389/lib389/plugins.py
+++ b/src/lib389/lib389/plugins.py
@@ -455,10 +455,19 @@ class ReferentialIntegrityPlugin(Plugin):
if self.status():
from lib389.backend import Backends
backends = Backends(self._instance).list()
+ attrs = self.get_attr_vals_utf8_l("referint-membership-attr")
+ container = self.get_attr_val_utf8_l("nsslapd-plugincontainerscope")
for backend in backends:
- indexes = backend.get_indexes()
suffix = backend.get_attr_val_utf8_l('nsslapd-suffix')
- attrs = self.get_attr_vals_utf8_l("referint-membership-attr")
+ if suffix == "cn=changelog":
+ # Always skip retro changelog
+ continue
+ if container is not None:
+ # Check if this backend is in the scope
+ if not container.endswith(suffix):
+ # skip this backend that is not in the scope
+ continue
+ indexes = backend.get_indexes()
for attr in attrs:
report = copy.deepcopy(DSRILE0002)
try:
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months
[389-ds-base] branch 389-ds-base-1.4.2 updated: Issue 50806 - Fix minor issues in lib389 health checks
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
mreynolds pushed a commit to branch 389-ds-base-1.4.2
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.2 by this push:
new 02fdfa7 Issue 50806 - Fix minor issues in lib389 health checks
02fdfa7 is described below
commit 02fdfa71315670ca90aa4c2ccee3de3406ffa7b5
Author: Mark Reynolds <mreynolds(a)redhat.com>
AuthorDate: Fri Jan 10 10:29:02 2020 -0500
Issue 50806 - Fix minor issues in lib389 health checks
Description: For permissions checks, add a list of permissions
that is acceptable instead of single value.
For RI plugin attribute indexing checks, we now check
if a container scope is specified. If it is set, we
skip all the other backends that are not in the scope.
This prevents false positives.
relates: https://pagure.io/389-ds-base/issue/50806
Reviewed by: mhonek(Thanks!)
---
src/lib389/lib389/dseldif.py | 40 ++++++++++++++++++++++++++++------------
src/lib389/lib389/plugins.py | 13 +++++++++++--
2 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
index 75fc76a..d1dc242 100644
--- a/src/lib389/lib389/dseldif.py
+++ b/src/lib389/lib389/dseldif.py
@@ -329,13 +329,27 @@ class FSChecks(object):
self.dirsrv = dirsrv
self._certdb = self.dirsrv.get_cert_dir()
self.ds_files = [
- ('/etc/resolv.conf', '644', DSPERMLE0001),
- (self._certdb + "/pin.txt", '600', DSPERMLE0002),
- (self._certdb + "/pwdfile.txt", '600', DSPERMLE0002),
+ {
+ 'name': '/etc/resolv.conf',
+ 'perms': [644],
+ 'report': DSPERMLE0001
+ },
+ {
+ 'name': self._certdb + "/pin.txt",
+ 'perms': [400, 600],
+ 'report': DSPERMLE0002
+ },
+ {
+ 'name': self._certdb + "/pwdfile.txt",
+ 'perms': [400, 600],
+ 'report': DSPERMLE0002
+ },
]
self._lint_functions = [self._lint_file_perms]
def lint(self):
+ """Run a lint/healthcheck for this class
+ """
results = []
for fn in self._lint_functions:
for result in fn():
@@ -344,14 +358,16 @@ class FSChecks(object):
return results
def _lint_file_perms(self):
- # Check file permissions are correct
+ """Test file permissions are safe
+ """
for ds_file in self.ds_files:
- perms = str(oct(os.stat(ds_file[0])[ST_MODE])[-3:])
- if perms != ds_file[1]:
- report = copy.deepcopy(ds_file[2])
- report['items'].append(ds_file[0])
- report['detail'] = report['detail'].replace('FILE', ds_file[0])
- report['detail'] = report['detail'].replace('PERMS', ds_file[1])
- report['fix'] = report['fix'].replace('FILE', ds_file[0])
- report['fix'] = report['fix'].replace('PERMS', ds_file[1])
+ perms = int(oct(os.stat(ds_file['name'])[ST_MODE])[-3:])
+ if perms not in ds_file['perms']:
+ perms = str(ds_file['perms'][0])
+ report = copy.deepcopy(ds_file['report'])
+ report['items'].append(ds_file['name'])
+ report['detail'] = report['detail'].replace('FILE', ds_file['name'])
+ report['detail'] = report['detail'].replace('PERMS', perms)
+ report['fix'] = report['fix'].replace('FILE', ds_file['name'])
+ report['fix'] = report['fix'].replace('PERMS', perms)
yield report
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
index 97c5d1d..0775e46 100644
--- a/src/lib389/lib389/plugins.py
+++ b/src/lib389/lib389/plugins.py
@@ -455,10 +455,19 @@ class ReferentialIntegrityPlugin(Plugin):
if self.status():
from lib389.backend import Backends
backends = Backends(self._instance).list()
+ attrs = self.get_attr_vals_utf8_l("referint-membership-attr")
+ container = self.get_attr_val_utf8_l("nsslapd-plugincontainerscope")
for backend in backends:
- indexes = backend.get_indexes()
suffix = backend.get_attr_val_utf8_l('nsslapd-suffix')
- attrs = self.get_attr_vals_utf8_l("referint-membership-attr")
+ if suffix == "cn=changelog":
+ # Always skip retro changelog
+ continue
+ if container is not None:
+ # Check if this backend is in the scope
+ if not container.endswith(suffix):
+ # skip this backend that is not in the scope
+ continue
+ indexes = backend.get_indexes()
for attr in attrs:
report = copy.deepcopy(DSRILE0002)
try:
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months
[389-ds-base] branch 389-ds-base-1.4.2 updated: Issue 50754 - Add Restore Change Log option to CLI
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
spichugi pushed a commit to branch 389-ds-base-1.4.2
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.2 by this push:
new 824abb8 Issue 50754 - Add Restore Change Log option to CLI
824abb8 is described below
commit 824abb8432df15946968cfc4b352362712ac07ba
Author: Simon Pichugin <spichugi(a)redhat.com>
AuthorDate: Sun Dec 22 23:46:26 2019 +0100
Issue 50754 - Add Restore Change Log option to CLI
Description: dsconf can export the changelog
but there is no feature to import a changelog dump.
Add the feature.
Fix replication CLI parsers.
Add 'copy_with_permissions' function to lib389.utils.
usage: dsconf instance replication restore-changelog [-h] {from-ldif,from-changelogdir}
positional arguments:
{from-ldif,from-changelogdir}
Replication Configuration
from-ldif Restore a single LDIF file.
from-changelogdir Restore LDIF files from changelogdir.
https://pagure.io/389-ds-base/issue/50754
Reviewed by: mreynolds (Thanks!)
---
src/lib389/lib389/cli_conf/replication.py | 79 +++++++++++++++++++++++++------
src/lib389/lib389/replica.py | 64 +++++++++++++++++++++++--
src/lib389/lib389/utils.py | 10 ++++
3 files changed, 136 insertions(+), 17 deletions(-)
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index 47f530a..ede2529 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -11,10 +11,12 @@ import logging
import os
import json
import ldap
+import stat
+from shutil import copyfile
from getpass import getpass
from lib389._constants import ReplicaRole, DSRC_HOME
from lib389.cli_base.dsrc import dsrc_to_repl_monitor
-from lib389.utils import is_a_dn
+from lib389.utils import is_a_dn, copy_with_permissions
from lib389.replica import Replicas, ReplicationMonitor, BootstrapReplicationManager, Changelog5, ChangelogLDIF
from lib389.tasks import CleanAllRUVTask, AbortCleanAllRUVTask
from lib389._mapped_object import DSLdapObjects
@@ -1014,6 +1016,39 @@ def dump_cl(inst, basedn, log, args):
cl_ldif.decode()
+def restore_cl_ldif(inst, basedn, log, args):
+ user_ldif = os.path.abspath(args.LDIF_PATH[0])
+ try:
+ assert os.path.exists(user_ldif)
+ except AssertionError:
+ raise FileNotFoundError(f"File {args.LDIF_PATH[0]} was not found")
+
+ replicas = Replicas(inst)
+ replica = replicas.get(args.replica_root[0])
+ replica_name = replica.get_attr_val_utf8_l("nsDS5ReplicaName")
+ target_ldif = f'{replica_name}.ldif'
+ target_ldif_exists = os.path.exists(target_ldif)
+ cl = Changelog5(inst)
+ cl_dir = cl.get_attr_val_utf8_l("nsslapd-changelogdir")
+ cl_dir_file = [i.lower() for i in os.listdir(cl_dir) if i.lower().startswith(replica_name)][0]
+ cl_dir_stat = os.stat(os.path.join(cl_dir, cl_dir_file))
+ # Make sure we don't remove existing files
+ if target_ldif_exists:
+ copy_with_permissions(target_ldif, f'{target_ldif}.backup')
+ copyfile(user_ldif, target_ldif)
+ os.chown(target_ldif, cl_dir_stat[stat.ST_UID], cl_dir_stat[stat.ST_GID])
+ os.chmod(target_ldif, cl_dir_stat[stat.ST_MODE])
+ replicas.restore_changelog(replica_roots=args.replica_root, log=log)
+ os.remove(target_ldif)
+ if target_ldif_exists:
+ os.rename(f'{target_ldif}.backup', target_ldif)
+
+
+def restore_cl_dir(inst, basedn, log, args):
+ replicas = Replicas(inst)
+ replicas.restore_changelog(replica_roots=args.REPLICA_ROOTS, log=log)
+
+
def create_parser(subparsers):
############################################
@@ -1104,19 +1139,35 @@ def create_parser(subparsers):
repl_get_cl = repl_subcommands.add_parser('get-changelog', help='Display replication changelog attributes.')
repl_get_cl.set_defaults(func=get_cl)
- repl_set_cl = repl_subcommands.add_parser('dump-changelog', help='Decode Directory Server replication change log and dump it to an LDIF')
- repl_set_cl.set_defaults(func=dump_cl)
- repl_set_cl.add_argument('-c', '--csn-only', action='store_true',
- help="Dump and interpret CSN only. This option can be used with or without -i option.")
- repl_set_cl.add_argument('-l', '--preserve-ldif-done', action='store_true',
- help="Preserve generated ldif.done files from changelogdir.")
- repl_set_cl.add_argument('-i', '--changelog-ldif',
- help="If you already have a ldif-like changelog, but the changes in that file are encoded,"
- " you may use this option to decode that ldif-like changelog. It should be base64 encoded.")
- repl_set_cl.add_argument('-o', '--output-file', help="Path name for the final result. Default to STDOUT if omitted.")
- repl_set_cl.add_argument('-r', '--replica-roots', nargs="+",
- help="Specify replica roots whose changelog you want to dump. The replica "
- "roots may be seperated by comma. All the replica roots would be dumped if the option is omitted.")
+ repl_dump_cl = repl_subcommands.add_parser('dump-changelog', help='Decode Directory Server replication change log and dump it to an LDIF')
+ repl_dump_cl.set_defaults(func=dump_cl)
+ repl_dump_cl.add_argument('-c', '--csn-only', action='store_true',
+ help="Dump and interpret CSN only. This option can be used with or without -i option.")
+ repl_dump_cl.add_argument('-l', '--preserve-ldif-done', action='store_true',
+ help="Preserve generated ldif.done files from changelogdir.")
+ repl_dump_cl.add_argument('-i', '--changelog-ldif',
+ help="If you already have a ldif-like changelog, but the changes in that file are encoded,"
+ " you may use this option to decode that ldif-like changelog. It should be base64 encoded.")
+ repl_dump_cl.add_argument('-o', '--output-file', help="Path name for the final result. Default to STDOUT if omitted.")
+ repl_dump_cl.add_argument('-r', '--replica-roots', nargs="+",
+ help="Specify replica roots whose changelog you want to dump. The replica "
+ "roots may be seperated by comma. All the replica roots would be dumped if the option is omitted.")
+
+ repl_restore_cl = repl_subcommands.add_parser('restore-changelog',
+ help='Restore Directory Server replication change log from LDIF file or change log directory')
+ restore_subcommands = repl_restore_cl.add_subparsers(help='Restore Replication Changelog')
+ restore_ldif = restore_subcommands.add_parser('from-ldif', help='Restore a single LDIF file.')
+ restore_ldif.set_defaults(func=restore_cl_ldif)
+ restore_ldif.add_argument('LDIF_PATH', nargs=1, help='The path of changelog LDIF file.')
+ restore_ldif.add_argument('-r', '--replica-root', nargs=1, required=True,
+ help="Specify one replica root whose changelog you want to restore. "
+ "The replica root will be consumed from the LDIF file name if the option is omitted.")
+
+ restore_changelogdir = restore_subcommands.add_parser('from-changelogdir', help='Restore LDIF files from changelogdir.')
+ restore_changelogdir.set_defaults(func=restore_cl_dir)
+ restore_changelogdir.add_argument('REPLICA_ROOTS', nargs="+",
+ help="Specify replica roots whose changelog you want to restore. The replica "
+ "roots may be seperated by comma. All the replica roots would be dumped if the option is omitted.")
repl_set_parser = repl_subcommands.add_parser('set', help='Set an attribute in the replication configuration')
repl_set_parser.set_defaults(func=set_repl_config)
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index 79ebd98..c026ff7 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -16,11 +16,13 @@ import logging
import uuid
import json
import copy
+from shutil import copyfile
from operator import itemgetter
from itertools import permutations
from lib389._constants import *
from lib389.properties import *
-from lib389.utils import normalizeDN, escapeDNValue, ensure_bytes, ensure_str, ensure_list_str, ds_is_older
+from lib389.utils import (normalizeDN, escapeDNValue, ensure_bytes, ensure_str,
+ ensure_list_str, ds_is_older, copy_with_permissions)
from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError
from lib389._mapped_object import DSLdapObjects, DSLdapObject
from lib389.passwd import password_generate
@@ -1594,6 +1596,11 @@ class Replica(DSLdapObject):
"""
self.replace('nsds5task', 'cl2ldif')
+ def begin_task_ldif2cl(self):
+ """Begin ldif to changelog task
+ """
+ self.replace('nsds5task', 'ldif2cl')
+
def get_suffix(self):
"""Return the suffix
"""
@@ -1656,12 +1663,16 @@ class Replicas(DSLdapObjects):
return replica
def process_and_dump_changelog(self, replica_roots=[], csn_only=False, preserve_ldif_done=False, log=None):
- """Dump and decode Directory Server replication change log
+ """Dump and decode Directory Server replication changelog
:param replica_roots: Replica suffixes that need to be processed
:type replica_roots: list of str
:param csn_only: Grep only the CSNs from the file
:type csn_only: bool
+ :param preserve_ldif_done: Preserve the result LDIF and rename it to [old_name].done
+ :type preserve_ldif_done: bool
+ :param log: The logger object
+ :type log: logger
"""
if log is None:
@@ -1688,7 +1699,7 @@ class Replicas(DSLdapObjects):
current_time = time.time()
replica = self.get(repl_root)
log.info(f"# Replica Root: {repl_root}")
- replica.replace("nsDS5Task", 'CL2LDIF')
+ replica.begin_task_cl2ldif()
# Decode the dumped changelog
for file in [i for i in os.listdir(cl_dir) if i.endswith('.ldif')]:
@@ -1712,6 +1723,53 @@ class Replicas(DSLdapObjects):
if not got_ldif:
log.info("LDIF file: Not found")
+ def restore_changelog(self, replica_roots, log=None):
+ """Restore Directory Server replication changelog from '.ldif' or '.ldif.done' file
+
+ :param replica_roots: Replica suffixes that need to be processed (and optional LDIF file path)
+ :type replica_roots: list of str
+ :param log: The logger object
+ :type log: logger
+ """
+
+ if log is None:
+ log = self._log
+
+ try:
+ cl = Changelog5(self._instance)
+ cl_dir = cl.get_attr_val_utf8_l("nsslapd-changelogdir")
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError("Changelog entry was not found. Probably, the replication is not enabled on this instance")
+
+ # Get all the replicas on the server if --replica-roots option is not specified
+ if not replica_roots:
+ raise ValueError("List of replication roots should be supplied")
+
+ # Dump the changelog for the replica
+ for repl_root in replica_roots:
+ replica = self.get(repl_root)
+ replica_name = replica.get_attr_val_utf8_l("nsDS5ReplicaName")
+ cl_dir_content = os.listdir(cl_dir)
+ changelog_ldif = [i.lower() for i in cl_dir_content if i.lower() == f"{replica_name}.ldif"]
+ changelog_ldif_done = [i.lower() for i in cl_dir_content if i.lower() == f"{replica_name}.ldif.done"]
+
+ if changelog_ldif:
+ replica.begin_task_cl2ldif()
+ elif changelog_ldif_done:
+ ldif_done_file = os.path.join(cl_dir, changelog_ldif_done[0])
+ ldif_file = os.path.join(cl_dir, f"{replica_name}.ldif")
+ ldif_file_exists = os.path.exists(ldif_file)
+ if ldif_file_exists:
+ copy_with_permissions(ldif_file, f'{ldif_file}.backup')
+ copy_with_permissions(ldif_done_file, ldif_file)
+ replica.begin_task_cl2ldif()
+ os.remove(ldif_file)
+ if ldif_file_exists:
+ os.rename(f'{ldif_file}.backup', ldif_file)
+ else:
+ log.error(f"Changelog LDIF for '{repl_root}' was not found")
+ continue
+
class BootstrapReplicationManager(DSLdapObject):
"""A Replication Manager credential for bootstrapping the repl process.
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index 2951136..a25f1c1 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -31,6 +31,7 @@ import shutil
import ldap
import socket
import time
+import stat
from datetime import datetime
import sys
import filecmp
@@ -1364,3 +1365,12 @@ def print_nice_time(seconds):
return f'{m:d} minute{m_plural}, {s:d} second{s_plural}'
else:
return f'{s:d} second{s_plural}'
+
+
+def copy_with_permissions(source, target):
+ """Copy a file while preserving all permissions"""
+
+ shutil.copy2(source, target)
+ st = os.stat(source)
+ os.chown(target, st[stat.ST_UID], st[stat.ST_GID])
+ os.chmod(target, st[stat.ST_MODE])
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months
[389-ds-base] branch 389-ds-base-1.4.1 updated: Issue 50754 - Add Restore Change Log option to CLI
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
spichugi pushed a commit to branch 389-ds-base-1.4.1
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.1 by this push:
new 99772c9 Issue 50754 - Add Restore Change Log option to CLI
99772c9 is described below
commit 99772c9d68a4f61de1b9b7603014c0707cdeb667
Author: Simon Pichugin <spichugi(a)redhat.com>
AuthorDate: Sun Dec 22 23:46:26 2019 +0100
Issue 50754 - Add Restore Change Log option to CLI
Description: dsconf can export the changelog
but there is no feature to import a changelog dump.
Add the feature.
Fix replication CLI parsers.
Add 'copy_with_permissions' function to lib389.utils.
usage: dsconf instance replication restore-changelog [-h] {from-ldif,from-changelogdir}
positional arguments:
{from-ldif,from-changelogdir}
Replication Configuration
from-ldif Restore a single LDIF file.
from-changelogdir Restore LDIF files from changelogdir.
https://pagure.io/389-ds-base/issue/50754
Reviewed by: mreynolds (Thanks!)
---
src/lib389/lib389/cli_conf/replication.py | 79 +++++++++++++++++++++++++------
src/lib389/lib389/replica.py | 64 +++++++++++++++++++++++--
src/lib389/lib389/utils.py | 9 ++++
3 files changed, 135 insertions(+), 17 deletions(-)
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index 47f530a..ede2529 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -11,10 +11,12 @@ import logging
import os
import json
import ldap
+import stat
+from shutil import copyfile
from getpass import getpass
from lib389._constants import ReplicaRole, DSRC_HOME
from lib389.cli_base.dsrc import dsrc_to_repl_monitor
-from lib389.utils import is_a_dn
+from lib389.utils import is_a_dn, copy_with_permissions
from lib389.replica import Replicas, ReplicationMonitor, BootstrapReplicationManager, Changelog5, ChangelogLDIF
from lib389.tasks import CleanAllRUVTask, AbortCleanAllRUVTask
from lib389._mapped_object import DSLdapObjects
@@ -1014,6 +1016,39 @@ def dump_cl(inst, basedn, log, args):
cl_ldif.decode()
+def restore_cl_ldif(inst, basedn, log, args):
+ user_ldif = os.path.abspath(args.LDIF_PATH[0])
+ try:
+ assert os.path.exists(user_ldif)
+ except AssertionError:
+ raise FileNotFoundError(f"File {args.LDIF_PATH[0]} was not found")
+
+ replicas = Replicas(inst)
+ replica = replicas.get(args.replica_root[0])
+ replica_name = replica.get_attr_val_utf8_l("nsDS5ReplicaName")
+ target_ldif = f'{replica_name}.ldif'
+ target_ldif_exists = os.path.exists(target_ldif)
+ cl = Changelog5(inst)
+ cl_dir = cl.get_attr_val_utf8_l("nsslapd-changelogdir")
+ cl_dir_file = [i.lower() for i in os.listdir(cl_dir) if i.lower().startswith(replica_name)][0]
+ cl_dir_stat = os.stat(os.path.join(cl_dir, cl_dir_file))
+ # Make sure we don't remove existing files
+ if target_ldif_exists:
+ copy_with_permissions(target_ldif, f'{target_ldif}.backup')
+ copyfile(user_ldif, target_ldif)
+ os.chown(target_ldif, cl_dir_stat[stat.ST_UID], cl_dir_stat[stat.ST_GID])
+ os.chmod(target_ldif, cl_dir_stat[stat.ST_MODE])
+ replicas.restore_changelog(replica_roots=args.replica_root, log=log)
+ os.remove(target_ldif)
+ if target_ldif_exists:
+ os.rename(f'{target_ldif}.backup', target_ldif)
+
+
+def restore_cl_dir(inst, basedn, log, args):
+ replicas = Replicas(inst)
+ replicas.restore_changelog(replica_roots=args.REPLICA_ROOTS, log=log)
+
+
def create_parser(subparsers):
############################################
@@ -1104,19 +1139,35 @@ def create_parser(subparsers):
repl_get_cl = repl_subcommands.add_parser('get-changelog', help='Display replication changelog attributes.')
repl_get_cl.set_defaults(func=get_cl)
- repl_set_cl = repl_subcommands.add_parser('dump-changelog', help='Decode Directory Server replication change log and dump it to an LDIF')
- repl_set_cl.set_defaults(func=dump_cl)
- repl_set_cl.add_argument('-c', '--csn-only', action='store_true',
- help="Dump and interpret CSN only. This option can be used with or without -i option.")
- repl_set_cl.add_argument('-l', '--preserve-ldif-done', action='store_true',
- help="Preserve generated ldif.done files from changelogdir.")
- repl_set_cl.add_argument('-i', '--changelog-ldif',
- help="If you already have a ldif-like changelog, but the changes in that file are encoded,"
- " you may use this option to decode that ldif-like changelog. It should be base64 encoded.")
- repl_set_cl.add_argument('-o', '--output-file', help="Path name for the final result. Default to STDOUT if omitted.")
- repl_set_cl.add_argument('-r', '--replica-roots', nargs="+",
- help="Specify replica roots whose changelog you want to dump. The replica "
- "roots may be seperated by comma. All the replica roots would be dumped if the option is omitted.")
+ repl_dump_cl = repl_subcommands.add_parser('dump-changelog', help='Decode Directory Server replication change log and dump it to an LDIF')
+ repl_dump_cl.set_defaults(func=dump_cl)
+ repl_dump_cl.add_argument('-c', '--csn-only', action='store_true',
+ help="Dump and interpret CSN only. This option can be used with or without -i option.")
+ repl_dump_cl.add_argument('-l', '--preserve-ldif-done', action='store_true',
+ help="Preserve generated ldif.done files from changelogdir.")
+ repl_dump_cl.add_argument('-i', '--changelog-ldif',
+ help="If you already have a ldif-like changelog, but the changes in that file are encoded,"
+ " you may use this option to decode that ldif-like changelog. It should be base64 encoded.")
+ repl_dump_cl.add_argument('-o', '--output-file', help="Path name for the final result. Default to STDOUT if omitted.")
+ repl_dump_cl.add_argument('-r', '--replica-roots', nargs="+",
+ help="Specify replica roots whose changelog you want to dump. The replica "
+ "roots may be seperated by comma. All the replica roots would be dumped if the option is omitted.")
+
+ repl_restore_cl = repl_subcommands.add_parser('restore-changelog',
+ help='Restore Directory Server replication change log from LDIF file or change log directory')
+ restore_subcommands = repl_restore_cl.add_subparsers(help='Restore Replication Changelog')
+ restore_ldif = restore_subcommands.add_parser('from-ldif', help='Restore a single LDIF file.')
+ restore_ldif.set_defaults(func=restore_cl_ldif)
+ restore_ldif.add_argument('LDIF_PATH', nargs=1, help='The path of changelog LDIF file.')
+ restore_ldif.add_argument('-r', '--replica-root', nargs=1, required=True,
+ help="Specify one replica root whose changelog you want to restore. "
+ "The replica root will be consumed from the LDIF file name if the option is omitted.")
+
+ restore_changelogdir = restore_subcommands.add_parser('from-changelogdir', help='Restore LDIF files from changelogdir.')
+ restore_changelogdir.set_defaults(func=restore_cl_dir)
+ restore_changelogdir.add_argument('REPLICA_ROOTS', nargs="+",
+ help="Specify replica roots whose changelog you want to restore. The replica "
+ "roots may be seperated by comma. All the replica roots would be dumped if the option is omitted.")
repl_set_parser = repl_subcommands.add_parser('set', help='Set an attribute in the replication configuration')
repl_set_parser.set_defaults(func=set_repl_config)
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index 79ebd98..c026ff7 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -16,11 +16,13 @@ import logging
import uuid
import json
import copy
+from shutil import copyfile
from operator import itemgetter
from itertools import permutations
from lib389._constants import *
from lib389.properties import *
-from lib389.utils import normalizeDN, escapeDNValue, ensure_bytes, ensure_str, ensure_list_str, ds_is_older
+from lib389.utils import (normalizeDN, escapeDNValue, ensure_bytes, ensure_str,
+ ensure_list_str, ds_is_older, copy_with_permissions)
from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError
from lib389._mapped_object import DSLdapObjects, DSLdapObject
from lib389.passwd import password_generate
@@ -1594,6 +1596,11 @@ class Replica(DSLdapObject):
"""
self.replace('nsds5task', 'cl2ldif')
+ def begin_task_ldif2cl(self):
+ """Begin ldif to changelog task
+ """
+ self.replace('nsds5task', 'ldif2cl')
+
def get_suffix(self):
"""Return the suffix
"""
@@ -1656,12 +1663,16 @@ class Replicas(DSLdapObjects):
return replica
def process_and_dump_changelog(self, replica_roots=[], csn_only=False, preserve_ldif_done=False, log=None):
- """Dump and decode Directory Server replication change log
+ """Dump and decode Directory Server replication changelog
:param replica_roots: Replica suffixes that need to be processed
:type replica_roots: list of str
:param csn_only: Grep only the CSNs from the file
:type csn_only: bool
+ :param preserve_ldif_done: Preserve the result LDIF and rename it to [old_name].done
+ :type preserve_ldif_done: bool
+ :param log: The logger object
+ :type log: logger
"""
if log is None:
@@ -1688,7 +1699,7 @@ class Replicas(DSLdapObjects):
current_time = time.time()
replica = self.get(repl_root)
log.info(f"# Replica Root: {repl_root}")
- replica.replace("nsDS5Task", 'CL2LDIF')
+ replica.begin_task_cl2ldif()
# Decode the dumped changelog
for file in [i for i in os.listdir(cl_dir) if i.endswith('.ldif')]:
@@ -1712,6 +1723,53 @@ class Replicas(DSLdapObjects):
if not got_ldif:
log.info("LDIF file: Not found")
+ def restore_changelog(self, replica_roots, log=None):
+ """Restore Directory Server replication changelog from '.ldif' or '.ldif.done' file
+
+ :param replica_roots: Replica suffixes that need to be processed (and optional LDIF file path)
+ :type replica_roots: list of str
+ :param log: The logger object
+ :type log: logger
+ """
+
+ if log is None:
+ log = self._log
+
+ try:
+ cl = Changelog5(self._instance)
+ cl_dir = cl.get_attr_val_utf8_l("nsslapd-changelogdir")
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError("Changelog entry was not found. Probably, the replication is not enabled on this instance")
+
+ # Get all the replicas on the server if --replica-roots option is not specified
+ if not replica_roots:
+ raise ValueError("List of replication roots should be supplied")
+
+ # Dump the changelog for the replica
+ for repl_root in replica_roots:
+ replica = self.get(repl_root)
+ replica_name = replica.get_attr_val_utf8_l("nsDS5ReplicaName")
+ cl_dir_content = os.listdir(cl_dir)
+ changelog_ldif = [i.lower() for i in cl_dir_content if i.lower() == f"{replica_name}.ldif"]
+ changelog_ldif_done = [i.lower() for i in cl_dir_content if i.lower() == f"{replica_name}.ldif.done"]
+
+ if changelog_ldif:
+ replica.begin_task_cl2ldif()
+ elif changelog_ldif_done:
+ ldif_done_file = os.path.join(cl_dir, changelog_ldif_done[0])
+ ldif_file = os.path.join(cl_dir, f"{replica_name}.ldif")
+ ldif_file_exists = os.path.exists(ldif_file)
+ if ldif_file_exists:
+ copy_with_permissions(ldif_file, f'{ldif_file}.backup')
+ copy_with_permissions(ldif_done_file, ldif_file)
+ replica.begin_task_cl2ldif()
+ os.remove(ldif_file)
+ if ldif_file_exists:
+ os.rename(f'{ldif_file}.backup', ldif_file)
+ else:
+ log.error(f"Changelog LDIF for '{repl_root}' was not found")
+ continue
+
class BootstrapReplicationManager(DSLdapObject):
"""A Replication Manager credential for bootstrapping the repl process.
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index 27b4047..292d1a7 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -31,6 +31,7 @@ import shutil
import ldap
import socket
import time
+import stat
from datetime import datetime
import sys
import filecmp
@@ -1359,3 +1360,11 @@ def print_nice_time(seconds):
else:
return f'{s:d} second{s_plural}'
+
+def copy_with_permissions(source, target):
+ """Copy a file while preserving all permissions"""
+
+ shutil.copy2(source, target)
+ st = os.stat(source)
+ os.chown(target, st[stat.ST_UID], st[stat.ST_GID])
+ os.chmod(target, st[stat.ST_MODE])
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months
[389-ds-base] branch 389-ds-base-1.4.1 updated: Ticket 50667 - dsctl -l did not respect PREFIX
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
firstyear pushed a commit to branch 389-ds-base-1.4.1
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.1 by this push:
new 4fe64b0 Ticket 50667 - dsctl -l did not respect PREFIX
4fe64b0 is described below
commit 4fe64b0dfc883893512507712c64a1786299583e
Author: William Brown <william(a)blackhats.net.au>
AuthorDate: Wed Oct 23 12:01:04 2019 +1000
Ticket 50667 - dsctl -l did not respect PREFIX
Bug Description: dsctl list was not coded to allow
using the paths module.
Fix Description: Change to the paths module to allow
better and consistent CLI handling.
https://pagure.io/389-ds-base/issue/50667
Author: William Brown <william(a)blackhats.net.au>
Review by: mreynolds, spichugi (thanks)
---
src/lib389/cli/dsctl | 4 ++--
src/lib389/lib389/cli_ctl/instance.py | 2 +-
src/lib389/lib389/utils.py | 8 ++++++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/lib389/cli/dsctl b/src/lib389/cli/dsctl
index 1d88cb3..97424b9 100755
--- a/src/lib389/cli/dsctl
+++ b/src/lib389/cli/dsctl
@@ -46,8 +46,8 @@ parser.add_argument('-l', '--list',
default=False, action='store_true'
)
-parser.add_argument('--remove-all', nargs="?", default=False, const=None,
- help="Remove all instances of Directory Server (you can also provide an optional directory prefix for this argument)",
+parser.add_argument('--remove-all', default=False, action='store_true',
+ help=argparse.SUPPRESS
)
subparsers = parser.add_subparsers(help="action")
diff --git a/src/lib389/lib389/cli_ctl/instance.py b/src/lib389/lib389/cli_ctl/instance.py
index 95958e1..f0111f3 100644
--- a/src/lib389/lib389/cli_ctl/instance.py
+++ b/src/lib389/lib389/cli_ctl/instance.py
@@ -127,7 +127,7 @@ def instance_remove_all(log, args):
"""Remove all instances - clean sweep!
"""
- inst_names = get_instance_list(args.remove_all)
+ inst_names = get_instance_list()
if len(inst_names) > 0:
answer = input("Are you sure you want to remove all the Directory Server instances? Enter \"Yes\" to continue: ")
if answer != 'Yes':
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index 19f0b93..27b4047 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -1243,9 +1243,10 @@ def get_ldapurl_from_serverid(instance):
return ("ldap://{}:{}".format(host, port), None)
-def get_instance_list(prefix=None):
+def get_instance_list():
# List all server instances
- conf_dir = (prefix or "") + "/etc/dirsrv/"
+ paths = Paths()
+ conf_dir = os.path.join(paths.sysconf_dir, 'dirsrv')
insts = []
try:
for inst in os.listdir(conf_dir):
@@ -1253,6 +1254,9 @@ def get_instance_list(prefix=None):
insts.append(inst)
except OSError as e:
log.error("Failed to check directory: {} - {}".format(conf_dir, str(e)))
+ except IOError as e:
+ log.error(e)
+ log.error("Perhaps you need to be a different user?")
insts.sort()
return insts
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months
[389-ds-base] branch 389-ds-base-1.4.2 updated: Ticket 50667 - dsctl -l did not respect PREFIX
by pagure@pagure.io
This is an automated email from the git hooks/post-receive script.
firstyear pushed a commit to branch 389-ds-base-1.4.2
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.2 by this push:
new 7a09961 Ticket 50667 - dsctl -l did not respect PREFIX
7a09961 is described below
commit 7a09961fb6b0905b3b6329f8016dca0e2b6d5f29
Author: William Brown <william(a)blackhats.net.au>
AuthorDate: Wed Oct 23 12:01:04 2019 +1000
Ticket 50667 - dsctl -l did not respect PREFIX
Bug Description: dsctl list was not coded to allow
using the paths module.
Fix Description: Change to the paths module to allow
better and consistent CLI handling.
https://pagure.io/389-ds-base/issue/50667
Author: William Brown <william(a)blackhats.net.au>
Review by: mreynolds, spichugi (thanks)
---
src/lib389/cli/dsctl | 4 ++--
src/lib389/lib389/cli_ctl/instance.py | 2 +-
src/lib389/lib389/utils.py | 8 ++++++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/lib389/cli/dsctl b/src/lib389/cli/dsctl
index 8484f86..a4a5f9c 100755
--- a/src/lib389/cli/dsctl
+++ b/src/lib389/cli/dsctl
@@ -47,8 +47,8 @@ parser.add_argument('-l', '--list',
default=False, action='store_true'
)
-parser.add_argument('--remove-all', nargs="?", default=False, const=None,
- help="Remove all instances of Directory Server (you can also provide an optional directory prefix for this argument)",
+parser.add_argument('--remove-all', default=False, action='store_true',
+ help=argparse.SUPPRESS
)
subparsers = parser.add_subparsers(help="action")
diff --git a/src/lib389/lib389/cli_ctl/instance.py b/src/lib389/lib389/cli_ctl/instance.py
index 95958e1..f0111f3 100644
--- a/src/lib389/lib389/cli_ctl/instance.py
+++ b/src/lib389/lib389/cli_ctl/instance.py
@@ -127,7 +127,7 @@ def instance_remove_all(log, args):
"""Remove all instances - clean sweep!
"""
- inst_names = get_instance_list(args.remove_all)
+ inst_names = get_instance_list()
if len(inst_names) > 0:
answer = input("Are you sure you want to remove all the Directory Server instances? Enter \"Yes\" to continue: ")
if answer != 'Yes':
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index 36422dd..2951136 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -1249,9 +1249,10 @@ def get_ldapurl_from_serverid(instance):
return ("ldap://{}:{}".format(host, port), None)
-def get_instance_list(prefix=None):
+def get_instance_list():
# List all server instances
- conf_dir = (prefix or "") + "/etc/dirsrv/"
+ paths = Paths()
+ conf_dir = os.path.join(paths.sysconf_dir, 'dirsrv')
insts = []
try:
for inst in os.listdir(conf_dir):
@@ -1259,6 +1260,9 @@ def get_instance_list(prefix=None):
insts.append(inst)
except OSError as e:
log.error("Failed to check directory: {} - {}".format(conf_dir, str(e)))
+ except IOError as e:
+ log.error(e)
+ log.error("Perhaps you need to be a different user?")
insts.sort()
return insts
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
3 years, 2 months