From 3054205d93f6dddbfb86de040ac55521c08ca13b Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Jan 13 2020 18:49:20 +0000 Subject: 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!) --- 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 = - -
-
- Authenticating to all the replicas ... -
-
; - } - - return ( - -
- - - - Replication Login Credentials - - - -
-

- 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. -

-
- - - - Bind DN - - - - - - - - - - Password - - - - - - - {spinner} -
-
- - - - -
-
- ); - } -} - -class ReplLagReportModal extends React.Component { - render() { - const { - showModal, - closeHandler, - agmts, - pokeAgmt, - viewAgmt - } = this.props; - - return ( - - - - - Replication Lag Report - - - - - - - - - - ); - } -} - 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 = ; + } return ( @@ -327,13 +196,7 @@ class AgmtDetailsModal extends React.Component { - + {initButton} + {newEntry ? "Add" : "Edit"} Report Credentials + + + + +
+ + + + Hostname + + + + + + + + + + Port + + + + + + + + + + Bind DN + + + + + + + + + + Password + + + + + + + + + + Interractive Input + + + + + + +
+ +
+
+ + + + + +
+ ); + } +} + +class ReportAliasesModal extends React.Component { + render() { + const { + handleFieldChange, + showModal, + closeHandler, + newEntry, + hostname, + port, + alias, + addConfig, + editConfig + } = this.props; + + return ( + +
+ + + {newEntry ? "Add" : "Edit"} Report Credentials + + + + +
+ + + + Alias + + + + + + + + + + Hostname + + + + + + + + + + Port + + + + + + +
+ +
+
+ + + + +
+
+ ); + } +} + +class ReportLoginModal extends React.Component { + render() { + const { + showModal, + closeHandler, + handleChange, + processCredsInput, + instanceName, + disableBinddn, + loginBinddn, + loginBindpw + } = this.props; + + return ( + +
+ + + Replication Login Credentials for {instanceName} + + +
+

+ In order to get the replication agreement lag times and state the + authentication credentials to the remote replicas must be provided. +

+
+

+ Bind DN was acquired from Replica Credentials table. If you want + to bind as another user, change or remove the Bind DN there. +

+
+ + + + Bind DN + + + + + + + + + + Password + + + + + + +
+
+ + + + +
+
+ ); + } +} + +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 = ; + if (reportLoading) { + spinner = ( +
+ + {reportRefreshing ? "Refreshing" : "Loading"} the report... + + +
+ ); + } + let reportHeader = ""; + if (reportData.length > 0) { + reportHeader = ( +
+ + + + Show All (Including Disabled Agreements) + + + + + + + Table View + + + + +
+
+ ); + } 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 = [(
+ +
+ )]; + } else { + for (let supplier of reportData) { + let s_data = supplier.data; + if (s_data.length === 1 && s_data[0].replica_status.startsWith("Unavailable")) { + supplierData = ( +
+

+ Can not get replication information from Replica +

+

+ Replica Status: {s_data[0].replica_status} +

+
+ ); + } else { + supplierData = supplier.data.map(replica => ( +
+

+ Replica Root: {replica.replica_root} +

+

+ Replica ID: {replica.replica_id} +

+

+ Replica Status: {replica.replica_status} +

+

+ Max CSN: {replica.maxcsn} +

+ {"agmts_status" in replica && + replica.agmts_status.length > 0 && + "agmt-name" in replica.agmts_status[0] ? ( + + ) : ( +

+ No Agreements Were Found +

+ )} +
+ )); + } + supplierName = ( +
+
+

+ Supplier: {supplier.name} +

+
+
+ {supplierData} +
+ ); + suppliers.push(supplierName); + } + } + + let report = suppliers.map(supplier => ( +
+ {supplier} +
+
+ )); + if (reportLoading) { + report = + + {spinner} + ; + } + + return ( +
+ {reportHeader} + {report} +
+ ); + } +} // 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 [ - - + { - this.props.viewAgmt(rowData['agmt-name']); + this.props.viewAgmt(rowData['agmt-name'][0]); }}> View Agreement Details { - this.props.pokeAgmt(rowData['agmt-name']); + this.props.pokeAgmt(rowData['agmt-name'][0]); }}> Poke Agreement @@ -760,16 +760,16 @@ class AgmtTable extends React.Component { formatters: [ (value, { rowData }) => { return [ - - + { - this.props.viewAgmt(rowData['agmt-name']); + this.props.viewAgmt(rowData['agmt-name'][0]); }}> View Agreement Details { - this.props.pokeAgmt(rowData['agmt-name']); + this.props.pokeAgmt(rowData['agmt-name'][0]); }}> Poke Agreement @@ -1175,16 +1175,16 @@ class LagReportTable extends React.Component { formatters: [ (value, { rowData }) => { return [ - - + { - this.props.viewAgmt(rowData['agmt-name']); + this.props.viewAgmt(rowData['agmt-name'][0]); }}> View Agreement Details { - this.props.pokeAgmt(rowData['agmt-name']); + this.props.pokeAgmt(rowData['agmt-name'][0]); }}> Poke Agreement @@ -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 ( +
+ +
+ ); + } +} + +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 [ + + + { + this.props.editConfig(rowData); + }} + > + Edit Alias + + + { + this.props.deleteConfig(rowData); + }} + > + Delete Alias + + + + ]; + } + ] + } + } + ] + }; + } + + 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 = ( + + ); + } else { + reportAliasTable = ( + + ); + } + + return
{reportAliasTable}
; + } +} + +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 [ + + {value == "" ? Edit To Add a Bind DN Data : value } + + ]; + } + ] + } + }, + { + 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 = Interractive Input is set; + if (!rowData.pwInputInterractive) { + if (value == "") { + pwField = Both Password or Interractive Input flag are not set; + } else { + pwField = "********"; + } + } + return [ + + {pwField} + + ]; + } + ] + } + }, + { + property: "actions", + header: { + props: { + index: 3, + rowSpan: 1, + colSpan: 1 + }, + formatters: [actionHeaderCellFormatter] + }, + cell: { + props: { + index: 3 + }, + formatters: [ + (value, { rowData }) => { + return [ + + + { + this.props.editConfig(rowData); + }} + > + Edit Connection + + + { + this.props.deleteConfig(rowData); + }} + > + Delete Connection + + + + ]; + } + ] + } + } + ] + }; + } + + 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 = ( + + ); + } else { + reportConnTable = ( + + ); + } + + return
{reportConnTable}
; + } +} + +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 [ + + {value || No Agreements Were Found} + + ]; + } + ] + } + }, + { + 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 [ + + + + ]; + } + ] + } + } + ] + }; + } + + 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 = ( + + ); + } else { + reportSingleTable = ( + + ); + } + + return
{reportSingleTable}
; + } +} + +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 [ + + + + ]; + } + ] + } + } ] }; - 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 ( -
+ let reportConsumersTable; + let filteredRows = this.props.rows; + if (!this.props.showDisabledAgreements) { + filteredRows = searchFilter("on", ["replica-enabled"], filteredRows); + } + if (filteredRows.length < 1) { + reportConsumersTable = ( + + ); + } else { + reportConsumersTable = ( -
- ); + ); + } + + return
{reportConsumersTable}
; } } - // 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 = + ; + } + if (this.state.showCredentialsModal) { + reportCredentialsModal = + ; + } + if (this.state.showAliasesModal) { + reportAliasesModal = + ; + } if (this.state.showAgmtModal) { agmtDetailModal = ; } @@ -668,6 +1249,105 @@ export class ReplMonitor extends React.Component { />; } + let reportContent = +
+ + + +
+ +

