This is an automated email from the git hooks/post-receive script.
mreynolds pushed a commit to branch 389-ds-base-1.4.0
in repository 389-ds-base.
The following commit(s) were added to refs/heads/389-ds-base-1.4.0 by this push:
new 4f710b3 Ticket 50327 - Add replication conflict support to UI
4f710b3 is described below
commit 4f710b35ca5e29ad32ef49f3faa3086a58ba6bf7
Author: Mark Reynolds <mreynolds(a)redhat.com>
AuthorDate: Fri Apr 19 16:50:36 2019 -0400
Ticket 50327 - Add replication conflict support to UI
Description: Added a page under the monitor tab to view and management
replication conflict and glue entries.
https://pagure.io/389-ds-base/issue/50327
Reviewed by: spichugi(Thanks!)
(cherry picked from commit 21e10bd59dc6c3094337c8340a28a588ddd7cfa4)
---
src/cockpit/389-console/src/css/ds.css | 26 +-
.../389-console/src/lib/database/chaining.jsx | 2 +-
.../389-console/src/lib/monitor/monitorModals.jsx | 178 +++++++++
.../389-console/src/lib/monitor/monitorTables.jsx | 391 +++++++++++++++++++-
.../389-console/src/lib/monitor/replMonitor.jsx | 406 ++++++++++++++++++++-
src/cockpit/389-console/src/monitor.jsx | 50 +++
src/lib389/lib389/conflicts.py | 13 +-
7 files changed, 1047 insertions(+), 19 deletions(-)
diff --git a/src/cockpit/389-console/src/css/ds.css
b/src/cockpit/389-console/src/css/ds.css
index e0ceeb8..1ad8d5c 100644
--- a/src/cockpit/389-console/src/css/ds.css
+++ b/src/cockpit/389-console/src/css/ds.css
@@ -701,8 +701,9 @@ td {
width: 450px;
}
-.ds-modal-wide {
- min-width: 850px;
+.ds-modal-wide .modal-content{
+ width: 850px !important;
+ min-width: 850px !important;
vertical-align: middle;
}
@@ -1025,8 +1026,9 @@ td {
line-height: 0;
}
-.ds-accordian-div {
+.ds-modal-row {
margin-left: 20px;
+ margin-right: 0px !important;
}
.ds-chaining-list {
@@ -1105,6 +1107,24 @@ textarea {
overflow-y: scroll;
}
+.ds-conflict {
+ margin-top: 5px;
+ padding-top: 5px;
+ vertical-align: top;
+ width: 375px;
+ height: 275px;
+ max-height: 350px !important;
+ white-space: pre;
+ line-height: 1.5;
+ font-family: monospace !important;
+ overflow-y: auto;
+ overflow-x: scroll;
+}
+
+.ds-conflict-btn {
+ width: 110px;
+}
+
option {
color: #181818;
}
diff --git a/src/cockpit/389-console/src/lib/database/chaining.jsx
b/src/cockpit/389-console/src/lib/database/chaining.jsx
index f20fca6..3dd3ec4 100644
--- a/src/cockpit/389-console/src/lib/database/chaining.jsx
+++ b/src/cockpit/389-console/src/lib/database/chaining.jsx
@@ -1041,7 +1041,7 @@ export class ChainingConfig extends React.Component {
<CustomCollapse>
<div className="ds-accordion-panel">
- <div className="ds-accordian-div">
+ <div className="ds-margin-left">
<div className="ds-container">
<div className="ds-inline">
<div>
diff --git a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
index a63c67a..5593cb0 100644
--- a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
+++ b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
@@ -491,6 +491,165 @@ class WinsyncAgmtDetailsModal extends React.Component {
}
}
+class ConflictCompareModal extends React.Component {
+ render() {
+ const {
+ showModal,
+ conflictEntry,
+ validEntry,
+ swapFunc,
+ convertFunc,
+ deleteFunc,
+ handleConvertChange,
+ closeHandler,
+ } = this.props;
+
+ let ignoreAttrs = ['createtimestamp', 'creatorsname',
'modifytimestamp',
+ 'modifiersname', 'entryid', 'entrydn',
'parentid', 'numsubordinates'];
+ let conflict = "dn: " + conflictEntry.dn + "\n";
+ let valid = "dn: " + validEntry.dn + "\n";
+ let conflictChildren = "0";
+ let validChildren = "0";
+
+ for (const key in conflictEntry.attrs) {
+ if (key == "numsubordinates") {
+ conflictChildren = conflictEntry.attrs[key];
+ }
+ if (!ignoreAttrs.includes(key)) {
+ for (let attr of conflictEntry.attrs[key]) {
+ conflict += key + ": " + attr + "\n";
+ }
+ }
+ }
+ for (const key in validEntry.attrs) {
+ if (key == "numsubordinates") {
+ validChildren = <font
color="red">{validEntry.attrs[key]}</font>;
+ }
+ if (!ignoreAttrs.includes(key)) {
+ for (let attr of validEntry.attrs[key]) {
+ valid += key + ": " + attr + "\n";
+ }
+ }
+ }
+
+ return (
+ <Modal show={showModal} className="ds-modal-wide"
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>
+ Resolve Replication Conflicts
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Form horizontal autoComplete="off">
+ <div className="ds-modal-row">
+ <Row>
+ <Col sm={5}>
+ <Row>
+ <h3>Conflict Entry</h3>
+ </Row>
+ <Row>
+ <textarea
className="ds-conflict" value={conflict} readOnly />
+ </Row>
+ <p />
+ <Row>
+ <p>Child Entries:
<b>{conflictChildren}</b></p>
+ </Row>
+ </Col>
+ <Col sm={1} />
+ <Col sm={5}>
+ <Row>
+ <h3>Valid Entry</h3>
+ </Row>
+ <Row>
+ <textarea
className="ds-conflict" value={valid} readOnly />
+ </Row>
+ <p />
+ <Row>
+ <p>Child Entries:
<b>{validChildren}</b></p>
+ </Row>
+ </Col>
+ </Row>
+ <hr />
+ <Row>
+ <h4>You can convert the <b>Conflict
Entry</b> into a new valid entry by providing a new RDN value below, like
"<i>cn=NEW_RDN</i>"</h4>
+ </Row>
+ <Row>
+ <Col sm={2}>
+ <Button
+ bsStyle="primary"
+ className="ds-conflict-btn"
+ onClick={() => {
+ convertFunc(conflictEntry.dn);
+ }}
+ >
+ Convert Conflict
+ </Button>
+ </Col>
+ <Col sm={4}>
+ <input onChange={handleConvertChange}
type="text" placeholder="Enter new RDN here" size="30"
/>
+ </Col>
+ </Row>
+ <p />
+ <Row>
+ <h4>Or, you can replace, or swap, the
<b>Valid Entry</b> (and its child entries) with the <b>Conflict
Entry</b></h4>
+ </Row>
+ <Row>
+ <Col sm={3}>
+ <Button
+ bsStyle="primary"
+ className="ds-conflict-btn"
+ onClick={() => {
+ swapFunc(conflictEntry.dn);
+ }}
+ >
+ Swap Entries
+ </Button>
+ </Col>
+ </Row>
+ <p />
+ <Row>
+ <h4>Or, you can delete the <b>Conflict
Entry</b></h4>
+ </Row>
+ <Row>
+ <Col sm={3}>
+ <Button
+ bsStyle="primary"
+ className="ds-conflict-btn"
+ onClick={() => {
+ deleteFunc(conflictEntry.dn);
+ }}
+ >
+ Delete Conflict
+ </Button>
+ </Col>
+ </Row>
+ </div>
+ </Form>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button
+ bsStyle="default"
+ className="btn-cancel"
+ onClick={closeHandler}
+ >
+ Close
+ </Button>
+ </Modal.Footer>
+ </div>
+ </Modal>
+ );
+ }
+}
+
// Prototypes and defaultProps
AgmtDetailsModal.propTypes = {
showModal: PropTypes.bool,
@@ -550,10 +709,29 @@ ReplLoginModal.defaultProps = {
error: {},
};
+ConflictCompareModal.propTypes = {
+ showModal: PropTypes.bool,
+ conflictEntry: PropTypes.object,
+ validEntry: PropTypes.object,
+ swapFunc: PropTypes.func,
+ convertFunc: PropTypes.func,
+ closeHandler: PropTypes.func,
+};
+
+ConflictCompareModal.defaultProps = {
+ showModal: false,
+ conflictEntry: {},
+ validEntry: {},
+ swapFunc: noop,
+ convertFunc: noop,
+ closeHandler: noop,
+};
+
export {
TaskLogModal,
AgmtDetailsModal,
ReplLagReportModal,
ReplLoginModal,
WinsyncAgmtDetailsModal,
+ ConflictCompareModal,
};
diff --git a/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
b/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
index 592e514..c401196 100644
--- a/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
+++ b/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx
@@ -1218,6 +1218,372 @@ class LagReportTable extends React.Component {
}
}
+class GlueTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ fieldsToSearch: ["dn", "created"],
+ rowKey: "dn",
+ columns: [
+ {
+ property: "dn",
+ header: {
+ label: "Glue Entry",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "desc",
+ header: {
+ label: "Conflict Description",
+ props: {
+ index: 1,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 1
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "created",
+ header: {
+ label: "Created",
+ props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 2
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+
+ {
+ property: "actions",
+ header: {
+ props: {
+ index: 3,
+ rowSpan: 1,
+ colSpan: 1
+ },
+ formatters: [actionHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 3
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.dn[0]}>
+ <DropdownButton id={rowData.dn[0]}
+ bsStyle="default"
title="Actions">
+ <MenuItem eventKey="1"
onClick={() => {
+ this.props.convertGlue(rowData.dn[0]);
+ }}>
+ Convert Glue Entry
+ </MenuItem>
+ <MenuItem eventKey="2"
onClick={() => {
+ this.props.deleteGlue(rowData.dn[0]);
+ }}>
+ Delete Glue Entry
+ </MenuItem>
+ </DropdownButton>
+ </td>
+ ];
+ }
+ ]
+ }
+ }
+ ]
+ };
+ this.getColumns = this.getColumns.bind(this);
+ this.getSingleColumn = this.getSingleColumn.bind(this);
+ } // Constructor
+
+ getColumns() {
+ return this.state.columns;
+ }
+
+ getSingleColumn () {
+ return [
+ {
+ property: "msg",
+ header: {
+ label: "Replication Glue Entries",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ ];
+ }
+
+ render() {
+ let glueTable;
+ if (this.props.glues.length < 1) {
+ glueTable =
+ <DSTable
+ noSearchBar
+ getColumns={this.getSingleColumn}
+ rowKey={"msg"}
+ rows={[{msg: "No glue entries"}]}
+ />;
+ } else {
+ let rows = [];
+ for (let glue of this.props.glues) {
+ rows.push({
+ dn: [glue.dn],
+ desc: glue.attrs.nsds5replconflict,
+ created: [get_date_string(glue.attrs.createtimestamp[0])],
+ });
+ }
+
+ glueTable =
+ <DSTable
+ getColumns={this.getColumns}
+ fieldsToSearch={this.state.fieldsToSearch}
+ toolBarSearchField={this.state.searchField}
+ rowKey={this.state.rowKey}
+ rows={rows}
+ disableLoadingSpinner
+ toolBarPagination={[6, 12, 24, 48, 96]}
+ toolBarPaginationPerPage={6}
+ />;
+ }
+
+ return (
+ <div className="ds-margin-top-xlg">
+ {glueTable}
+ </div>
+ );
+ }
+}
+
+class ConflictTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ fieldsToSearch: ["dn", "desc"],
+ rowKey: "dn",
+ columns: [
+ {
+ property: "dn",
+ header: {
+ label: "Conflict DN",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "desc",
+ header: {
+ label: "Conflict Description",
+ props: {
+ index: 1,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 1
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ {
+ property: "created",
+ header: {
+ label: "Created",
+ props: {
+ index: 2,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 2
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+
+ {
+ property: "actions",
+ header: {
+ props: {
+ index: 3,
+ rowSpan: 1,
+ colSpan: 1
+ },
+ formatters: [actionHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 3
+ },
+ formatters: [
+ (value, { rowData }) => {
+ return [
+ <td key={rowData.dn[0]}>
+ <Button
+ onClick={() => {
+
this.props.resolveConflict(rowData.dn[0]);
+ }}
+ >
+ Resolve
+ </Button>
+ </td>
+ ];
+ }
+ ]
+ }
+ }
+ ]
+ };
+ this.getColumns = this.getColumns.bind(this);
+ this.getSingleColumn = this.getSingleColumn.bind(this);
+ } // Constructor
+
+ getColumns() {
+ return this.state.columns;
+ }
+
+ getSingleColumn () {
+ return [
+ {
+ property: "msg",
+ header: {
+ label: "Replication Conflict Entries",
+ props: {
+ index: 0,
+ rowSpan: 1,
+ colSpan: 1,
+ sort: true
+ },
+ transforms: [],
+ formatters: [],
+ customFormatters: [sortableHeaderCellFormatter]
+ },
+ cell: {
+ props: {
+ index: 0
+ },
+ formatters: [tableCellFormatter]
+ }
+ },
+ ];
+ }
+
+ render() {
+ let conflictTable;
+ if (this.props.conflicts.length < 1) {
+ conflictTable =
+ <DSTable
+ noSearchBar
+ getColumns={this.getSingleColumn}
+ rowKey={"msg"}
+ rows={[{msg: "No conflict entries"}]}
+ />;
+ } else {
+ let rows = [];
+ for (let conflict of this.props.conflicts) {
+ rows.push({
+ dn: [conflict.dn],
+ desc: conflict.attrs.nsds5replconflict,
+ created: [get_date_string(conflict.attrs.createtimestamp[0])],
+ });
+ }
+
+ conflictTable =
+ <DSTable
+ getColumns={this.getColumns}
+ fieldsToSearch={this.state.fieldsToSearch}
+ toolBarSearchField={this.state.searchField}
+ rowKey={this.state.rowKey}
+ rows={rows}
+ disableLoadingSpinner
+ toolBarPagination={[6, 12, 24, 48, 96]}
+ toolBarPaginationPerPage={6}
+ />;
+ }
+
+ return (
+ <div className="ds-margin-top-xlg">
+ {conflictTable}
+ </div>
+ );
+ }
+}
+
// Proptypes and defaults
LagReportTable.propTypes = {
@@ -1244,7 +1610,6 @@ AgmtTable.defaultProps = {
pokeAgmt: noop
};
-// Proptyes and defaults
WinsyncAgmtTable.propTypes = {
agmts: PropTypes.array,
viewAgmt: PropTypes.func,
@@ -1285,6 +1650,28 @@ AbortCleanALLRUVTable.defaultProps = {
viewLog: PropTypes.func,
};
+ConflictTable.propTypes = {
+ conflicts: PropTypes.array,
+ resolveConflict: PropTypes.func,
+};
+
+ConflictTable.defaultProps = {
+ conflicts: [],
+ resolveConflict: noop,
+};
+
+GlueTable.propTypes = {
+ glues: PropTypes.array,
+ convertGlue: PropTypes.func,
+ deleteGlue: PropTypes.func,
+};
+
+GlueTable.defaultProps = {
+ glues: PropTypes.array,
+ convertGlue: noop,
+ deleteGlue: noop,
+};
+
export {
ConnectionTable,
AgmtTable,
@@ -1292,4 +1679,6 @@ export {
LagReportTable,
CleanALLRUVTable,
AbortCleanALLRUVTable,
+ ConflictTable,
+ GlueTable,
};
diff --git a/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
b/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
index a742a0d..ca2e354 100644
--- a/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
+++ b/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx
@@ -8,7 +8,9 @@ import {
AgmtTable,
WinsyncAgmtTable,
CleanALLRUVTable,
- AbortCleanALLRUVTable
+ AbortCleanALLRUVTable,
+ ConflictTable,
+ GlueTable,
} from "./monitorTables.jsx";
import {
TaskLogModal,
@@ -16,6 +18,7 @@ import {
WinsyncAgmtDetailsModal,
ReplLagReportModal,
ReplLoginModal,
+ ConflictCompareModal,
} from "./monitorModals.jsx";
import {
Nav,
@@ -41,9 +44,18 @@ export class ReplMonitor extends React.Component {
showInitConfirm: false,
showLoginModal: false,
showLagReport: false,
+ showCompareModal: false,
+ showConfirmDeleteGlue: false,
+ showConfirmConvertGlue: false,
+ showConfirmSwapConflict: false,
+ showConfirmConvertConflict: false,
+ showConfirmDeleteConflict: false,
reportLoading: false,
lagAgmts: [],
agmt: "",
+ convertRDN: "",
+ glueEntry: "",
+ conflictEntry: "",
binddn: "cn=Directory Manager",
bindpw: "",
errObj: {}
@@ -70,6 +82,253 @@ export class ReplMonitor extends React.Component {
this.closeLogModal = this.closeLogModal.bind(this);
this.handleLoginModal = this.handleLoginModal.bind(this);
this.closeLoginModal = this.closeLoginModal.bind(this);
+ // Conflict entry functions
+ this.convertConflict = this.convertConflict.bind(this);
+ this.swapConflict = this.swapConflict.bind(this);
+ this.deleteConflict = this.deleteConflict.bind(this);
+ this.resolveConflict = this.resolveConflict.bind(this);
+ this.convertGlue = this.convertGlue.bind(this);
+ this.deleteGlue = this.deleteGlue.bind(this);
+ this.closeCompareModal = this.closeCompareModal.bind(this);
+ this.confirmDeleteGlue = this.confirmDeleteGlue.bind(this);
+ this.confirmConvertGlue = this.confirmConvertGlue.bind(this);
+ this.closeConfirmDeleteGlue = this.closeConfirmDeleteGlue.bind(this);
+ this.closeConfirmConvertGlue = this.closeConfirmConvertGlue.bind(this);
+ this.handleConvertChange = this.handleConvertChange.bind(this);
+
+ this.confirmDeleteConflict = this.confirmDeleteConflict.bind(this);
+ this.confirmConvertConflict = this.confirmConvertConflict.bind(this);
+ this.confirmSwapConflict = this.confirmSwapConflict.bind(this);
+
+ this.closeConfirmDeleteConflict = this.closeConfirmDeleteConflict.bind(this);
+ this.closeConfirmConvertConflict = this.closeConfirmConvertConflict.bind(this);
+ this.closeConfirmSwapConflict = this.closeConfirmSwapConflict.bind(this);
+ }
+
+ convertConflict (dn) {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "convert", dn, "--new-rdn=" +
this.state.convertRDN];
+ log_cmd("convertConflict", "convert conflict entry", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ this.props.reloadConflicts();
+ this.props.addNotification(
+ "success",
+ `Replication conflict entry was converted into a valid entry`
+ );
+ this.setState({
+ showCompareModal: false,
+ convertRDN: ""
+ });
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to convert conflict entry entry: ${dn} - ${errMsg.desc}`
+ );
+ });
+ }
+
+ swapConflict (dn) {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "swap", dn];
+ log_cmd("swapConflict", "swap in conflict entry", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ this.props.reloadConflicts();
+ this.props.addNotification(
+ "success",
+ `Replication Conflict Entry is now the Valid Entry`
+ );
+ this.setState({
+ showCompareModal: false,
+ });
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to swap in conflict entry: ${dn} - ${errMsg.desc}`
+ );
+ });
+ }
+
+ deleteConflict (dn) {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "delete", dn];
+ log_cmd("deleteConflict", "Delete conflict entry", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ this.props.reloadConflicts();
+ this.props.addNotification(
+ "success",
+ `Replication conflict entry was deleted`
+ );
+ this.setState({
+ showCompareModal: false,
+ });
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to delete conflict entry: ${dn} - ${errMsg.desc}`
+ );
+ });
+ }
+
+ resolveConflict (dn) {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "compare", dn];
+ log_cmd("resolveConflict", "Compare conflict entry with valid
entry", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ let entries = JSON.parse(content);
+ this.setState({
+ cmpConflictEntry: entries.items[0],
+ cmpValidEntry: entries.items[1],
+ showCompareModal: true,
+ });
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to get conflict entries: ${dn} - ${errMsg.desc}`
+ );
+ });
+ }
+
+ confirmConvertGlue (dn) {
+ this.setState({
+ showConfirmConvertGlue: true,
+ glueEntry: dn
+ });
+ }
+
+ closeConfirmConvertGlue () {
+ this.setState({
+ showConfirmConvertGlue: false,
+ glueEntry: ""
+ });
+ }
+
+ convertGlue (dn) {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "convert-glue", dn];
+ log_cmd("convertGlue", "Convert glue entry to normal entry",
cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ this.props.reloadConflicts();
+ this.props.addNotification(
+ "success",
+ `Replication glue entry was converted`
+ );
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to convert glue entry: ${dn} - ${errMsg.desc}`
+ );
+ });
+ }
+
+ confirmDeleteGlue (dn) {
+ this.setState({
+ showConfirmDeleteGlue: true,
+ glueEntry: dn
+ });
+ }
+
+ deleteGlue (dn) {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "delete-glue", dn];
+ log_cmd("deleteGlue", "Delete glue entry", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ this.props.reloadConflicts();
+ this.props.addNotification(
+ "success",
+ `Replication glue entry was deleted`
+ );
+ })
+ .fail(err => {
+ let errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ `Failed to delete glue entry: ${dn} - ${errMsg.desc}`
+ );
+ });
+ }
+
+ closeConfirmDeleteGlue () {
+ this.setState({
+ showConfirmDeleteGlue: false,
+ glueEntry: ""
+ });
+ }
+
+ confirmConvertConflict (dn) {
+ if (this.state.convertRDN == "") {
+ this.props.addNotification(
+ "error",
+ `You must provide a RDN if you want to convert the Conflict Entry`
+ );
+ return;
+ }
+ this.setState({
+ showConfirmConvertConflict: true,
+ conflictEntry: dn
+ });
+ }
+
+ closeConfirmConvertConflict () {
+ this.setState({
+ showConfirmConvertConflict: false,
+ conflictEntry: ""
+ });
+ }
+
+ confirmSwapConflict (dn) {
+ this.setState({
+ showConfirmSwapConflict: true,
+ conflictEntry: dn
+ });
+ }
+
+ closeConfirmSwapConflict () {
+ this.setState({
+ showConfirmSwapConflict: false,
+ conflictEntry: ""
+ });
+ }
+
+ confirmDeleteConflict (dn) {
+ this.setState({
+ showConfirmDeleteConflict: true,
+ conflictEntry: dn
+ });
+ }
+
+ closeConfirmDeleteConflict () {
+ this.setState({
+ showConfirmDeleteConflict: false,
+ conflictEntry: ""
+ });
+ }
+
+ closeCompareModal () {
+ this.setState({
+ showCompareModal: false
+ });
}
handleNavSelect(key) {
@@ -354,13 +613,23 @@ export class ReplMonitor extends React.Component {
});
}
+ handleConvertChange(e) {
+ const value = e.target.value;
+ this.setState({
+ convertRDN: value,
+ });
+ }
+
render() {
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 agmtDetailModal = "";
let winsyncAgmtDetailModal = "";
+ let compareConflictModal = "";
if (this.state.showAgmtModal) {
agmtDetailModal =
@@ -381,6 +650,19 @@ export class ReplMonitor extends React.Component {
initAgmt={this.confirmWinsyncInit}
/>;
}
+ if (this.state.showCompareModal) {
+ compareConflictModal =
+ <ConflictCompareModal
+ showModal
+ conflictEntry={this.state.cmpConflictEntry}
+ validEntry={this.state.cmpValidEntry}
+ swapFunc={this.confirmSwapConflict}
+ convertFunc={this.confirmConvertConflict}
+ deleteFunc={this.confirmDeleteConflict}
+ handleConvertChange={this.handleConvertChange}
+ closeHandler={this.closeCompareModal}
+ />;
+ }
let cleanNavTitle = 'CleanAllRUV Tasks <font size="1">('
+ cleanTasks.length + ')</font>';
let abortNavTitle = 'Abort CleanAllRUV Tasks <font
size="1">(' + abortTasks.length + ')</font>';
@@ -414,9 +696,60 @@ export class ReplMonitor extends React.Component {
</TabContent>
</div>;
- let AgmtNavTitle = 'Agreements <font size="1">(' +
replAgmts.length + ')</font>';
- let WinsyncAgmtNavTitle = 'Winsync Agreements <font
size="1">(' + replWinsyncAgmts.length + ')</font>';
- let TasksNavTitle = 'Tasks <font size="1">(' +
(cleanTasks.length + abortTasks.length) + ')</font>';
+ let conflictNavTitle = 'Conflict Entries <font
size="1">(' + conflictEntries.length + ')</font>';
+ let glueNavTitle = 'Glue Entries <font size="1">(' +
glueEntries.length + ')</font>';
+ let conflictContent =
+ <div>
+ <Nav bsClass="nav nav-tabs nav-tabs-pf">
+ <NavItem className="ds-nav-med" eventKey={1}>
+ <div dangerouslySetInnerHTML={{__html: conflictNavTitle}}
/>
+ </NavItem>
+ <NavItem className="ds-nav-med" eventKey={2}>
+ <div dangerouslySetInnerHTML={{__html: glueNavTitle}} />
+ </NavItem>
+ </Nav>
+ <TabContent>
+ <TabPane eventKey={1}>
+ <div className="ds-indent ds-margin-top-lg">
+ <p>
+ Replication conflict entries occur when two entries are
created with the same
+ DN on different servers. The automatic conflict
resolution procedure renames
+ the last entry created to include the entry's unique
identifier (nsuniqueid)
+ in the DN. There are several ways to resolve a conflict,
but that is up to
+ you on which option to use.
+ </p>
+ <ConflictTable
+ conflicts={conflictEntries}
+ resolveConflict={this.resolveConflict}
+ key={conflictEntries}
+ />
+ </div>
+ </TabPane>
+ <TabPane eventKey={2}>
+ <div className="ds-indent ds-margin-top-lg">
+ <p>
+ When a <b>Delete</b> operation is replicated
and the consumer server finds that the entry to be
+ deleted has child entries, the conflict resolution
procedure creates a "<i>glue entry</i>" to
+ avoid having orphaned entries in the database. In the
same way, when an <b>Add</b> operation is
+ replicated and the consumer server cannot find the parent
entry, the conflict resolution
+ procedure creates a "<i>glue
entry</i>" representing the parent so that the new entry is not an
+ orphan entry.
+ </p>
+ <GlueTable
+ glues={glueEntries}
+ convertGlue={this.confirmConvertGlue}
+ deleteGlue={this.confirmDeleteGlue}
+ key={glueEntries}
+ />
+ </div>
+ </TabPane>
+ </TabContent>
+ </div>;
+
+ let replAgmtNavTitle = 'Replication Agreements <font
size="1">(' + replAgmts.length + ')</font>';
+ let winsyncNavTitle = 'Winsync Agreements <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>';
return (
<div id="monitor-suffix-page" className="container-fluid
ds-tab-table">
@@ -424,18 +757,21 @@ export class ReplMonitor extends React.Component {
<div>
<Nav bsClass="nav nav-tabs nav-tabs-pf">
<NavItem eventKey={1}>
- <div dangerouslySetInnerHTML={{__html: AgmtNavTitle}}
/>
+ <div dangerouslySetInnerHTML={{__html:
replAgmtNavTitle}} />
</NavItem>
<NavItem eventKey={2}>
- <div dangerouslySetInnerHTML={{__html:
WinsyncAgmtNavTitle}} />
+ <div dangerouslySetInnerHTML={{__html:
winsyncNavTitle}} />
</NavItem>
<NavItem eventKey={3}>
- <div dangerouslySetInnerHTML={{__html: TasksNavTitle}}
/>
+ <div dangerouslySetInnerHTML={{__html: tasksNavTitle}}
/>
+ </NavItem>
+ <NavItem eventKey={4}>
+ <div dangerouslySetInnerHTML={{__html:
conflictsNavTitle}} />
</NavItem>
</Nav>
<TabContent>
<TabPane eventKey={1}>
- <div className="ds-margin-top-lg">
+ <div className="ds-indent ds-tab-table">
<AgmtTable
agmts={replAgmts}
pokeAgmt={this.pokeAgmt}
@@ -451,9 +787,8 @@ export class ReplMonitor extends React.Component {
</Button>
</div>
</TabPane>
-
<TabPane eventKey={2}>
- <div className="ds-margin-top-lg">
+ <div className="dds-indent
ds-tab-table">
<WinsyncAgmtTable
agmts={replWinsyncAgmts}
pokeAgmt={this.pokeWinsyncAgmt}
@@ -461,7 +796,6 @@ export class ReplMonitor extends React.Component {
/>
</div>
</TabPane>
-
<TabPane eventKey={3}>
<div className="ds-indent ds-tab-table">
<TabContainer id="task-tabs"
defaultActiveKey={1}>
@@ -469,6 +803,13 @@ export class ReplMonitor extends React.Component {
</TabContainer>
</div>
</TabPane>
+ <TabPane eventKey={4}>
+ <div className="ds-indent ds-tab-table">
+ <TabContainer id="task-tabs"
defaultActiveKey={1}>
+ {conflictContent}
+ </TabContainer>
+ </div>
+ </TabPane>
</TabContent>
</div>
</TabContainer>
@@ -508,8 +849,49 @@ export class ReplMonitor extends React.Component {
pokeAgmt={this.pokeAgmt}
viewAgmt={this.showAgmtModal}
/>
+ <ConfirmPopup
+ showModal={this.state.showConfirmDeleteGlue}
+ closeHandler={this.closeConfirmDeleteGlue}
+ actionFunc={this.deleteGlue}
+ actionParam={this.state.glueEntry}
+ msg="Are you really sure you want to delete this glue entry and
its child entries?"
+ msgContent={this.state.glueEntry}
+ />
+ <ConfirmPopup
+ showModal={this.state.showConfirmConvertGlue}
+ closeHandler={this.closeConfirmConvertGlue}
+ actionFunc={this.convertGlue}
+ actionParam={this.state.glueEntry}
+ msg="Are you really sure you want to convert this glue entry to
a regular entry?"
+ msgContent={this.state.glueEntry}
+ />
+ <ConfirmPopup
+ showModal={this.state.showConfirmConvertConflict}
+ closeHandler={this.closeConfirmConvertConflict}
+ actionFunc={this.convertConflict}
+ actionParam={this.state.conflictEntry}
+ msg="Are you really sure you want to convert this conflict entry
to a regular entry?"
+ msgContent={this.state.conflictEntry}
+ />
+ <ConfirmPopup
+ showModal={this.state.showConfirmSwapConflict}
+ closeHandler={this.closeConfirmSwapConflict}
+ actionFunc={this.swapConflict}
+ actionParam={this.state.conflictEntry}
+ msg="Are you really sure you want to swap this conflict entry
with the valid entry (this would remove the valid entry and any child entries it might
have)?"
+ msgContent={this.state.conflictEntry}
+ />
+ <ConfirmPopup
+ showModal={this.state.showConfirmDeleteConflict}
+ closeHandler={this.closeConfirmDeleteConflict}
+ actionFunc={this.deleteConflict}
+ actionParam={this.state.conflictEntry}
+ msg="Are you really sure you want to delete this conflict
entry?"
+ msgContent={this.state.conflictEntry}
+ />
{agmtDetailModal}
{winsyncAgmtDetailModal}
+ {compareConflictModal}
</div>
);
}
@@ -524,6 +906,7 @@ ReplMonitor.propTypes = {
addNotification: PropTypes.func,
reloadAgmts: PropTypes.func,
reloadWinsyncAgmts: PropTypes.func,
+ reloadConflicts: PropTypes.func,
};
ReplMonitor.defaultProps = {
@@ -533,6 +916,7 @@ ReplMonitor.defaultProps = {
addNotification: noop,
reloadAgmts: noop,
reloadWinsyncAgmts: noop,
+ reloadConflicts: noop,
};
export default ReplMonitor;
diff --git a/src/cockpit/389-console/src/monitor.jsx
b/src/cockpit/389-console/src/monitor.jsx
index ae67f4a..89d47b0 100644
--- a/src/cockpit/389-console/src/monitor.jsx
+++ b/src/cockpit/389-console/src/monitor.jsx
@@ -131,6 +131,8 @@ export class Monitor extends React.Component {
this.replSuffixChange = this.replSuffixChange.bind(this);
this.reloadReplAgmts = this.reloadReplAgmts.bind(this);
this.reloadReplWinsyncAgmts = this.reloadReplWinsyncAgmts.bind(this);
+ this.loadConflicts = this.loadConflicts.bind(this);
+ this.loadGlues = this.loadGlues.bind(this);
// Logging
this.loadMonitor = this.loadMonitor.bind(this);
this.refreshAccessLog = this.refreshAccessLog.bind(this);
@@ -661,6 +663,53 @@ export class Monitor extends React.Component {
...this.state[this.state.replSuffix],
abortTasks: config.items,
},
+ }, this.loadConflicts());
+ })
+ .fail(() => {
+ // Notification of failure (could only be server down)
+ this.setState({
+ replLoading: false,
+ });
+ });
+ }
+
+ loadConflicts() {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "list", this.state.replSuffix];
+ log_cmd("loadConflicts", "Load conflict entries", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ let config = JSON.parse(content);
+ this.setState({
+ [this.state.replSuffix]: {
+ ...this.state[this.state.replSuffix],
+ conflicts: config.items,
+ glues: []
+ },
+ }, this.loadGlues());
+ })
+ .fail(() => {
+ // Notification of failure (could only be server down)
+ this.setState({
+ replLoading: false,
+ });
+ });
+ }
+
+ loadGlues() {
+ let cmd = ["dsconf", "-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ "repl-conflict", "list-glue", this.state.replSuffix];
+ log_cmd("loadGlues", "Load glue entries", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ let config = JSON.parse(content);
+ this.setState({
+ [this.state.replSuffix]: {
+ ...this.state[this.state.replSuffix],
+ glues: config.items,
+ },
}, this.setState(
{
replLoading: false,
@@ -1089,6 +1138,7 @@ export class Monitor extends React.Component {
addNotification={this.addNotification}
reloadAgmts={this.reloadReplAgmts}
reloadWinsyncAgmts={this.reloadReplWinsyncAgmts}
+ reloadConflicts={this.loadConflicts}
key={this.state.replSuffix}
/>
</div>
diff --git a/src/lib389/lib389/conflicts.py b/src/lib389/lib389/conflicts.py
index b1f86e0..ec7b67f 100644
--- a/src/lib389/lib389/conflicts.py
+++ b/src/lib389/lib389/conflicts.py
@@ -9,6 +9,7 @@
import ldap
from lib389._mapped_object import DSLdapObject, DSLdapObjects, _gen_filter
+from lib389.utils import is_a_dn
class ConflictEntry(DSLdapObject):
@@ -27,11 +28,14 @@ class ConflictEntry(DSLdapObject):
self._object_filter = '(objectclass=ldapsubentry)'
def convert(self, new_rdn):
- """Convert conflict entry to a vlid entry, but we need to
+ """Convert conflict entry to a valid entry, but we need to
give the conflict entry a new rdn since we are not replacing
the existing valid counterpart entry.
"""
+ if not is_a_dn(new_rdn):
+ raise ValueError("The new RDN (" + new_rdn + ") is not a valid
DN")
+
# Get the conflict entry info
conflict_value = self.get_attr_val_utf8('nsds5ReplConflict')
entry_dn = conflict_value.split(' ', 3)[2]
@@ -62,10 +66,13 @@ class ConflictEntry(DSLdapObject):
new_rdn = "{}={}".format(rdn_attr, entry_rdn)
tmp_rdn = new_rdn + 'tmp'
- # Delete valid entry (to be replaced by conflict entry)
+ # Delete valid entry and its children (to be replaced by conflict entry)
original_entry = DSLdapObject(self._instance, dn=entry_dn)
original_entry._protected = False
- original_entry.delete()
+ filterstr = "(|(objectclass=*)(objectclass=ldapsubentry))"
+ ents = self._instance.search_s(original_entry._dn, ldap.SCOPE_SUBTREE, filterstr,
escapehatch='i am sure')
+ for ent in sorted(ents, key=lambda e: len(e.dn), reverse=True):
+ self._instance.delete_ext_s(ent.dn, serverctrls=self._server_controls,
clientctrls=self._client_controls, escapehatch='i am sure')
# Rename conflict entry to tmp rdn so we can clean up the rdn attr
self.rename(tmp_rdn, deloldrdn=False)
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.