From b6729a99f3a3d4c6ebe82d4bb60ea2a6f8727782 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 27 Jun 2025 18:43:39 -0700 Subject: [PATCH] Issue 6822 - Backend creation cleanup and Database UI tab error handling (#6823) Description: Add rollback functionality when mapping tree creation fails during backend creation to prevent orphaned backends. Improve error handling in Database, Replication and Monitoring UI tabs to gracefully handle backend get-tree command failures. Fixes: https://github.com/389ds/389-ds-base/issues/6822 Reviewed by: @mreynolds389 (Thanks!) --- src/cockpit/389-console/src/database.jsx | 119 ++++++++------ src/cockpit/389-console/src/monitor.jsx | 172 +++++++++++--------- src/cockpit/389-console/src/replication.jsx | 55 ++++--- src/lib389/lib389/backend.py | 18 +- 4 files changed, 210 insertions(+), 154 deletions(-) diff --git a/src/cockpit/389-console/src/database.jsx b/src/cockpit/389-console/src/database.jsx index c0c4be414..276125dfc 100644 --- a/src/cockpit/389-console/src/database.jsx +++ b/src/cockpit/389-console/src/database.jsx @@ -478,6 +478,59 @@ export class Database extends React.Component { } loadSuffixTree(fullReset) { + const treeData = [ + { + name: _("Global Database Configuration"), + icon: , + id: "dbconfig", + }, + { + name: _("Chaining Configuration"), + icon: , + id: "chaining-config", + }, + { + name: _("Backups & LDIFs"), + icon: , + id: "backups", + }, + { + name: _("Password Policies"), + id: "pwp", + icon: , + children: [ + { + name: _("Global Policy"), + icon: , + id: "pwpolicy", + }, + { + name: _("Local Policies"), + icon: , + id: "localpwpolicy", + }, + ], + defaultExpanded: true + }, + { + name: _("Suffixes"), + icon: , + id: "suffixes-tree", + children: [], + defaultExpanded: true, + action: ( + + ), + } + ]; + const cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", "backend", "get-tree", @@ -491,58 +544,20 @@ export class Database extends React.Component { suffixData = JSON.parse(content); this.processTree(suffixData); } - const treeData = [ - { - name: _("Global Database Configuration"), - icon: , - id: "dbconfig", - }, - { - name: _("Chaining Configuration"), - icon: , - id: "chaining-config", - }, - { - name: _("Backups & LDIFs"), - icon: , - id: "backups", - }, - { - name: _("Password Policies"), - id: "pwp", - icon: , - children: [ - { - name: _("Global Policy"), - icon: , - id: "pwpolicy", - }, - { - name: _("Local Policies"), - icon: , - id: "localpwpolicy", - }, - ], - defaultExpanded: true - }, - { - name: _("Suffixes"), - icon: , - id: "suffixes-tree", - children: suffixData, - defaultExpanded: true, - action: ( - - ), - } - ]; + + let current_node = this.state.node_name; + if (fullReset) { + current_node = DB_CONFIG; + } + + treeData[4].children = suffixData; // suffixes node + this.setState(() => ({ + nodes: treeData, + node_name: current_node, + }), this.loadAttrs); + }) + .fail(err => { + // Handle backend get-tree failure gracefully let current_node = this.state.node_name; if (fullReset) { current_node = DB_CONFIG; diff --git a/src/cockpit/389-console/src/monitor.jsx b/src/cockpit/389-console/src/monitor.jsx index ad48d1f87..91a8e3e37 100644 --- a/src/cockpit/389-console/src/monitor.jsx +++ b/src/cockpit/389-console/src/monitor.jsx @@ -200,6 +200,84 @@ export class Monitor extends React.Component { } loadSuffixTree(fullReset) { + const basicData = [ + { + name: _("Server Statistics"), + icon: , + id: "server-monitor", + type: "server", + }, + { + name: _("Replication"), + icon: , + id: "replication-monitor", + type: "replication", + defaultExpanded: true, + children: [ + { + name: _("Synchronization Report"), + icon: , + id: "sync-report", + item: "sync-report", + type: "repl-mon", + }, + { + name: _("Log Analysis"), + icon: , + id: "log-analysis", + item: "log-analysis", + type: "repl-mon", + } + ], + }, + { + name: _("Database"), + icon: , + id: "database-monitor", + type: "database", + children: [], // Will be populated with treeData on success + defaultExpanded: true, + }, + { + name: _("Logging"), + icon: , + id: "log-monitor", + defaultExpanded: true, + children: [ + { + name: _("Access Log"), + icon: , + id: "access-log-monitor", + type: "log", + }, + { + name: _("Audit Log"), + icon: , + id: "audit-log-monitor", + type: "log", + }, + { + name: _("Audit Failure Log"), + icon: , + id: "auditfail-log-monitor", + type: "log", + }, + { + name: _("Errors Log"), + icon: , + id: "error-log-monitor", + type: "log", + }, + { + name: _("Security Log"), + icon: , + id: "security-log-monitor", + type: "log", + }, + ] + }, + ]; + const cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", "backend", "get-tree", @@ -210,83 +288,7 @@ export class Monitor extends React.Component { .done(content => { const treeData = JSON.parse(content); this.processTree(treeData); - const basicData = [ - { - name: _("Server Statistics"), - icon: , - id: "server-monitor", - type: "server", - }, - { - name: _("Replication"), - icon: , - id: "replication-monitor", - type: "replication", - defaultExpanded: true, - children: [ - { - name: _("Synchronization Report"), - icon: , - id: "sync-report", - item: "sync-report", - type: "repl-mon", - }, - { - name: _("Log Analysis"), - icon: , - id: "log-analysis", - item: "log-analysis", - type: "repl-mon", - } - ], - }, - { - name: _("Database"), - icon: , - id: "database-monitor", - type: "database", - children: [], - defaultExpanded: true, - }, - { - name: _("Logging"), - icon: , - id: "log-monitor", - defaultExpanded: true, - children: [ - { - name: _("Access Log"), - icon: , - id: "access-log-monitor", - type: "log", - }, - { - name: _("Audit Log"), - icon: , - id: "audit-log-monitor", - type: "log", - }, - { - name: _("Audit Failure Log"), - icon: , - id: "auditfail-log-monitor", - type: "log", - }, - { - name: _("Errors Log"), - icon: , - id: "error-log-monitor", - type: "log", - }, - { - name: _("Security Log"), - icon: , - id: "security-log-monitor", - type: "log", - }, - ] - }, - ]; + let current_node = this.state.node_name; let type = this.state.node_type; if (fullReset) { @@ -296,6 +298,22 @@ export class Monitor extends React.Component { basicData[2].children = treeData; // database node this.processReplSuffixes(basicData[1].children); + this.setState(() => ({ + nodes: basicData, + node_name: current_node, + node_type: type, + }), this.update_tree_nodes); + }) + .fail(err => { + // Handle backend get-tree failure gracefully + let current_node = this.state.node_name; + let type = this.state.node_type; + if (fullReset) { + current_node = "server-monitor"; + type = "server"; + } + this.processReplSuffixes(basicData[1].children); + this.setState(() => ({ nodes: basicData, node_name: current_node, diff --git a/src/cockpit/389-console/src/replication.jsx b/src/cockpit/389-console/src/replication.jsx index fa492fd2a..aa535bfc7 100644 --- a/src/cockpit/389-console/src/replication.jsx +++ b/src/cockpit/389-console/src/replication.jsx @@ -177,6 +177,16 @@ export class Replication extends React.Component { loaded: false }); + const basicData = [ + { + name: _("Suffixes"), + icon: , + id: "repl-suffixes", + children: [], + defaultExpanded: true + } + ]; + const cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", "backend", "get-tree", @@ -199,15 +209,7 @@ export class Replication extends React.Component { } } } - const basicData = [ - { - name: _("Suffixes"), - icon: , - id: "repl-suffixes", - children: [], - defaultExpanded: true - } - ]; + let current_node = this.state.node_name; let current_type = this.state.node_type; let replicated = this.state.node_replicated; @@ -258,6 +260,19 @@ export class Replication extends React.Component { } basicData[0].children = treeData; + this.setState({ + nodes: basicData, + node_name: current_node, + node_type: current_type, + node_replicated: replicated, + }, () => { this.update_tree_nodes() }); + }) + .fail(err => { + // Handle backend get-tree failure gracefully + let current_node = this.state.node_name; + let current_type = this.state.node_type; + let replicated = this.state.node_replicated; + this.setState({ nodes: basicData, node_name: current_node, @@ -905,18 +920,18 @@ export class Replication extends React.Component { disableTree: false }); }); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - cockpit.format(_("Error loading replication agreements configuration - $0"), errMsg.desc) - ); - this.setState({ - suffixLoading: false, - disableTree: false + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + cockpit.format(_("Error loading replication agreements configuration - $0"), errMsg.desc) + ); + this.setState({ + suffixLoading: false, + disableTree: false + }); }); - }); }) .fail(err => { // changelog failure diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py index 1319fa0cd..5bff61c58 100644 --- a/src/lib389/lib389/backend.py +++ b/src/lib389/lib389/backend.py @@ -694,24 +694,32 @@ class Backend(DSLdapObject): parent_suffix = properties.pop('parent', False) # Okay, now try to make the backend. - super(Backend, self).create(dn, properties, basedn) + backend_obj = super(Backend, self).create(dn, properties, basedn) # We check if the mapping tree exists in create, so do this *after* if create_mapping_tree is True: - properties = { + mapping_tree_properties = { 'cn': self._nprops_stash['nsslapd-suffix'], 'nsslapd-state': 'backend', 'nsslapd-backend': self._nprops_stash['cn'], } if parent_suffix: # This is a subsuffix, set the parent suffix - properties['nsslapd-parent-suffix'] = parent_suffix - self._mts.create(properties=properties) + mapping_tree_properties['nsslapd-parent-suffix'] = parent_suffix + + try: + self._mts.create(properties=mapping_tree_properties) + except Exception as e: + try: + backend_obj.delete() + except Exception as cleanup_error: + self._instance.log.error(f"Failed to cleanup backend after mapping tree creation failure: {cleanup_error}") + raise e # We can't create the sample entries unless a mapping tree was installed. if sample_entries is not False and create_mapping_tree is True: self.create_sample_entries(sample_entries) - return self + return backend_obj def delete(self): """Deletes the backend, it's mapping tree and all related indices. -- 2.49.0