How To Use Replication Sync Report

+
    +
  1. + Fill in Replica Credentials; +
      +
    • • Initially, the list is populated with existing instance agreements and the active instance itself;
    • +
    • • You can use regular expressions for the Connection Data field;
    • +
    • • It is advised to use an Interactive Input option for a password because it's more secure.
    • +
    +
  2. +
  3. + Add Instance Aliases if needed; +
      +
    • • Adding the aliases will make the report more readable;
    • +
    • • Each instance can have one alias. For example, you can give names like this: + Alias=Main Master, Hostname=192.168.122.01, Port=38901;
    • +
    • • In a result, the report will have an entry like this: + Supplier: Main Master (192.168.122.01:38901).
    • +
    +
  4. +
  5. + Press Generate Report button; +
      +
    • • It will initiate the report creation;
    • +
    • • You may be asked for the credentials while the process is running through the agreements.
    • +
    +
  6. +
  7. + Once report is generated you can review it and enable continuous refreshing. +
      +
    • • More consumer replication data is available under the 'View Data' button;
    • +
    • • You can set the timeout and the new report will be created by that;
    • +
    • • It will use the specified credentials (both preset and from interactive input).
    • +
    +
  8. +
+
+ + + + +

+ +

+
+ +
+ +
+
+
+
; let cleanNavTitle = 'CleanAllRUV Tasks (' + cleanTasks.length + ')'; let abortNavTitle = 'Abort CleanAllRUV Tasks (' + abortTasks.length + ')'; let taskContent = @@ -750,8 +1430,9 @@ export class ReplMonitor extends React.Component { ; - let replAgmtNavTitle = 'Replication Agreements (' + replAgmts.length + ')'; - let winsyncNavTitle = 'Winsync Agreements (' + replWinsyncAgmts.length + ')'; + let fullReportTitle = 'Sync Report'; + let replAgmtNavTitle = 'Agreements (' + replAgmts.length + ')'; + let winsyncNavTitle = 'Winsync (' + replWinsyncAgmts.length + ')'; let tasksNavTitle = 'Tasks (' + (cleanTasks.length + abortTasks.length) + ')'; let conflictsNavTitle = 'Conflicts (' + (conflictEntries.length + glueEntries.length) + ')'; @@ -761,37 +1442,44 @@ export class ReplMonitor extends React.Component {
+ + {reportContent} + +
+
+ +
-
- +
- +
{taskContent}
- +
{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']} /> - - + {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