From 3dd9bd369a6464366ba17cba2029dc4038d35815 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Mon, 24 Apr 2023 12:35:49 -0400 Subject: [PATCH] Issue 5738 - RFE - UI - Read/write replication monitor info to .dsrc file Description: Allow UI to use the .dsrc replication monitor info, and also allow the UI to write new report configurations. This prevents an admin from having to enter this information every time they want to run a report relates: https://github.com/389ds/389-ds-base/issues/5738 Reviewed by: spichugi(Thanks!) --- dirsrvtests/tests/suites/clu/dsrc_test.py | 152 +- dirsrvtests/tests/suites/webui/README | 1 + .../webui/monitoring/monitoring_test.py | 20 +- src/Cargo.lock | 343 +++-- src/cockpit/389-console/src/css/ds.css | 2 +- .../src/lib/monitor/monitorModals.jsx | 184 ++- .../src/lib/monitor/monitorTables.jsx | 170 ++- .../src/lib/monitor/replMonAgmts.jsx | 96 ++ .../src/lib/monitor/replMonConflict.jsx | 559 +++++++ .../src/lib/monitor/replMonTasks.jsx | 146 ++ .../src/lib/monitor/replMonWinsync.jsx | 95 ++ .../src/lib/monitor/replMonitor.jsx | 1327 ++++++++--------- src/cockpit/389-console/src/monitor.jsx | 417 ++++-- src/lib389/lib389/cli_ctl/dsrc.py | 93 +- 14 files changed, 2661 insertions(+), 944 deletions(-) create mode 100644 dirsrvtests/tests/suites/webui/README create mode 100644 src/cockpit/389-console/src/lib/monitor/replMonAgmts.jsx create mode 100644 src/cockpit/389-console/src/lib/monitor/replMonConflict.jsx create mode 100644 src/cockpit/389-console/src/lib/monitor/replMonTasks.jsx create mode 100644 src/cockpit/389-console/src/lib/monitor/replMonWinsync.jsx diff --git a/dirsrvtests/tests/suites/clu/dsrc_test.py b/dirsrvtests/tests/suites/clu/dsrc_test.py index 321f90f5f1..c1fb086e60 100644 --- a/dirsrvtests/tests/suites/clu/dsrc_test.py +++ b/dirsrvtests/tests/suites/clu/dsrc_test.py @@ -11,13 +11,39 @@ import os from os.path import expanduser from lib389.cli_base import FakeArgs -from lib389.cli_ctl.dsrc import create_dsrc, modify_dsrc, delete_dsrc, display_dsrc +from lib389.cli_ctl.dsrc import create_dsrc, modify_dsrc, delete_dsrc, display_dsrc, replmon_dsrc from lib389._constants import DEFAULT_SUFFIX, DN_DM from lib389.topologies import topology_st as topo log = logging.getLogger(__name__) +def get_fake_args(): + # Setup our args + args = FakeArgs() + args.basedn = DEFAULT_SUFFIX + args.groups_rdn = None + args.people_rdn = None + args.binddn = DN_DM + args.json = None + args.uri = None + args.saslmech = None + args.tls_cacertdir = None + args.tls_cert = None + args.tls_key = None + args.tls_reqcert = None + args.starttls = None + args.cancel_starttls = None + args.pwdfile = None + args.do_it = True + args.add_conn = None + args.del_conn = None + args.add_alias = None + args.del_alias = None + + return args + + @pytest.fixture(scope="function") def setup(topo, request): """Preserve any existing .dsrc file""" @@ -65,22 +91,7 @@ def test_dsrc(topo, setup): different_suffix = "o=different" # Setup our args - args = FakeArgs() - args.basedn = DEFAULT_SUFFIX - args.groups_rdn = None - args.people_rdn = None - args.binddn = DN_DM - args.json = None - args.uri = None - args.saslmech = None - args.tls_cacertdir = None - args.tls_cert = None - args.tls_key = None - args.tls_reqcert = None - args.starttls = None - args.cancel_starttls = None - args.pwdfile = None - args.do_it = True + args = get_fake_args() # Create a dsrc configuration entry create_dsrc(inst, log, args) @@ -138,6 +149,113 @@ def test_dsrc(topo, setup): display_dsrc(inst, log, args) +def test_dsrc_repl_mon(topo, setup): + """Test "dsctl dsrc repl-mon" command, add & remove creds and aliases + + :id: 33007d01-f11c-456b-bb16-fcd7920c9fc8 + :setup: Standalone Instance + :steps: + 1. Add connection + 2. Add same connection - should fail + 3. Delete connection + 4. Delete same connection - should fail + 5. Add alias + 6. Add same alias - should fail + 7. Delete alias + 8. Delete same alias again 0 should fail + + :expectedresults: + 1. Success + 2. Success + 3. Success + 4. Success + 5. Success + 6. Success + 7. Success + 8. Success + """ + + inst = topo.standalone + args = get_fake_args() + create_dsrc(inst, log, args) + + # Add replica connection + assert not topo.logcap.contains("repl-monitor-connections") + repl_conn = "replica_1:localhost:5555:cn=directory manager:password" + args.add_conn = [repl_conn,] + replmon_dsrc(inst, log, args) + display_dsrc(inst, topo.logcap.log, args) + assert topo.logcap.contains("repl-monitor-connections") + assert topo.logcap.contains("replica_1 = localhost:5555:cn=directory manager:password") + topo.logcap.flush() + args.add_conn = None + + # Add duplicate replica connection + args.add_conn = [repl_conn, ] + try: + replmon_dsrc(inst, log, args) + assert False + except ValueError: + pass + args.add_conn = None + + # Delete replica connection + args.del_conn = ["replica_1"] + replmon_dsrc(inst, log, args) + display_dsrc(inst, topo.logcap.log, args) + assert not topo.logcap.contains("replica_1 = localhost:5555:cn=directory manager:password") + assert not topo.logcap.contains("repl-monitor-connections") + topo.logcap.flush() + args.del_conn = None + + # Delete replica connection (already deleted) + args.del_conn = ["replica_1"] + try: + replmon_dsrc(inst, log, args) + assert False + except ValueError: + pass + args.del_conn = None + + # Add Alias + assert not topo.logcap.contains("repl-monitor-aliases") + repl_alias = "my_alias:localhost:4444" + args.add_alias = [repl_alias,] + replmon_dsrc(inst, log, args) + display_dsrc(inst, topo.logcap.log, args) + assert topo.logcap.contains("repl-monitor-aliases") + assert topo.logcap.contains("my_alias = localhost:4444") + topo.logcap.flush() + args.add_alias = None + + # Add Duplicate Alias + args.add_alias = [repl_alias,] + try: + replmon_dsrc(inst, log, args) + assert False + except ValueError: + pass + args.add_alias = None + + # Delete Alias + args.del_alias = ["my_alias",] + replmon_dsrc(inst, log, args) + display_dsrc(inst, topo.logcap.log, args) + assert not topo.logcap.contains("my_alias = localhost:4444") + assert not topo.logcap.contains("repl-monitor-aliases") + topo.logcap.flush() + args.del_alias = None + + # Delete alias (already deleted) + args.del_alias = ["my_alias", ] + try: + replmon_dsrc(inst, log, args) + assert False + except ValueError: + pass + args.del_alias = None + + if __name__ == '__main__': # Run isolated # -s for DEBUG mode diff --git a/dirsrvtests/tests/suites/webui/README b/dirsrvtests/tests/suites/webui/README new file mode 100644 index 0000000000..2481a21fee --- /dev/null +++ b/dirsrvtests/tests/suites/webui/README @@ -0,0 +1 @@ +To run locally you need to set WEBUI=1 and PASSWD= env variables. You also need to remove "root" from /etc/cockpit/disallowed-users diff --git a/dirsrvtests/tests/suites/webui/monitoring/monitoring_test.py b/dirsrvtests/tests/suites/webui/monitoring/monitoring_test.py index 173e503cb6..33d426e895 100644 --- a/dirsrvtests/tests/suites/webui/monitoring/monitoring_test.py +++ b/dirsrvtests/tests/suites/webui/monitoring/monitoring_test.py @@ -129,24 +129,14 @@ def test_replication_visibility(topology_st, page, browser_name): log.info('Click on Monitoring tab and then on Replication in the menu and check if element is loaded.') frame.get_by_role('tab', name='Monitoring', exact=True).click() frame.locator('#replication-monitor').click() + frame.get_by_role('button', name='Synchronization Report').wait_for() + frame.locator('#sync-report').click() + frame.get_by_role('tab', name='Prepare New Report').click() frame.get_by_role('button', name='Generate Report').wait_for() assert frame.get_by_role('button', name='Generate Report').is_visible() log.info('Click on Agreements tab and check if element is loaded.') - frame.get_by_role('tab', name='Agreements').click() - assert frame.get_by_role('columnheader', name='Replication Agreements').is_visible() - - log.info('Click on Winsync tab and check if element is loaded.') - frame.get_by_role('tab', name='Winsync').click() - assert frame.get_by_role('columnheader', name='Winsync Agreements').is_visible() - - log.info('Click on Tasks tab and check if element is loaded.') - frame.get_by_role('tab', name='Tasks').click() - assert frame.get_by_role('columnheader', name='CleanAllRUV Tasks').is_visible() - - log.info('Click on Conflict Entries tab and check if element is loaded.') - frame.get_by_role('tab', name='Conflict Entries').click() - assert frame.get_by_role('columnheader', name='Replication Conflict Entries').is_visible() + assert frame.locator('#replication-suffix-dc\\=example\\,dc\\=com').is_visible() def test_database_visibility(topology_st, page, browser_name): @@ -169,7 +159,7 @@ def test_database_visibility(topology_st, page, browser_name): log.info('Click on Monitoring tab, then click on database button and check if element is loaded.') frame.get_by_role('tab', name='Monitoring', exact=True).click() - frame.locator('#dc\=example\,dc\=com').click() + frame.locator('#dc\\=example\\,dc\\=com').click() frame.get_by_text('Entry Cache Hit Ratio').wait_for() assert frame.get_by_text('Entry Cache Hit Ratio').is_visible() diff --git a/src/Cargo.lock b/src/Cargo.lock index a53fc82642..d1ae4ebcdf 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -28,7 +28,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -69,16 +69,16 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 1.0.109", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -147,9 +147,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg", "cfg-if", @@ -181,9 +181,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] @@ -210,11 +210,32 @@ dependencies = [ "uuid", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -249,9 +270,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -276,6 +297,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "instant" version = "0.1.12" @@ -285,26 +312,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "libc" -version = "0.2.139" +version = "0.2.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "librnsslapd" @@ -325,6 +363,12 @@ dependencies = [ "slapd", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" + [[package]] name = "lock_api" version = "0.4.9" @@ -355,24 +399,24 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" -version = "0.10.48" +version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags", "cfg-if", @@ -385,22 +429,21 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "openssl-sys" -version = "0.9.83" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -427,7 +470,7 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] @@ -477,9 +520,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -499,9 +542,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -546,19 +589,33 @@ dependencies = [ ] [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "winapi", + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.37.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", ] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "scopeguard" @@ -568,29 +625,29 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.152" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -627,9 +684,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -637,29 +694,27 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", - "syn", - "unicode-xid", + "unicode-ident", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", ] [[package]] @@ -673,41 +728,41 @@ dependencies = [ [[package]] name = "tokio" -version = "1.25.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" dependencies = [ "autocfg", "pin-project-lite", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "toml" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-width" @@ -715,12 +770,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "uuid" version = "0.8.2" @@ -778,78 +827,152 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "zeroize" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.15", ] diff --git a/src/cockpit/389-console/src/css/ds.css b/src/cockpit/389-console/src/css/ds.css index b43ed34cd6..8353e0b154 100644 --- a/src/cockpit/389-console/src/css/ds.css +++ b/src/cockpit/389-console/src/css/ds.css @@ -66,7 +66,7 @@ td { width: 300px; min-width: 300px; min-height: 350px; - max-height: 550px; + max-height: 650px; margin-bottom: 5px; overflow: auto; margin-left: 15px; diff --git a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx index b4baf8be45..cb01a7475b 100644 --- a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx +++ b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx @@ -20,6 +20,7 @@ import { import { CopyIcon, } from '@patternfly/react-icons'; +import faSyncAlt from '@fortawesome/free-solid-svg-icons'; import PropTypes from "prop-types"; import { get_date_string } from "../tools.jsx"; import { ReportSingleTable, ReportConsumersTable } from "./monitorTables.jsx"; @@ -413,7 +414,7 @@ class ReportCredentialsModal extends React.Component { key="save" variant="primary" onClick={newEntry ? addConfig : editConfig} - isDisabled={hostname == "" || binddn == "" || bindpw == ""} + isDisabled={hostname === "" || binddn === "" || bindpw === ""} > Save , @@ -519,6 +520,158 @@ class ReportCredentialsModal extends React.Component { } } +class ReportConnectionModal extends React.Component { + render() { + const { + handleFieldChange, + showModal, + closeHandler, + name, + hostname, + port, + binddn, + pwInputInterractive, + bindpw, + addConn, + onMinusConfig, + onPlusConfig, + onConfigChange, + } = this.props; + + return ( + + Save + , + + ]} + > + + +
+ + + Connection Name + + + { + handleFieldChange(e); + }} + /> + + + + + Hostname + + + { + handleFieldChange(e); + }} + /> + + + + + Port + + + { onMinusConfig("connPort") }} + onChange={(e) => { onConfigChange(e, "connPort", 1) }} + onPlus={() => { onPlusConfig("connPort") }} + inputName="input" + inputAriaLabel="number input" + minusBtnAriaLabel="minus" + plusBtnAriaLabel="plus" + widthChars={8} + /> + + + + + Bind DN + + + { + handleFieldChange(e); + }} + /> + + + + + Password + + + { + handleFieldChange(e); + }} + /> + + + + + Interractive Input + + + { + handleFieldChange(e); + }} + /> + + +
+
+
+
+ ); + } +} + class ReportAliasesModal extends React.Component { render() { const { @@ -862,7 +1015,10 @@ class FullReportContent extends React.Component { } supplierData = supData.map(replica => ( - + Replica Root @@ -1002,6 +1158,29 @@ ReportCredentialsModal.defaultProps = { newEntry: false, }; +ReportConnectionModal.propTypes = { + showModal: PropTypes.bool, + closeHandler: PropTypes.func, + handleFieldChange: PropTypes.func, + name: PropTypes.string, + hostname: PropTypes.string, + port: PropTypes.number, + binddn: PropTypes.string, + bindpw: PropTypes.string, + pwInputInterractive: PropTypes.bool, + addConn: PropTypes.func, +}; + +ReportConnectionModal.defaultProps = { + showModal: false, + name: "", + hostname: "", + port: 636, + binddn: "", + bindpw: "", + pwInputInterractive: false, +}; + ReportAliasesModal.propTypes = { showModal: PropTypes.bool, closeHandler: PropTypes.func, @@ -1058,6 +1237,7 @@ export { AgmtDetailsModal, ConflictCompareModal, ReportCredentialsModal, + ReportConnectionModal, 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 81ab4af305..48629a96ab 100644 --- a/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx +++ b/src/cockpit/389-console/src/lib/monitor/monitorTables.jsx @@ -2005,7 +2005,173 @@ class ReportConsumersTable extends React.Component { ); } } + +class ReplDSRCTable extends React.Component { + constructor(props) { + super(props); + + this.state = { + sortBy: {}, + columns: [ + { title: 'Name', transforms: [sortable] }, + { title: 'Connection Data', transforms: [sortable] }, + { title: 'Bind DN', transforms: [sortable] }, + { title: 'Password', transforms: [sortable] }, + { title: ''}, + ], + rows: [], + }; + this.onSort = this.onSort.bind(this); + } + + componentDidMount() { + let columns = [...this.state.columns]; + let rows = []; + + for (const conn of this.props.rows) { + let cred = conn[4]; + if (conn[4] === "*") { + const desc = Prompt; + cred = { title: desc }; + } else if (!conn[4].startsWith("[")) { + cred = "**********"; + } + rows.push({ + cells: [ + conn[0], conn[1] + ":" + conn[2], conn[3], cred, { props: { textCenter: true }, title: this.props.getDeleteButton(conn[0]) } + ] + }); + } + if (this.props.rows.length == 0) { + rows = [{ cells: ['There is no saved replication monitor connections'] }]; + columns = [{ title: 'Replication Connections' }]; + } + this.setState({ + rows: rows, + columns: columns + }); + } + + onSort(_event, index, direction) { + let rows = [...this.state.rows]; + const sortedRows = rows.sort((a, b) => (a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)); + this.setState({ + sortBy: { + index, + direction + }, + rows: direction === SortByDirection.asc ? sortedRows : sortedRows.reverse() + }); + } + + render() { + const { columns, rows, sortBy } = this.state; + + return ( +
+ + + +
+
+ ); + } +} + +class ReplDSRCAliasTable extends React.Component { + constructor(props) { + super(props); + + this.state = { + sortBy: {}, + columns: [ + { title: 'Alias', transforms: [sortable] }, + { title: 'Connection Data', transforms: [sortable] }, + { title: ''} + ], + rows: [], + }; + this.onSort = this.onSort.bind(this); + } + + componentDidMount() { + let columns = [...this.state.columns]; + let rows = []; + + for (const alias of this.props.rows) { + rows.push({ + cells: [ + alias[0], alias[1] + ":" + alias[2], { props: { textCenter: true }, title: this.props.getDeleteButton(alias[0]) } + ] + }); + } + if (this.props.rows.length == 0) { + rows = [{ cells: ['There are no saved replication monitor aliases'] }]; + columns = [{ title: 'Replication Monitoring Aliases' }]; + } + this.setState({ + rows: rows, + columns: columns + }); + } + + onSort(_event, index, direction) { + let rows = [...this.state.rows]; + const sortedRows = rows.sort((a, b) => (a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)); + this.setState({ + sortBy: { + index, + direction + }, + rows: direction === SortByDirection.asc ? sortedRows : sortedRows.reverse() + }); + } + + render() { + const { columns, rows, sortBy } = this.state; + + return ( +
+ + + +
+
+ ); + } +} + + // Proptypes and defaults +ReplDSRCAliasTable.defaultProps = { + rows: PropTypes.array +}; + +ReplDSRCAliasTable.defaultProps = { + rows: [] +}; + +ReplDSRCTable.defaultProps = { + rows: PropTypes.array +}; + +ReplDSRCTable.defaultProps = { + rows: [] +}; AgmtTable.propTypes = { agmts: PropTypes.array, @@ -2124,5 +2290,7 @@ export { ReportAliasesTable, ReportConsumersTable, ReportSingleTable, - DiskTable + DiskTable, + ReplDSRCTable, + ReplDSRCAliasTable, }; diff --git a/src/cockpit/389-console/src/lib/monitor/replMonAgmts.jsx b/src/cockpit/389-console/src/lib/monitor/replMonAgmts.jsx new file mode 100644 index 0000000000..689b68094b --- /dev/null +++ b/src/cockpit/389-console/src/lib/monitor/replMonAgmts.jsx @@ -0,0 +1,96 @@ +import React from "react"; +import cockpit from "cockpit"; +import { log_cmd } from "../tools.jsx"; +import PropTypes from "prop-types"; +import { + AgmtTable, +} from "./monitorTables.jsx"; + +import { + Text, + TextContent, + TextVariants, +} from "@patternfly/react-core"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSyncAlt } from '@fortawesome/free-solid-svg-icons'; + +export class ReplAgmtMonitor extends React.Component { + constructor (props) { + super(props); + + this.state = {}; + + this.pokeAgmt = this.pokeAgmt.bind(this); + } + + componentDidMount() { + this.props.enableTree(); + } + + pokeAgmt (evt) { + const agmt_name = evt.target.id; + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-agmt", "poke", "--suffix=" + this.props.suffix, agmt_name]; + log_cmd("pokeAgmt", "Awaken the agreement", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + this.props.addNotification( + "success", + `Replication agreement has been poked` + ); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to poke replication agreement ${agmt_name} - ${errMsg.desc}` + ); + }); + } + + render () { + const replAgmts = this.props.data.replAgmts; + + return ( +
+
+ + + Monitor Replication Agreements + + + +
+ +
+ ); + } +} + +// Props and defaultProps + +ReplAgmtMonitor.propTypes = { + data: PropTypes.object, + suffix: PropTypes.string, + serverId: PropTypes.string, + addNotification: PropTypes.func, + enableTree: PropTypes.func, +}; + +ReplAgmtMonitor.defaultProps = { + data: {}, + suffix: "", + serverId: "", +}; + +export default ReplAgmtMonitor; diff --git a/src/cockpit/389-console/src/lib/monitor/replMonConflict.jsx b/src/cockpit/389-console/src/lib/monitor/replMonConflict.jsx new file mode 100644 index 0000000000..4c8dc5574e --- /dev/null +++ b/src/cockpit/389-console/src/lib/monitor/replMonConflict.jsx @@ -0,0 +1,559 @@ +import React from "react"; +import cockpit from "cockpit"; +import { log_cmd } from "../tools.jsx"; +import PropTypes from "prop-types"; +import { + ConflictTable, + GlueTable, +} from "./monitorTables.jsx"; +import { + TaskLogModal, + ConflictCompareModal, +} from "./monitorModals.jsx"; +import { + Tab, + Tabs, + TabTitleText, + Text, + TextContent, + TextVariants, + Tooltip, +} from "@patternfly/react-core"; +import { DoubleConfirmModal } from "../notifications.jsx"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSyncAlt } from '@fortawesome/free-solid-svg-icons'; + +export class ReplMonConflict extends React.Component { + constructor (props) { + super(props); + + this.state = { + showConfirmConvertConflict: false, + showConfirmSwapConflict: false, + showConfirmDeleteConflict: false, + showCompareModal: false, + showConfirmDeleteGlue: false, + showConfirmConvertGlue: false, + showConfirmConvertGlue: false, + swapConflictRadio: false, + deleteConflictRadio: false, + convertConflictRadio: false, + convertRDN: "", + glueEntry: "", + conflictEntry: "", + + modalChecked: false, + modalSpinning: false, + activeTabConflictKey: 0, + }; + + this.handleNavConflictSelect = (event, tabIndex) => { + this.setState({ + activeTabConflictKey: tabIndex + }); + }; + + 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.handleRadioChange = this.handleRadioChange.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleConflictConversion = this.handleConflictConversion.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); + } + + componentDidMount() { + this.props.enableTree(); + } + + handleRadioChange(value, evt) { + // Handle the radio button changes + const radioID = { + swapConflictRadio: false, + deleteConflictRadio: false, + convertConflictRadio: false, + }; + + radioID[evt.target.id] = value; + this.setState({ + swapConflictRadio: radioID.swapConflictRadio, + deleteConflictRadio: radioID.deleteConflictRadio, + convertConflictRadio: radioID.convertConflictRadio, + }); + } + + handleChange(value, evt) { + // PF 4 version + if (evt.target.type === 'number') { + if (value) { + value = parseInt(value); + } else { + value = 1; + } + } + this.setState({ + [evt.target.id]: value + }); + } + + convertConflict () { + this.setState({ modalSpinning: true }); + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-conflict", "convert", this.state.conflictEntry, "--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, + }); + this.closeConfirmConvertConflict(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to convert conflict entry entry: ${this.state.conflictEntry} - ${errMsg.desc}` + ); + this.closeConfirmConvertConflict(); + }); + } + + swapConflict () { + this.setState({ modalSpinning: true }); + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-conflict", "swap", this.state.conflictEntry]; + 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, + }); + this.closeConfirmSwapConflict(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to swap in conflict entry: ${this.state.conflictEntry} - ${errMsg.desc}` + ); + this.closeConfirmSwapConflict(); + }); + } + + deleteConflict () { + this.setState({ modalSpinning: true }); + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-conflict", "delete", this.state.conflictEntry]; + + 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, + }); + this.closeConfirmConvertConflict(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to delete conflict entry: ${this.state.conflictEntry} - ${errMsg.desc}` + ); + this.closeConfirmDeleteConflict(); + }); + } + + resolveConflict (dn) { + const 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 => { + const entries = JSON.parse(content); + this.setState({ + cmpConflictEntry: entries.items[0], + cmpValidEntry: entries.items[1], + showCompareModal: true, + deleteConflictRadio: true, + swapConflictRadio: false, + convertConflictRadio: false, + convertRDN: "", + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to get conflict entries: ${dn} - ${errMsg.desc}` + ); + }); + } + + confirmConvertGlue (dn) { + this.setState({ + showConfirmConvertGlue: true, + glueEntry: dn, + modalChecked: false, + modalSpinning: false, + }); + } + + closeConfirmConvertGlue () { + this.setState({ + showConfirmConvertGlue: false, + glueEntry: "", + modalChecked: false, + modalSpinning: false, + }); + } + + convertGlue () { + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-conflict", "convert-glue", this.state.glueEntry]; + 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` + ); + this.closeConfirmConvertGlue(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to convert glue entry: ${this.state.glueEntry} - ${errMsg.desc}` + ); + this.closeConfirmConvertGlue(); + }); + } + + confirmDeleteGlue (dn) { + this.setState({ + showConfirmDeleteGlue: true, + glueEntry: dn, + modalChecked: false, + modalSpinning: false, + }); + } + + deleteGlue () { + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-conflict", "delete-glue", this.state.glueEntry]; + 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` + ); + this.closeConfirmDeleteGlue(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to delete glue entry: ${this.state.glueEntry} - ${errMsg.desc}` + ); + this.closeConfirmDeleteGlue(); + }); + } + + closeConfirmDeleteGlue () { + this.setState({ + showConfirmDeleteGlue: false, + glueEntry: "", + modalChecked: false, + modalSpinning: false, + }); + } + + handleConflictConversion (dn) { + // Follow the radio button and perform the conflict resolution + if (this.state.deleteConflictRadio) { + this.confirmDeleteConflict(dn); + } else if (this.state.swapConflictRadio) { + this.confirmSwapConflict(dn); + } else { + this.confirmConvertConflict(dn); + } + } + + 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, + modalChecked: false, + modalSpinning: false, + }); + } + + closeConfirmConvertConflict () { + this.setState({ + showConfirmConvertConflict: false, + conflictEntry: "", + modalChecked: false, + modalSpinning: false, + convertRDN: "", + }); + } + + confirmSwapConflict (dn) { + this.setState({ + showConfirmSwapConflict: true, + conflictEntry: dn, + modalChecked: false, + modalSpinning: false, + }); + } + + closeConfirmSwapConflict () { + this.setState({ + showConfirmSwapConflict: false, + conflictEntry: "", + modalChecked: false, + modalSpinning: false, + }); + } + + confirmDeleteConflict (dn) { + this.setState({ + showConfirmDeleteConflict: true, + conflictEntry: dn, + modalChecked: false, + modalSpinning: false, + }); + } + + closeConfirmDeleteConflict () { + this.setState({ + showConfirmDeleteConflict: false, + conflictEntry: "", + modalChecked: false, + modalSpinning: false, + }); + } + + closeCompareModal () { + this.setState({ + showCompareModal: false, + modalChecked: false, + modalSpinning: false, + }); + } + + + render () { + const conflictEntries = this.props.data.conflicts; + const glueEntries = this.props.data.glues; + + return ( +
+
+ + + Monitor Conflict and Glue Entries + + + +
+ + Conflict Entries ({conflictEntries.length})}> +
+ + Replication conflict entries occur when two entries are created with the + same DN (or name) on different servers at about the same time. The automatic conflict + resolution procedure renames the entry created last. Its RDN is changed + into a multi-valued RDN that includes the entry's original RDN and it's unique + identifier (nsUniqueId). There are several ways to resolve a conflict, + but choosing which option to use is up to you. +
+ } + > + What Is A Replication Conflict Entry? + + +
+ + Glue Entries ({glueEntries.length})}> +
+ + When a Delete operation is replicated and the consumer server finds that the entry to be + deleted has child entries, the conflict resolution procedure creates a "glue entry" to + avoid having orphaned entries in the database. In the same way, when an Add operation is + replicated and the consumer server cannot find the parent entry, the conflict resolution + procedure creates a "glue entry", representing the "parent entry", so that the new entry is + not an orphaned entry. You can choose to convert the glue entry, or remove the glue entry and + all its child entries. +
+ } + > + What Is A Replication Glue Entry? + + + +
+ + + + + + + + + ); + } +} + +// Props and defaultProps + +ReplMonConflict.propTypes = { + data: PropTypes.object, + suffix: PropTypes.string, + serverId: PropTypes.string, + addNotification: PropTypes.func, + enableTree: PropTypes.func, +}; + +ReplMonConflict.defaultProps = { + data: {}, + suffix: "", + serverId: "", +}; + +export default ReplMonConflict; diff --git a/src/cockpit/389-console/src/lib/monitor/replMonTasks.jsx b/src/cockpit/389-console/src/lib/monitor/replMonTasks.jsx new file mode 100644 index 0000000000..1a94b431f1 --- /dev/null +++ b/src/cockpit/389-console/src/lib/monitor/replMonTasks.jsx @@ -0,0 +1,146 @@ +import React from "react"; +import cockpit from "cockpit"; +import { log_cmd } from "../tools.jsx"; +import PropTypes from "prop-types"; +import { + CleanALLRUVTable, + AbortCleanALLRUVTable, +} from "./monitorTables.jsx"; +import { + TaskLogModal, +} from "./monitorModals.jsx"; +import { + Tab, + Tabs, + TabTitleText, + Text, + TextContent, + TextVariants, +} from "@patternfly/react-core"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSyncAlt } from '@fortawesome/free-solid-svg-icons'; + +export class ReplMonTasks extends React.Component { + constructor (props) { + super(props); + + this.state = { + showLogModal: false, + logData: "", + activeTabTaskKey: 0, + }; + + this.handleNavTaskSelect = (event, tabIndex) => { + this.setState({ + activeTabTaskKey: tabIndex + }); + }; + + this.viewCleanLog = this.viewCleanLog.bind(this); + this.viewAbortLog = this.viewAbortLog.bind(this); + this.closeLogModal = this.closeLogModal.bind(this); + } + + componentDidMount() { + this.props.enableTree(); + } + + viewCleanLog (name) { + let logData = ""; + for (const task of this.props.data.cleanTasks) { + if (task.attrs.cn[0] == name) { + logData = task.attrs.nstasklog[0]; + break; + } + } + this.setState({ + showLogModal: true, + logData: logData + }); + } + + viewAbortLog (name) { + let logData = ""; + for (const task of this.props.data.abortTasks) { + if (task.attrs.cn[0] == name) { + logData = task.attrs.nstasklog[0]; + break; + } + } + this.setState({ + showLogModal: true, + logData: logData + }); + } + + closeLogModal() { + this.setState({ + showLogModal: false + }); + } + + render () { + const cleanTasks = this.props.data.cleanTasks; + const abortTasks = this.props.data.abortTasks; + + return ( +
+
+ + + Monitor Replication Tasks + + + +
+ + CleanAllRUV Tasks ({cleanTasks.length})}> +
+ +
+
+ Abort CleanAllRUV Tasks ({abortTasks.length})}> +
+ +
+
+
+ +
+ ); + } +} + +// Props and defaultProps + +ReplMonTasks.propTypes = { + data: PropTypes.object, + suffix: PropTypes.string, + serverId: PropTypes.string, + addNotification: PropTypes.func, + enableTree: PropTypes.func, +}; + +ReplMonTasks.defaultProps = { + data: {}, + suffix: "", + serverId: "", +}; + +export default ReplMonTasks; diff --git a/src/cockpit/389-console/src/lib/monitor/replMonWinsync.jsx b/src/cockpit/389-console/src/lib/monitor/replMonWinsync.jsx new file mode 100644 index 0000000000..25f69bec85 --- /dev/null +++ b/src/cockpit/389-console/src/lib/monitor/replMonWinsync.jsx @@ -0,0 +1,95 @@ +import React from "react"; +import cockpit from "cockpit"; +import { log_cmd } from "../tools.jsx"; +import PropTypes from "prop-types"; +import { + WinsyncAgmtTable, +} from "./monitorTables.jsx"; +import { + Text, + TextContent, + TextVariants, +} from "@patternfly/react-core"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSyncAlt } from '@fortawesome/free-solid-svg-icons'; + +export class ReplAgmtWinsync extends React.Component { + constructor (props) { + super(props); + + this.state = {}; + + this.pokeAgmt = this.pokeAgmt.bind(this); + } + + componentDidMount() { + this.props.enableTree(); + } + + pokeAgmt (evt) { + const agmt_name = evt.target.id; + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "repl-winsync-agmt", "poke", "--suffix=" + this.props.suffix, agmt_name]; + log_cmd("pokeAgmt", "Awaken the agreement", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + this.props.addNotification( + "success", + `Replication Winysnc agreement has been poked` + ); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to poke replication winsync agreement ${agmt_name} - ${errMsg.desc}` + ); + }); + } + + render () { + const replWinsyncAgmts = this.props.data.replWinsyncAgmts; + + return ( +
+
+ + + Monitor Winsync Agreements + + + +
+ +
+ ); + } +} + +// Props and defaultProps + +ReplAgmtWinsync.propTypes = { + data: PropTypes.object, + suffix: PropTypes.string, + serverId: PropTypes.string, + addNotification: PropTypes.func, + enableTree: PropTypes.func, +}; + +ReplAgmtWinsync.defaultProps = { + data: {}, + suffix: "", + serverId: "", +}; + +export default ReplAgmtWinsync; diff --git a/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx b/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx index 121a6c7617..c0cf445947 100644 --- a/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx +++ b/src/cockpit/389-console/src/lib/monitor/replMonitor.jsx @@ -2,40 +2,38 @@ import React from "react"; import cockpit from "cockpit"; import { log_cmd } from "../tools.jsx"; import PropTypes from "prop-types"; -import { DoubleConfirmModal } from "../notifications.jsx"; import { ReportCredentialsTable, ReportAliasesTable, - AgmtTable, - WinsyncAgmtTable, - CleanALLRUVTable, - AbortCleanALLRUVTable, - ConflictTable, - GlueTable, + ReplDSRCTable, + ReplDSRCAliasTable, } from "./monitorTables.jsx"; import { FullReportContent, ReportLoginModal, ReportCredentialsModal, + ReportConnectionModal, ReportAliasesModal, - TaskLogModal, AgmtDetailsModal, - ConflictCompareModal, } from "./monitorModals.jsx"; import { Button, ExpandableSection, + Spinner, Tab, Tabs, TabTitleText, Text, TextContent, TextVariants, - Tooltip, } from "@patternfly/react-core"; import { SortByDirection, } from '@patternfly/react-table'; +import TrashAltIcon from '@patternfly/react-icons/dist/js/icons/trash-alt-icon'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSyncAlt } from '@fortawesome/free-solid-svg-icons'; +import { DoubleConfirmModal } from "../notifications.jsx"; const _ = cockpit.gettext; @@ -43,10 +41,8 @@ export class ReplMonitor extends React.Component { constructor (props) { super(props); this.state = { - activeTabKey: 0, - activeTabReplKey: 0, - activeTabTaskKey: 0, - activeTabConflictKey: 0, + activeKey: 0, + activeConfigKey: 0, logData: "", showBindModal: false, showLogModal: false, @@ -54,12 +50,8 @@ export class ReplMonitor extends React.Component { showReportLoginModal: false, showCredentialsModal: false, showAliasesModal: false, - showCompareModal: false, - showConfirmDeleteGlue: false, - showConfirmConvertGlue: false, - showConfirmSwapConflict: false, - showConfirmConvertConflict: false, - showConfirmDeleteConflict: false, + loadingDSRC: false, + modalSpinning: false, modalChecked: false, lagAgmts: [], @@ -67,9 +59,6 @@ export class ReplMonitor extends React.Component { aliasData: [], reportData: [], agmt: "", - convertRDN: "", - glueEntry: "", - conflictEntry: "", binddn: "cn=Directory Manager", bindpw: "", errObj: {}, @@ -83,6 +72,16 @@ export class ReplMonitor extends React.Component { doFullReportCleanup: false, reportRefreshing: false, reportLoading: false, + // dsrc + credRows: [...this.props.credRows], + aliasRows: [...this.props.aliasRows], + showAddDSRCCredModal: false, + showAddDSRCAliasModal: false, + showConfirmDeleteDSRCCred: false, + showConfirmDeleteDSRCAlias: false, + showConfirmOverwriteDSRC: false, + deleteConn: "", + deleteAlias: "", credsInstanceName: "", disableBinddn: false, @@ -105,10 +104,6 @@ export class ReplMonitor extends React.Component { credSortBy: {}, aliasesList: [], aliasSortBy: {}, - - deleteConflictRadio: true, - swapConflictRadio: false, - convertConflictRadio: false, }; this.onToggle = (isExpanded) => { @@ -117,42 +112,12 @@ export class ReplMonitor extends React.Component { }); }; - // Toggle currently active tab - this.handleNavSelect = (event, tabIndex) => { - this.setState({ - activeTabKey: tabIndex - }); - }; - // Toggle currently active tab - this.handleNavTaskSelect = (event, tabIndex) => { - this.setState({ - activeTabTaskKey: tabIndex - }); - }; - // Toggle currently active tab - this.handleNavConflictSelect = (event, tabIndex) => { - this.setState({ - activeTabConflictKey: tabIndex - }); - }; - // Toggle currently active tab - this.handleNavReplSelect = (event, tabIndex) => { - this.setState({ - activeTabReplKey: tabIndex - }); - }; - this.maxValue = 65534; this.handleFieldChange = this.handleFieldChange.bind(this); - this.handleReportNavSelect = this.handleReportNavSelect.bind(this); - this.pokeAgmt = this.pokeAgmt.bind(this); - this.pokeWinsyncAgmt = this.pokeWinsyncAgmt.bind(this); + this.handleNavConfigSelect = this.handleNavConfigSelect.bind(this); this.showAgmtModalRemote = this.showAgmtModalRemote.bind(this); this.closeAgmtModal = this.closeAgmtModal.bind(this); - this.viewCleanLog = this.viewCleanLog.bind(this); - this.viewAbortLog = this.viewAbortLog.bind(this); - this.closeLogModal = this.closeLogModal.bind(this); this.closeReportLoginModal = this.closeReportLoginModal.bind(this); // Replication report functions @@ -164,7 +129,7 @@ export class ReplMonitor extends React.Component { this.showEditCredsModal = this.showEditCredsModal.bind(this); this.closeCredsModal = this.closeCredsModal.bind(this); this.onCredSort = this.onCredSort.bind(this); - + this.handleNavSelect = this.handleNavSelect.bind(this); this.addAliases = this.addAliases.bind(this); this.editAliases = this.editAliases.bind(this); this.removeAliases = this.removeAliases.bind(this); @@ -179,30 +144,29 @@ export class ReplMonitor extends React.Component { 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); - 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.handleRadioChange = this.handleRadioChange.bind(this); - this.handleChange = this.handleChange.bind(this); - this.handleConflictConversion = this.handleConflictConversion.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); this.onMinusConfig = this.onMinusConfig.bind(this); this.onConfigChange = this.onConfigChange.bind(this); this.onPlusConfig = this.onPlusConfig.bind(this); + + // dsrc + this.loadDSRC = this.loadDSRC.bind(this); + this.showAddDSRCCred = this.showAddDSRCCred.bind(this); + this.showAddDSRCAlias = this.showAddDSRCAlias.bind(this); + this.closeAddDSRCCred = this.closeAddDSRCCred.bind(this); + this.closeAddDSRCAlias = this.closeAddDSRCAlias.bind(this); + this.addDSRCCred = this.addDSRCCred.bind(this); + this.addDSRCAlias = this.addDSRCAlias.bind(this); + this.showConfirmDeleteDSRCCred = this.showConfirmDeleteDSRCCred.bind(this); + this.showConfirmDeleteDSRCAlias = this.showConfirmDeleteDSRCAlias.bind(this); + this.showConfirmOverwriteDSRC = this.showConfirmOverwriteDSRC.bind(this); + this.closeConfirmDeleteDSRCCred = this.closeConfirmDeleteDSRCCred.bind(this); + this.closeConfirmDeleteDSRCAlias = this.closeConfirmDeleteDSRCAlias.bind(this); + this.closeConfirmOverwriteDSRC = this.closeConfirmOverwriteDSRC.bind(this); + this.deleteDSRCCred = this.deleteDSRCCred.bind(this); + this.deleteDSRCAlias = this.deleteDSRCAlias.bind(this); + this.getAliasDeleteButton = this.getAliasDeleteButton.bind(this); + this.getCredDeleteButton = this.getCredDeleteButton.bind(this); + this.overwriteDSRC = this.overwriteDSRC.bind(this); } componentDidUpdate(prevProps, prevState) { @@ -231,6 +195,55 @@ export class ReplMonitor extends React.Component { if (this.timer) window.clearTimeout(this.timer); } + loadDSRC() { + // Load dsrc replication report configuration + this.setState({ + loadingDSRC: true, + }); + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "display"]; + log_cmd("loadDSRC", "Check for replication monitor configurations in the .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(dsrc_content => { + const content = JSON.parse(dsrc_content); + let credRows = []; + let aliasRows = []; + if ("repl-monitor-connections" in content) { + const report_config = content["repl-monitor-connections"]; + for (const [connection, value] of Object.entries(report_config)) { + const conn = connection + ":" + value; + credRows.push(conn.split(':')); + // [repl-monitor-connections] + // connection1 = server1.example.com:389:cn=Directory Manager:* + } + } + if ("repl-monitor-aliases" in content) { + const report_config = content["repl-monitor-aliases"]; + for (const [alias_name, value] of Object.entries(report_config)) { + const alias = alias_name + ":" + value; + aliasRows.push(alias.split(':')); + // [repl-monitor-aliases] + // M1 = server1.example.com:38901 + } + } + this.setState({ + credRows, + aliasRows, + loadingDSRC: false, + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to get .dsrc information: ${errMsg.desc}` + ); + this.setState({ + loadingDSRC: false, + }); + }); + } + componentDidMount() { if (this.state.initCreds) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", @@ -251,19 +264,21 @@ export class ReplMonitor extends React.Component { } ] })); - for (const agmt of this.props.data.replAgmts) { - this.setState(prevState => ({ - credentialsList: [ - ...prevState.credentialsList, - { - connData: `${agmt.replica}`, - credsBinddn: config.attrs["nsslapd-rootdn"][0], - credsBindpw: "", - pwInputInterractive: true - } - ], - initCreds: false - })); + if ('replAgmts' in this.props.data) { + for (const agmt of this.props.data.replAgmts) { + this.setState(prevState => ({ + credentialsList: [ + ...prevState.credentialsList, + { + connData: `${agmt.replica}`, + credsBinddn: config.attrs["nsslapd-rootdn"][0], + credsBindpw: "", + pwInputInterractive: true + } + ], + initCreds: false + })); + } } }) .fail(err => { @@ -296,22 +311,6 @@ export class ReplMonitor extends React.Component { }); } - handleRadioChange(value, evt) { - // Handle the radio button changes - const radioID = { - swapConflictRadio: false, - deleteConflictRadio: false, - convertConflictRadio: false, - }; - - radioID[evt.target.id] = value; - this.setState({ - swapConflictRadio: radioID.swapConflictRadio, - deleteConflictRadio: radioID.deleteConflictRadio, - convertConflictRadio: radioID.convertConflictRadio, - }); - } - handleChange(value, evt) { // PF 4 version if (evt.target.type === 'number') { @@ -341,293 +340,44 @@ export class ReplMonitor extends React.Component { }); } - convertConflict () { - this.setState({ modalSpinning: true }); - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "convert", this.state.conflictEntry, "--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, - }); - this.closeConfirmConvertConflict(); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to convert conflict entry entry: ${this.state.conflictEntry} - ${errMsg.desc}` - ); - this.closeConfirmConvertConflict(); - }); - } - - swapConflict () { - this.setState({ modalSpinning: true }); - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "swap", this.state.conflictEntry]; - 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, - }); - this.closeConfirmSwapConflict(); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to swap in conflict entry: ${this.state.conflictEntry} - ${errMsg.desc}` - ); - this.closeConfirmSwapConflict(); - }); - } - - deleteConflict () { - this.setState({ modalSpinning: true }); - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "delete", this.state.conflictEntry]; - - 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, - }); - this.closeConfirmConvertConflict(); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to delete conflict entry: ${this.state.conflictEntry} - ${errMsg.desc}` - ); - this.closeConfirmDeleteConflict(); - }); - } - - resolveConflict (dn) { - const 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 => { - const entries = JSON.parse(content); - this.setState({ - cmpConflictEntry: entries.items[0], - cmpValidEntry: entries.items[1], - showCompareModal: true, - deleteConflictRadio: true, - swapConflictRadio: false, - convertConflictRadio: false, - convertRDN: "", - }); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to get conflict entries: ${dn} - ${errMsg.desc}` - ); - }); - } - - confirmConvertGlue (dn) { - this.setState({ - showConfirmConvertGlue: true, - glueEntry: dn, - modalChecked: false, - modalSpinning: false, - }); - } - - closeConfirmConvertGlue () { - this.setState({ - showConfirmConvertGlue: false, - glueEntry: "", - modalChecked: false, - modalSpinning: false, - }); - } - - convertGlue () { - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "convert-glue", this.state.glueEntry]; - 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` - ); - this.closeConfirmConvertGlue(); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to convert glue entry: ${this.state.glueEntry} - ${errMsg.desc}` - ); - this.closeConfirmConvertGlue(); - }); - } - - confirmDeleteGlue (dn) { - this.setState({ - showConfirmDeleteGlue: true, - glueEntry: dn, - modalChecked: false, - modalSpinning: false, - }); - } - - deleteGlue () { - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "delete-glue", this.state.glueEntry]; - 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` - ); - this.closeConfirmDeleteGlue(); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to delete glue entry: ${this.state.glueEntry} - ${errMsg.desc}` - ); - this.closeConfirmDeleteGlue(); - }); - } - - closeConfirmDeleteGlue () { - this.setState({ - showConfirmDeleteGlue: false, - glueEntry: "", - modalChecked: false, - modalSpinning: false, - }); - } - - handleConflictConversion (dn) { - // Follow the radio button and perform the conflict resolution - if (this.state.deleteConflictRadio) { - this.confirmDeleteConflict(dn); - } else if (this.state.swapConflictRadio) { - this.confirmSwapConflict(dn); - } else { - this.confirmConvertConflict(dn); - } - } - - 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, - modalChecked: false, - modalSpinning: false, - }); - } - - closeConfirmConvertConflict () { - this.setState({ - showConfirmConvertConflict: false, - conflictEntry: "", - modalChecked: false, - modalSpinning: false, - convertRDN: "", - }); - } - - confirmSwapConflict (dn) { - this.setState({ - showConfirmSwapConflict: true, - conflictEntry: dn, - modalChecked: false, - modalSpinning: false, - }); - } - - closeConfirmSwapConflict () { + handleNavSelect(event, tabIndex) { this.setState({ - showConfirmSwapConflict: false, - conflictEntry: "", - modalChecked: false, - modalSpinning: false, - }); - } - - confirmDeleteConflict (dn) { - this.setState({ - showConfirmDeleteConflict: true, - conflictEntry: dn, - modalChecked: false, - modalSpinning: false, + activeKey: tabIndex }); } - closeConfirmDeleteConflict () { + handleNavConfigSelect(event, tabIndex) { this.setState({ - showConfirmDeleteConflict: false, - conflictEntry: "", - modalChecked: false, - modalSpinning: false, - }); - } - - closeCompareModal () { - this.setState({ - showCompareModal: false, - modalChecked: false, - modalSpinning: false, + activeConfigKey: tabIndex }); } - handleNavSelect(key) { - this.setState({ - activeKey: key - }); + getCredDeleteButton(name) { + return ( + + { + this.showConfirmDeleteDSRCCred(name); + }} + title="Delete replica connection" + /> + + ); } - handleReportNavSelect(key) { - this.setState({ - activeReportKey: key - }); + getAliasDeleteButton(name) { + return ( + + { + this.showConfirmDeleteDSRCAlias(name); + }} + title="Delete replica alias" + /> + + ); } closeLogModal() { @@ -636,77 +386,6 @@ export class ReplMonitor extends React.Component { }); } - viewCleanLog (name) { - let logData = ""; - for (const task of this.props.data.cleanTasks) { - if (task.attrs.cn[0] == name) { - logData = task.attrs.nstasklog[0]; - break; - } - } - this.setState({ - showLogModal: true, - logData: logData - }); - } - - viewAbortLog (name) { - let logData = ""; - for (const task of this.props.data.abortTasks) { - if (task.attrs.cn[0] == name) { - logData = task.attrs.nstasklog[0]; - break; - } - } - this.setState({ - showLogModal: true, - logData: logData - }); - } - - pokeAgmt (evt) { - const agmt_name = evt.target.id; - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-agmt", "poke", "--suffix=" + this.props.suffix, agmt_name]; - log_cmd("pokeAgmt", "Awaken the agreement", cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.addNotification( - "success", - `Replication agreement has been poked` - ); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to poke replication agreement ${agmt_name} - ${errMsg.desc}` - ); - }); - } - - pokeWinsyncAgmt(name) { - const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-winsync-agmt", "poke", "--suffix=" + this.props.suffix, name]; - log_cmd("pokeAgmt", "Awaken the agreement", cmd); - cockpit - .spawn(cmd, { superuser: true, err: "message" }) - .done(content => { - this.props.addNotification( - "success", - `Replication winsync agreement has been poked` - ); - }) - .fail(err => { - const errMsg = JSON.parse(err); - this.props.addNotification( - "error", - `Failed to poke replication winsync agreement ${name} - ${errMsg.desc}` - ); - }); - } - showAgmtModalRemote (supplierName, replicaName, agmtName) { if (!agmtName) { this.props.addNotification( @@ -983,6 +662,135 @@ export class ReplMonitor extends React.Component { }); } + showConfirmOverwriteDSRC() { + this.setState({ + showConfirmOverwriteDSRC: true + }); + } + + closeConfirmOverwriteDSRC() { + this.setState({ + showConfirmOverwriteDSRC: false + }); + } + + overwriteDSRC () { + // Get current DSRC Settings + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "display"]; + log_cmd("overwriteDSRC", "gather conns and aliases from .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(dsrc_content => { + const content = JSON.parse(dsrc_content); + let dsrcCreds = []; + let dsrcAliases = []; + let deleteCmd = ["dsctl", "-j", this.props.serverId, "dsrc", "repl-mon"]; + let addCmd = ["dsctl", "-j", this.props.serverId, "dsrc", "repl-mon"]; + + // Gather the names of the replica connections and aliases + if ("repl-monitor-connections" in content) { + const report_config = content["repl-monitor-connections"]; + for (const [cred, value] of Object.entries(report_config)) { + dsrcCreds.push(cred); + } + } + if ("repl-monitor-aliases" in content) { + const report_config = content["repl-monitor-aliases"]; + for (const [alias, value] of Object.entries(report_config)) { + dsrcAliases.push(alias); + } + } + + // Remove existing replica connections and aliases + if (this.state.credentialsList.length > 0 && dsrcCreds.length > 0) { + // Ok we have new replica connections, remove the old ones + deleteCmd.push("--del-conn"); + for (const repl of dsrcCreds) { + deleteCmd.push(repl); + } + } + if (this.state.aliasesList.length > 0 && dsrcAliases.length > 0) { + // Ok we have new aliases, remove the old ones + deleteCmd.push("--del-alias"); + for (const alias of dsrcAliases) { + deleteCmd.push(alias); + } + } + if (deleteCmd.length === 5) { + deleteCmd == "echo"; // do nothing + } + + // Write new replica connections and aliases + if (this.state.credentialsList.length > 0) { + addCmd.push("--add-conn"); + for (const rowIdx in this.state.credentialsList) { + const row = this.state.credentialsList[rowIdx]; + let password = row.credsBindpw; + if (row.pwInputInterractive) { + password = "*"; + } + const idx = parseInt(rowIdx) + 1; + const cred = `replica_${idx}:${row.connData}:${row.credsBinddn}:${password}`; + addCmd.push(cred); + } + } + if (this.state.aliasesList.length > 0) { + addCmd.push("--add-alias"); + for (const row of this.state.aliasesList) { + const alias = `${row[0]}:${row[1]}`; + addCmd.push(alias); + } + } + log_cmd("overwriteDSRC", "delete conns and aliases in the .dsrc file", deleteCmd); + cockpit + .spawn(deleteCmd, { superuser: true, err: "message" }) + .done( () => { + log_cmd("overwriteDSRC", "add conns and aliases in the .dsrc file", addCmd); + cockpit + .spawn(addCmd, { superuser: true, err: "message" }) + .done( () => { + this.setState({ + showConfirmOverwriteDSRC: false + }, this.loadDSRC); + this.props.addNotification( + "success", + "Successfully saved monitor configuration to the .dsrc file" + ); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.setState({ + showConfirmOverwriteDSRC: false + }); + this.props.addNotification( + "error", + `Failed to delete from .dsrc file: ${errMsg.desc}` + ); + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to add to .dsrc content: ${errMsg.desc}` + ); + this.setState({ + showConfirmOverwriteDSRC: false + }); + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to get .dsrc content: ${errMsg.desc}` + ); + this.setState({ + showConfirmOverwriteDSRC: false + }); + }); + } + refreshFullReport() { this.doFullReport(); this.setState({ @@ -990,29 +798,40 @@ export class ReplMonitor extends React.Component { }); } - doFullReport() { + doFullReport(dsrc) { // Initiate the report and continue the processing in the input window this.setState({ reportLoading: true, - activeTabKey: 1 + activeKey: 2 }); let password = ""; const credentials = []; const printCredentials = []; - for (const row of this.state.credentialsList) { - if (row.pwInputInterractive) { - password = "*"; - } else { - password = `${row.credsBindpw}`; + const aliases = []; + if (dsrc) { + // Use the monitor info from .dsrc + for (const row of this.state.credRows) { + credentials.push(`${row[1]}:${row[2]}:${row[3]}:${row[4]}`); + printCredentials.push(`${row[1]}:${row[2]}:${row[3]}:********`); + } + for (const row of this.state.aliasRows) { + aliases.push(`${row[0]}=${row[1]}:${row[2]}`); + } + } else { + for (const 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}:********`); } - credentials.push(`${row.connData}:${row.credsBinddn}:${password}`); - printCredentials.push(`${row.connData}:${row.credsBinddn}:********`); - } - const aliases = []; - for (const row of this.state.aliasesList) { - aliases.push(`${row[0]}=${row[1]}`); + for (const row of this.state.aliasesList) { + aliases.push(`${row[0]}=${row[1]}`); + } } let buffer = ""; @@ -1091,6 +910,7 @@ export class ReplMonitor extends React.Component { } supplier_reparsed.push({ ...supplier, data: replica_reparsed }); } + const report_reparsed = { ...report, items: supplier_reparsed }; this.setState({ reportData: report_reparsed.items, @@ -1126,6 +946,7 @@ export class ReplMonitor extends React.Component { 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 (const creds of this.state.dynamicCredentialsList) { if (creds.credsInstanceName == this.state.credsInstanceName) { @@ -1133,7 +954,6 @@ export class ReplMonitor extends React.Component { 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({ @@ -1147,12 +967,13 @@ export class ReplMonitor extends React.Component { } // Check for password - } else if (last_line.startsWith("Enter a password") && last_line.endsWith(": ")) { + } else if ((last_line.startsWith("Enter a password") || last_line.startsWith("File ")) && 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 (const creds of this.state.dynamicCredentialsList) { if (creds.credsInstanceName == this.state.credsInstanceName) { found_creds = true; @@ -1169,7 +990,7 @@ export class ReplMonitor extends React.Component { bindpwRequired: true, credsInstanceName: this.state.credsInstanceName, disableBinddn: true, - loginBinddn: data.split("Enter a password for ")[1].split(" on")[0], + loginBinddn: data.split("nter a password for ")[1].split(" on")[0], loginBindpw: "" }); } @@ -1236,16 +1057,201 @@ export class ReplMonitor extends React.Component { }); } + // dsrc + showAddDSRCCred() { + // Add a connection to dsrc file + this.setState({ + showAddDSRCCredModal: true, + modalChecked: false, + modalSpinning: false, + connName: "", + connHostname: "", + connPort: 636, + connBindDN: "", + credsCred: "", + }); + } + + closeAddDSRCCred () { + this.setState({ + showAddDSRCCredModal: false, + }); + } + + showAddDSRCAlias() { + // Add alias to dsrc file + this.setState({ + showAddDSRCAliasModal: true, + modalChecked: false, + modalSpinning: false, + newEntry: true, + aliasName: "", + aliasPort: 636, + aliasHostname: "", + }); + } + + closeAddDSRCAlias () { + this.setState({ + showAddDSRCAliasModal: false, + }); + } + + showConfirmDeleteDSRCCred (name) { + this.setState({ + showConfirmDeleteDSRCCred: true, + connName: name, + modalChecked: false, + modalSpinning: false, + }); + } + + closeConfirmDeleteDSRCCred () { + this.setState({ + showConfirmDeleteDSRCCred: false, + }); + } + + showConfirmDeleteDSRCAlias (name) { + this.setState({ + showConfirmDeleteDSRCAlias: true, + aliasName: name, + modalChecked: false, + modalSpinning: false, + }); + } + + closeConfirmDeleteDSRCAlias () { + this.setState({ + showConfirmDeleteDSRCAlias: false, + }); + } + + deleteDSRCCred () { + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "repl-mon", "--del-conn=" + this.state.connName]; + + this.setState({ + loadingDSRC: true, + }); + + log_cmd("deleteDSRCCred", "Delete a replica connection from the .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(() => { + this.loadDSRC(); + this.setState({ + showConfirmDeleteDSRCCred: false, + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to update .dsrc information: ${errMsg.desc}` + ); + this.loadDSRC(); + }); + } + + deleteDSRCAlias () { + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "repl-mon", "--del-alias=" + this.state.aliasName]; + + this.setState({ + loadingDSRC: true, + }); + + log_cmd("deleteDSRCCred", "Delete a replication monitor alias from the .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(() => { + this.loadDSRC(); + this.setState({ + showConfirmDeleteDSRCAlias: false, + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to update .dsrc information: ${errMsg.desc}` + ); + this.loadDSRC(); + }); + } + + addDSRCCred () { + const { + connName, + connHostname, + connPort, + connBindDN, + connCred + } = this.state; + const conn = connName + ":" + connHostname + ":" + connPort + ":" + connBindDN + ":" + connCred; + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "repl-mon", "--add-conn=" + conn]; + + this.setState({ + loadingDSRC: true, + }); + + log_cmd("addDSRCCred", "Add a replica connection to the .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(() => { + this.setState({ + showAddDSRCCredModal: false, + }); + this.props.addNotification( + "success", + "Successfully added connection to .dsrc config file" + ); + this.loadDSRC(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to update .dsrc information: ${errMsg.desc}` + ); + this.loadDSRC(); + }); + } + + addDSRCAlias() { + const alias = this.state.aliasName + ":" + this.state.aliasHostname + ":" + this.state.aliasPort; + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "repl-mon", "--add-alias=" + alias]; + + this.setState({ + loadingDSRC: true, + }); + + log_cmd("addDSRCAlias", "Add an alias to the .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(() => { + this.setState({ + showAddDSRCAliasModal: false, + }); + this.props.addNotification( + "success", + "Successfully added alias to .dsrc config file" + ); + this.loadDSRC(); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to update .dsrc information: ${errMsg.desc}` + ); + this.loadDSRC(); + }); + } + render() { const reportData = this.state.reportData; const credentialsList = this.state.credentialsList; const aliasesList = this.state.aliasesList; - const replAgmts = this.props.data.replAgmts; - const replWinsyncAgmts = this.props.data.replWinsyncAgmts; - const cleanTasks = this.props.data.cleanTasks; - const abortTasks = this.props.data.abortTasks; - const conflictEntries = this.props.data.conflicts; - const glueEntries = this.props.data.glues; const fullReportModal = ""; let reportLoginModal = ""; let reportCredentialsModal = ""; @@ -1318,10 +1324,58 @@ export class ReplMonitor extends React.Component { extraPrimaryProps.spinnerAriaValueText = "Generating"; } - const reportContent = + let reportContent =
- - {_("Prepare Report")}}> + + Saved Report Configuration}> + + Replica Credentials}> + + + + Replica Naming Aliases}> + + + + +
+ {this.state.credRows.length > 0 && ( + + )} +
+ {"Prepare New Report"}}> {reportBtnName} +
- {_("Report Result")}}> -
- -
-
+ {reportData.length > 0 && ( + {"Report Result"}}> +
+ +
+
+ )}
; - const taskContent = -
- - CleanAllRUV Tasks ({cleanTasks.length})}> -
- -
-
- Abort CleanAllRUV Tasks ({abortTasks.length})}> -
- -
-
-
-
; + if (this.state.loadingDSRC) { + reportContent = +
+ + + Loading Replication DSRC Information ... + + + +
; + } - const conflictContent = -
- - Conflict Entries ({conflictEntries.length})}> -
- - Replication conflict entries occur when two entries are created with the - same DN (or name) on different servers at about the same time. The automatic conflict - resolution procedure renames the entry created last. Its RDN is changed - into a multi-valued RDN that includes the entry's original RDN and it's unique - identifier (nsUniqueId). There are several ways to resolve a conflict, - but choosing which option to use is up to you. -
- } - > - What Is A Replication Conflict Entry? - - -
- - Glue Entries ({glueEntries.length})}> -
- - When a Delete operation is replicated and the consumer server finds that the entry to be - deleted has child entries, the conflict resolution procedure creates a "glue entry" to - avoid having orphaned entries in the database. In the same way, when an Add operation is - replicated and the consumer server cannot find the parent entry, the conflict resolution - procedure creates a "glue entry", representing the "parent entry", so that the new entry is - not an orphaned entry. You can choose to convert the glue entry, or remove the glue entry and - all its child entries. -
- } - > - What Is A Replication Glue Entry? - - - -
- - ; + let overwriteWarning = ( + "Only one monitor configuraton can be saved in the server's " + + "'~/.dsrc' file. There is already an existing monitor " + + "configuration, and if you proceed it will be completely " + + "overwritten with the new configuraton."); + if (this.state.credRows.length == 0 && this.state.aliasRows.length == 0) { + overwriteWarning = ( + "This will save the current credentials and aliases to the " + + "server's '~/.dsrc' file so it can be reused in the future."); + } return ( -
-
- - Synchronization Report}> - {reportContent} - - Agreements ({replAgmts.length})}> -
- -
-
- Winsync ({replWinsyncAgmts.length})}> -
- -
-
- Tasks ({(cleanTasks.length + abortTasks.length)})}> -
- {taskContent} -
-
- Conflict Entries ({(conflictEntries.length + glueEntries.length)})}> -
- {conflictContent} -
-
-
- - - {fullReportModal} - {reportLoginModal} - {reportCredentialsModal} - {reportAliasesModal} - {agmtDetailModal} - {winsyncAgmtDetailModal} - +
+
+ + + Synchronization Report + + +
+ {reportContent} + {fullReportModal} + {reportLoginModal} + {reportCredentialsModal} + {reportAliasesModal} + {agmtDetailModal} + {winsyncAgmtDetailModal} - -
); @@ -1639,6 +1616,8 @@ ReplMonitor.propTypes = { data: PropTypes.object, suffix: PropTypes.string, serverId: PropTypes.string, + credRows: PropTypes.array, + aliasRows: PropTypes.array, addNotification: PropTypes.func, reloadConflicts: PropTypes.func, enableTree: PropTypes.func, @@ -1648,6 +1627,8 @@ ReplMonitor.defaultProps = { data: {}, suffix: "", serverId: "", + credRows: [], + aliasRows: [], }; export default ReplMonitor; diff --git a/src/cockpit/389-console/src/monitor.jsx b/src/cockpit/389-console/src/monitor.jsx index 53efd70d6f..de9748644d 100644 --- a/src/cockpit/389-console/src/monitor.jsx +++ b/src/cockpit/389-console/src/monitor.jsx @@ -12,11 +12,11 @@ import AuditFailLogMonitor from "./lib/monitor/auditfaillog.jsx"; import ErrorLogMonitor from "./lib/monitor/errorlog.jsx"; import SecurityLogMonitor from "./lib/monitor/securitylog.jsx"; import ReplMonitor from "./lib/monitor/replMonitor.jsx"; +import ReplAgmtMonitor from "./lib/monitor/replMonAgmts.jsx"; +import ReplAgmtWinsync from "./lib/monitor/replMonWinsync.jsx"; +import ReplMonTasks from "./lib/monitor/replMonTasks.jsx"; +import ReplMonConflict from "./lib/monitor/replMonConflict.jsx"; import { - FormSelect, - FormSelectOption, - Grid, - GridItem, Spinner, TreeView, Text, @@ -29,13 +29,13 @@ import { faLeaf, faLink, faTree, - faSyncAlt } from '@fortawesome/free-solid-svg-icons'; import { CatalogIcon, ClusterIcon, DatabaseIcon, TopologyIcon, + MonitoringIcon, } from '@patternfly/react-icons'; export class Monitor extends React.Component { @@ -47,6 +47,7 @@ export class Monitor extends React.Component { node_name: "", node_text: "", node_type: "", + node_item: "", loaded: false, snmpData: {}, ldbmData: {}, @@ -74,6 +75,9 @@ export class Monitor extends React.Component { replRole: "", replRid: "", replicatedSuffixes: [], + credRows: [], + aliasRows: [], + // Logging accesslogLocation: "", errorlogLocation: "", auditlogLocation: "", @@ -96,16 +100,17 @@ export class Monitor extends React.Component { this.loadDiskSpace = this.loadDiskSpace.bind(this); this.reloadDisks = this.reloadDisks.bind(this); // Replication - this.handleLoadMonitorReplication = this.handleLoadMonitorReplication.bind(this); + this.onHandleLoadMonitorReplication = this.onHandleLoadMonitorReplication.bind(this); this.loadCleanTasks = this.loadCleanTasks.bind(this); this.loadAbortTasks = this.loadAbortTasks.bind(this); this.loadReplicatedSuffixes = this.loadReplicatedSuffixes.bind(this); this.loadWinsyncAgmts = this.loadWinsyncAgmts.bind(this); - 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); + this.gatherAllReplicaHosts = this.gatherAllReplicaHosts.bind(this); + this.getAgmts = this.getAgmts.bind(this); // Logging this.loadMonitor = this.loadMonitor.bind(this); } @@ -139,6 +144,52 @@ export class Monitor extends React.Component { } } + processReplSuffixes(suffixTree) { + for (const suffix of this.state.replicatedSuffixes) { + suffixTree.push({ + name: suffix, + icon: , + id: "replication-suffix-" + suffix, + type: "replication-suffix", + defaultExpanded: false, + children: [ + { + name: "Agreements", + icon: , + id: suffix + "-agmts", + item: "agmt-mon", + type: "repl-mon", + suffix: suffix + }, + { + name: "Winsync Agreements", + icon: , + id: suffix + "-winsync", + item: "winsync-mon", + type: "repl-mon", + suffix: suffix + }, + { + name: "Tasks", + icon: , + id: suffix + "-tasks", + item: "task-mon", + type: "repl-mon", + suffix: suffix + }, + { + name: "Conflict Entries", + icon: , + id: suffix + "-conflict", + item: "conflict-mon", + type: "repl-mon", + suffix: suffix + }, + ], + }); + } + } + loadSuffixTree(fullReset) { const cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", @@ -162,6 +213,16 @@ export class Monitor extends React.Component { icon: , id: "replication-monitor", type: "replication", + defaultExpanded: true, + children: [ + { + name: "Synchronization Report", + icon: , + id: "sync-report", + item: "sync-report", + type: "repl-mon", + }, + ], }, { name: "Database", @@ -217,6 +278,8 @@ export class Monitor extends React.Component { type = "server"; } basicData[2].children = treeData; // database node + this.processReplSuffixes(basicData[1].children); + this.setState(() => ({ nodes: basicData, node_name: current_node, @@ -226,7 +289,9 @@ export class Monitor extends React.Component { } handleTreeClick(evt, treeViewItem, parentItem) { - if (treeViewItem.id === "log-monitor") { + if (treeViewItem.id === "log-monitor" || + treeViewItem.id === "replication-monitor" || + treeViewItem.id.startsWith("replication-suffix")) { return; } if (this.state.activeItems.length === 0 || treeViewItem === this.state.activeItems[0]) { @@ -292,21 +357,12 @@ export class Monitor extends React.Component { bename: "", }; }); - } else if (treeViewItem.id === "replication-monitor") { - if (!this.state.replInitLoaded) { - this.handleLoadMonitorReplication(); - } - this.setState(prevState => { - return { - activeItems: [treeViewItem, parentItem], - node_name: treeViewItem.id, - node_text: treeViewItem.name, - node_type: treeViewItem.type, - bename: "", - }; - }); + } else if (treeViewItem.id === "sync-report") { + this.gatherAllReplicaHosts(treeViewItem, parentItem); } else { - if (treeViewItem.id in this.state && + if (treeViewItem.type === "repl-mon") { + this.onHandleLoadMonitorReplication(treeViewItem, parentItem); + } else if (treeViewItem.id in this.state && ("chainingData" in this.state[treeViewItem.id] || "suffixData" in this.state[treeViewItem.id]) ) { @@ -354,6 +410,7 @@ export class Monitor extends React.Component { el.setAttribute('title', el.innerText); } } + this.loadDSRC(); }); } @@ -572,20 +629,20 @@ export class Monitor extends React.Component { }); } - loadCleanTasks() { + loadCleanTasks(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-tasks", "list-cleanruv-tasks", "--suffix=" + this.state.replSuffix]; + "repl-tasks", "list-cleanruv-tasks", "--suffix=" + replSuffix]; log_cmd("loadCleanTasks", "Load clean tasks", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], cleanTasks: config.items, }, - }, this.loadAbortTasks()); + }, this.loadAbortTasks(replSuffix)); }) .fail(() => { // Notification of failure (could only be server down) @@ -595,20 +652,20 @@ export class Monitor extends React.Component { }); } - loadAbortTasks() { + loadAbortTasks(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-tasks", "list-abortruv-tasks", "--suffix=" + this.state.replSuffix]; + "repl-tasks", "list-abortruv-tasks", "--suffix=" + replSuffix]; log_cmd("loadAbortCleanTasks", "Load abort tasks", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], abortTasks: config.items, }, - }, this.loadConflicts()); + }, this.loadConflicts(replSuffix)); }) .fail(() => { // Notification of failure (could only be server down) @@ -618,21 +675,21 @@ export class Monitor extends React.Component { }); } - loadConflicts() { + loadConflicts(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "list", this.state.replSuffix]; + "repl-conflict", "list", replSuffix]; log_cmd("loadConflicts", "Load conflict entries", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], conflicts: config.items, glues: [] }, - }, this.loadGlues()); + }, this.loadGlues(replSuffix)); }) .fail(() => { // Notification of failure (could only be server down) @@ -642,24 +699,23 @@ export class Monitor extends React.Component { }); } - loadGlues() { + loadGlues(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "repl-conflict", "list-glue", this.state.replSuffix]; + "repl-conflict", "list-glue", replSuffix]; log_cmd("loadGlues", "Load glue entries", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], glues: config.items, + }, - }, this.setState( - { - replLoading: false, - replInitLoaded: true - })); + replLoading: false, + replInitLoaded: true + }); }) .fail(() => { // Notification of failure (could only be server down) @@ -669,53 +725,159 @@ export class Monitor extends React.Component { }); } - loadWinsyncAgmts() { + loadDSRC() { + // Load dsrc replication report configuration + const dsrc_cmd = ["dsctl", "-j", this.props.serverId, "dsrc", "display"]; + log_cmd("loadDSRC", "Check for replication monitor configurations in the .dsrc file", dsrc_cmd); + cockpit + .spawn(dsrc_cmd, { superuser: true, err: "message" }) + .done(dsrc_content => { + const content = JSON.parse(dsrc_content); + const credRows = []; + const aliasRows = []; + if ("repl-monitor-connections" in content) { + const report_config = content["repl-monitor-connections"]; + for (const [connection, value] of Object.entries(report_config)) { + const conn = connection + ":" + value; + credRows.push(conn.split(':')); + // [repl-monitor-connections] + // connection1 = server1.example.com:389:cn=Directory Manager:* + } + } + if ("repl-monitor-aliases" in content) { + const report_config = content["repl-monitor-aliases"]; + for (const [alias_name, value] of Object.entries(report_config)) { + const alias = alias_name + ":" + value; + aliasRows.push(alias.split(':')); + // [repl-monitor-aliases] + // M1 = server1.example.com:38901 + } + } + this.setState({ + credRows, + aliasRows, + replLoading: false, + replInitLoaded: true + }); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.props.addNotification( + "error", + `Failed to get .dsrc information: ${errMsg.desc}` + ); + this.setState({ + replLoading: false, + }); + }); + } + + loadWinsyncAgmts(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "replication", "winsync-status", "--suffix=" + this.state.replSuffix]; + "replication", "winsync-status", "--suffix=" + replSuffix]; log_cmd("loadWinsyncAgmts", "Load winsync agmt status", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], replWinsyncAgmts: config.items, }, - }, this.loadCleanTasks()); + }, this.loadCleanTasks(replSuffix)); + }) + .fail(() => { + // Notification of failure (could only be server down) + this.setState({ + replLoading: false, + }); + }); + } + + getAgmts(suffixList, idx) { + const new_idx = idx + 1; + if (new_idx > suffixList.length) { + this.setState({ + replLoading: false, + disableTree: false, + }); + return; + } + + const suffix = suffixList[idx]; + const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "replication", "status", "--suffix=" + suffix]; + log_cmd("gatherAllReplicaHosts", "Get replication hosts for repl report", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + this.setState({ + [suffix]: { + ...this.state[suffix], + replAgmts: config.items, + abortTasks: [], + cleanTasks: [], + replWinsyncAgmts: [], + }, + }, () => { this.getAgmts(suffixList, new_idx) }); }) .fail(() => { // Notification of failure (could only be server down) this.setState({ replLoading: false, + disableTree: false, }); }); } - handleLoadMonitorReplication() { - const replSuffix = this.state.replSuffix; - if (replSuffix !== "") { + gatherAllReplicaHosts(treeViewItem, parentItem) { + if (treeViewItem.name !== "") { + this.setState({ + replLoading: true, + activeItems: [treeViewItem, parentItem], + node_name: treeViewItem.id, + node_text: treeViewItem.name, + node_type: treeViewItem.type, + node_item: treeViewItem.item, + }, () => { this.getAgmts(this.state.replicatedSuffixes, 0) }); + } else { + // We should enable it here because ReplMonitor never will be mounted + this.enableTree(); + } + } + + onHandleLoadMonitorReplication(treeViewItem, parentItem) { + if (treeViewItem.name !== "") { this.setState({ replLoading: true }); // Now load the agmts const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "replication", "status", "--suffix=" + replSuffix]; - log_cmd("handleLoadMonitorReplication", "Load replication agmts", cmd); + "replication", "status", "--suffix=" + treeViewItem.suffix]; + log_cmd("onHandleLoadMonitorReplication", "Load replication suffix info", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [replSuffix]: { - ...this.state[replSuffix], + [treeViewItem.suffix]: { + ...this.state[treeViewItem.suffix], replAgmts: config.items, abortTasks: [], cleanTasks: [], replWinsyncAgmts: [], }, - }, this.loadWinsyncAgmts()); + activeItems: [treeViewItem, parentItem], + node_name: treeViewItem.id, + node_text: treeViewItem.name, + node_type: treeViewItem.type, + node_item: treeViewItem.item, + replSuffix: treeViewItem.suffix, + disableTree: false, + }, this.loadWinsyncAgmts(treeViewItem.suffix)); }) .fail(() => { // Notification of failure (could only be server down) @@ -729,50 +891,40 @@ export class Monitor extends React.Component { } } - reloadReplAgmts() { + reloadReplAgmts(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "replication", "status", "--suffix=" + this.state.replSuffix]; + "replication", "status", "--suffix=" + replSuffix]; log_cmd("reloadReplAgmts", "Load replication agmts", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], replAgmts: config.items, }, }); }); } - reloadReplWinsyncAgmts() { + reloadReplWinsyncAgmts(replSuffix) { const cmd = ["dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", - "replication", "winsync-status", "--suffix=" + this.state.replSuffix]; + "replication", "winsync-status", "--suffix=" + replSuffix]; log_cmd("reloadReplWinsyncAgmts", "Load winysnc agmts", cmd); cockpit .spawn(cmd, { superuser: true, err: "message" }) .done(content => { const config = JSON.parse(content); this.setState({ - [this.state.replSuffix]: { - ...this.state[this.state.replSuffix], + [replSuffix]: { + ...this.state[replSuffix], replWinsyncAgmts: config.items, }, }); }); } - replSuffixChange(e) { - const value = e.target.value; - this.setState(() => ( - { - replSuffix: value, - replLoading: true - } - ), this.handleLoadMonitorReplication); - } - enableTree () { this.setState({ disableTree: false @@ -862,7 +1014,7 @@ export class Monitor extends React.Component { logLocation={this.state.securitylogLocation} enableTree={this.enableTree} />; - } else if (this.state.node_name === "replication-monitor") { + } else if (this.state.node_type === "repl-mon") { if (this.state.replLoading) { monitor_element =
@@ -874,57 +1026,74 @@ export class Monitor extends React.Component {
; } else { - if (this.state.replicatedSuffixes.length < 1) { + if (this.state.node_name === "sync-report") { monitor_element =
-

There are no suffixes that have been configured for replication

+
; - } else { - const suffixList = this.state.replicatedSuffixes.map((suffix) => - - ); + } else if (this.state.node_item === "agmt-mon") { monitor_element =
-
- - - Replication Monitoring - - - -
- - - { - this.replSuffixChange(event); - }} - aria-label="FormSelect Input" - > - {suffixList} - - - -
- -
+ +
; + } else if (this.state.node_item === "winsync-mon") { + monitor_element = +
+ +
; + } else if (this.state.node_item === "task-mon") { + monitor_element = +
+ +
; + } else if (this.state.node_item === "conflict-mon") { + monitor_element = +
+
; } } diff --git a/src/lib389/lib389/cli_ctl/dsrc.py b/src/lib389/lib389/cli_ctl/dsrc.py index ff5d2e8e33..c352e2edc6 100644 --- a/src/lib389/lib389/cli_ctl/dsrc.py +++ b/src/lib389/lib389/cli_ctl/dsrc.py @@ -12,6 +12,7 @@ from ldapurl import isLDAPUrl from ldap.dn import is_dn import configparser +from lib389.utils import is_a_dn def create_dsrc(inst, log, args): @@ -283,15 +284,97 @@ def display_dsrc(inst, log, args): config.read(dsrc_file) instances = config.sections() + result = {} for inst_section in instances: if args.json: - log.info(json.dumps({inst_section: dict(config[inst_section])}, indent=4)) + result[inst_section] = dict(config[inst_section]) else: log.info(f'[{inst_section}]') for k, v in config[inst_section].items(): log.info(f'{k} = {v}') log.info("") + if args.json: + log.info(json.dumps(result, indent=4)) + + +def replmon_dsrc(inst, log, args): + dsrc_file = f'{expanduser("~")}/.dsrc' + repl_conn_section = 'repl-monitor-connections' + repl_alias_section = 'repl-monitor-aliases' + + config = configparser.ConfigParser() + config.read(dsrc_file) + + # Process and validate the args + if args.add_conn is not None: + sections = config.sections() + if repl_conn_section not in sections: + config.add_section(repl_conn_section) + for conn in args.add_conn: + conn_parts = conn.split(":") + if len(conn_parts) != 5: + raise ValueError("Missing required information for connection: NAME:HOST:PORT:BINDDN:CREDENTIAL") + conn_name = conn_parts[0] + conn_host = conn_parts[1] + conn_port = conn_parts[2] + conn_binddn = conn_parts[3] + conn_cred = conn_parts[4] + if type(int(conn_port)) != int: + raise ValueError("Invalid value for PORT, must be a number") + if not is_a_dn(conn_binddn): + raise ValueError("The bind DN is not a valid DN") + if conn_name in config[repl_conn_section]: + raise ValueError("Replication connection with the same name already exists") + config[repl_conn_section][conn_name] = f"{conn_host}:{conn_port}:{conn_binddn}:{conn_cred}" + + if args.del_conn is not None: + sections = config.sections() + if repl_conn_section not in sections: + raise ValueError("There are no connections configured") + for conn in args.del_conn: + if conn not in config.options(repl_conn_section): + raise ValueError("Can not find connection: " + conn) + del config[repl_conn_section][conn] + if len(config[repl_conn_section]) == 0: + # Delete section header + del config[repl_conn_section] + + if args.add_alias is not None: + sections = config.sections() + if repl_alias_section not in sections: + config.add_section(repl_alias_section) + for alias in args.add_alias: + alias_parts = alias.split(":") + if len(alias_parts) != 3: + raise ValueError("Missing required information for alias: ALIAS_NAME:HOST:PORT") + alias_name = alias_parts[0] + alias_host = alias_parts[1] + alias_port = alias_parts[2] + if type(int(alias_port)) != int: + raise ValueError("Invalid value for PORT, must be a number") + if alias_name in config[repl_alias_section]: + raise ValueError("Replica alias with the same name already exists") + config[repl_alias_section][alias_name] = f"{alias_host}:{alias_port}" + + if args.del_alias is not None: + sections = config.sections() + if repl_alias_section not in sections: + raise ValueError("There are no aliases configured") + for alias in args.del_alias: + if alias not in config.options(repl_alias_section): + raise ValueError("Can not find alias: " + alias) + del config[repl_alias_section][alias] + if len(config[repl_alias_section]) == 0: + # delete section header + del config[repl_alias_section] + + # Okay now rewrite the file + with open(dsrc_file, 'w') as configfile: + config.write(configfile) + + log.info(f'Successfully updated: {dsrc_file}') + def create_parser(subparsers): dsrc_parser = subparsers.add_parser('dsrc', help="Manage the .dsrc file") @@ -342,3 +425,11 @@ def create_parser(subparsers): # Display .dsrc file dsrc_display_parser = subcommands.add_parser('display', help='Display the contents of the .dsrc file.') dsrc_display_parser.set_defaults(func=display_dsrc) + + # Replication Monitor actions + dsrc_replmon_parser = subcommands.add_parser('repl-mon', help='Display the contents of the .dsrc file.') + dsrc_replmon_parser.set_defaults(func=replmon_dsrc) + dsrc_replmon_parser.add_argument('--add-conn', nargs='+', help="Add a replica connection: 'NAME:HOST:PORT:BINDDN:CREDENTIAL'") + dsrc_replmon_parser.add_argument('--del-conn', nargs='+', help="delete a replica connection by its NAME") + dsrc_replmon_parser.add_argument('--add-alias', nargs='+', help="Add a host/port alias: 'ALIAS_NAME:HOST:PORT'") + dsrc_replmon_parser.add_argument('--del-alias', nargs='+', help="delete a host/port alias by its ALIAS_NAME")