Some notes:

1. As mentioned in the WIP patch thread: original 'del' worked also with winsync agreements. I'm not sure why is that. Shouldn't 'disconnect' be used for winsync agreements? At least man page says that. This patch doesn't support it if domain level > 0. Is it a blocker?


Following should be addressed in beta:

2. If `ipa-replica-manage del` is run before `ipa-csreplica-manage del` then the `ipa-csreplica-manage del` will fail unless run with --force options.

3. Check for orphaned server is missing. I want to use proper graph traversing algorithm for that given that we have the whole topology.

4. Probably a work for topology plugin: I've seen that the removed master doesn't remove its segments and agreements even though that it knows about its removal (doesn't have its own entry in cn=masters). It leads to failed replication connection attempts. Not a big issue, but also not wanted.
--
Petr Vobornik
From 8cb9cce314b97c785c7ea2bbadaf7d98c8c4f228 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Fri, 12 Jun 2015 15:56:30 +0200
Subject: [PATCH] ipa-replica-manage: adjust del to work with managed topology

Introduces new method for deletion of replica. This method is used if
managed topology is enabled.

part of https://fedorahosted.org/freeipa/ticket/4302
---
 install/tools/ipa-replica-manage | 228 ++++++++++++++++++++++++++++-----------
 1 file changed, 165 insertions(+), 63 deletions(-)

diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index a2b2c820d8e25a2587358e00dc4afc54b309d77b..132ba914d586de640d4829612fe6b7bf2abc9a67 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -25,6 +25,7 @@ import traceback
 from urllib2 import urlparse
 import ldap
 import socket
+import time
 
 from ipapython import ipautil
 from ipaserver.install import replication, dsinstance, installutils
@@ -560,6 +561,13 @@ def check_last_link(delrepl, realm, dirman_passwd, force):
     else:
         return None
 
+def check_last_link_managed(api, masters, hostname, force):
+    # segments = api.Command.topologysegment_find(u'realm', sizelimit=0).get('result')
+    # replica_names = [m.single_value('cn') for m in masters]
+    # orphaned = []
+    # TODO add proper graph traversing algorithm here
+    return None
+
 def enforce_host_existence(host, message=None):
     if host is not None and not ipautil.host_exists(host):
         if message is None:
@@ -567,8 +575,160 @@ def enforce_host_existence(host, message=None):
 
         sys.exit(message)
 
+def ensure_last_services(conn, hostname, masters, options):
+    """
+    1. When deleting master, check if there will be at least one remaining
+       DNS and CA server.
+    2. Pick CA renewal master
+
+    Return this_services, other_services, ca_hostname
+    """
+
+    this_services = []
+    other_services = []
+    ca_hostname = None
+
+    for master in masters:
+        try:
+            services = conn.get_entries(master['dn'], conn.SCOPE_ONELEVEL)
+        except errors.NotFound:
+            continue
+        services_cns = [s.single_value['cn'] for s in services]
+        if master == hostname:
+            this_services = services_cns
+        else:
+            other_services.append(services_cns)
+            if ca_hostname is None and 'CA' in services_cns:
+                ca_hostname = master
+
+    if 'CA' in this_services and not any(['CA' in o for o in other_services]):
+        print "Deleting this server is not allowed as it would leave your installation without a CA."
+        sys.exit(1)
+
+    other_dns = True
+    if 'DNS' in this_services and not any(['DNS' in o for o in other_services]):
+        other_dns = False
+        print "Deleting this server will leave your installation without a DNS."
+        if not options.force and not ipautil.user_input("Continue to delete?", False):
+            sys.exit("Deletion aborted")
+
+    # test if replica is not DNSSEC master
+    # allow to delete it if is last DNS server
+    if 'DNS' in this_services and other_dns and not options.force:
+        dnssec_masters = opendnssecinstance.get_dnssec_key_masters(conn)
+        if hostname in dnssec_masters:
+            print "Replica is active DNSSEC key master. Uninstall could break your DNS system."
+            sys.exit("Deletion aborted")
+
+    ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
+    if ca.is_renewal_master(hostname):
+        try:
+            ca.set_renewal_master(options.host)
+        except errors.NotFound:
+            ca.set_renewal_master(ca_hostname)
+
+    return this_services, other_services, ca_hostname
+
+
+def cleanup_server_dns_entries(realm, hostname, suffix, options):
+    try:
+        if bindinstance.dns_container_exists(options.host, suffix,
+                                             dm_password=options.dirman_passwd):
+            bind = bindinstance.BindInstance()
+            bind.remove_master_dns_records(hostname, realm, realm.lower())
+            bind.remove_ipa_ca_dns_records(hostname, realm.lower())
+            bind.remove_server_ns_records(hostname)
+
+            keysyncd = dnskeysyncinstance.DNSKeySyncInstance()
+            keysyncd.remove_replica_public_keys(hostname)
+    except Exception, e:
+        print "Failed to cleanup %s DNS entries: %s" % (hostname, e)
+        print "You may need to manually remove them from the tree"
+
+
 def del_master(realm, hostname, options):
 
+    if has_managed_topology():
+        del_master_managed(realm, hostname, options)
+    else:
+        del_master_direct(realm, hostname, options)
+
+def del_master_managed(realm, hostname, options):
+    """
+    Removing of master in managed_topology
+    """
+
+    hostname_u = unicode(hostname)
+    if hostname == options.host:
+        print "Can't remove itself: %s" % (options.host)
+        sys.exit(1)
+
+    # 1. Connect to the local server
+    try:
+        thisrepl = replication.ReplicationManager(realm, options.host,
+                                                  options.dirman_passwd)
+    except Exception as e:
+        print "Failed to connect to server %s: %s" % (options.host, e)
+        sys.exit(1)
+
+    # 2. Get all masters
+    masters = api.Command.server_find('', sizelimit=0)['result']
+
+    # 3. Check topology
+    orphans = check_last_link_managed(api, masters, hostname, options.force)
+
+    # 4. Check that we are not leaving the installation without CA and/or DNS
+    #    And pick new CA master.
+    ensure_last_services(api.Backend.ldap2, hostname, masters, options)
+
+    # Save the RID value before we start deleting
+    rid = get_rid_by_host(realm, options.host, hostname,
+                          options.dirman_passwd, options.nolookup)
+
+    # 5. Remove master entry. Topology plugin will remove replication agreements.
+    try:
+        api.Command.server_del(hostname_u)
+    except errors.NotFound:
+        print "Server entry already deleted: %s" % (options.host)
+
+    # 6. Cleanup
+    try:
+        thisrepl.replica_cleanup(hostname, realm, force=True)
+    except Exception, e:
+        print "Failed to cleanup %s entries: %s" % (hostname, e)
+        print "You may need to manually remove them from the tree"
+
+    # 7. Clean RUV for the deleted master
+    # Wait for topology plugin to delete segments
+    i = 0
+    while True:
+        left = api.Command.topologysegment_find(
+            u'realm', iparepltoposegmentleftnode=hostname_u, sizelimit=0)['result']
+        right = api.Command.topologysegment_find(
+            u'realm', iparepltoposegmentrightnode=hostname_u, sizelimit=0)['result']
+        if not left and not right:
+            print "Agreements deleted"
+            break
+        time.sleep(1)
+        if i == 5: # taking too long, something is wrong, report
+            print "Waiting for removal of replication agreements"
+        i += 1
+
+    # Clean RUV
+    if rid is not None:
+        try:
+            thisrepl.cleanallruv(rid)
+        except KeyboardInterrupt:
+            print "Wait for task interrupted. It will continue to run in the background"
+
+    # 8. And clean up the removed replica DNS entries if any.
+    cleanup_server_dns_entries(realm, hostname, thisrepl.suffix, options)
+
+def del_master_direct(realm, hostname, options):
+    """
+    Removing of master for realm without managed topology (domain level < 1)
+    """
+
     force_del = False
     delrepl = None
 
@@ -647,10 +807,8 @@ def del_master(realm, hostname, options):
 
     # Check for orphans if the remote server is up.
     if delrepl and not winsync:
-        masters_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm))
         try:
-            masters = delrepl.conn.get_entries(
-                masters_dn, delrepl.conn.SCOPE_ONELEVEL)
+            masters = api.Command.server_find('', sizelimit=0)['result']
         except Exception, e:
             masters = []
             print "Failed to read masters data from '%s': %s" % (
@@ -668,53 +826,9 @@ def del_master(realm, hostname, options):
                 print "You will need to reconfigure your replication topology to delete this server."
                 sys.exit(1)
 
-        # Check that we are not leaving the installation without CA and/or DNS
-        this_services = []
-        other_services = []
-        ca_hostname = None
-
-        for master_cn in [m.single_value['cn'] for m in masters]:
-            master_dn = DN(('cn', master_cn), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm))
-            try:
-                services = delrepl.conn.get_entries(master_dn,
-                                                    delrepl.conn.SCOPE_ONELEVEL)
-            except errors.NotFound:
-                continue
-            services_cns = [s.single_value['cn'] for s in services]
-
-            if master_cn == hostname:
-                this_services = services_cns
-            else:
-                other_services.append(services_cns)
-                if ca_hostname is None and 'CA' in services_cns:
-                    ca_hostname = master_cn
-
-        if 'CA' in this_services and not any(['CA' in o for o in other_services]):
-            print "Deleting this server is not allowed as it would leave your installation without a CA."
-            sys.exit(1)
-
-        other_dns = True
-        if 'DNS' in this_services and not any(['DNS' in o for o in other_services]):
-            other_dns = False
-            print "Deleting this server will leave your installation without a DNS."
-            if not options.force and not ipautil.user_input("Continue to delete?", False):
-                sys.exit("Deletion aborted")
-
-        # test if replica is not DNSSEC master
-        # allow to delete it if is last DNS server
-        if 'DNS' in this_services and other_dns and not options.force:
-            dnssec_masters = opendnssecinstance.get_dnssec_key_masters(delrepl.conn)
-            if hostname in dnssec_masters:
-                print "Replica is active DNSSEC key master. Uninstall could break your DNS system."
-                sys.exit("Deletion aborted")
-
-        # Pick CA renewal master
-        ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
-        if ca.is_renewal_master(hostname):
-            try:
-                ca.set_renewal_master(options.host)
-            except errors.NotFound:
-                ca.set_renewal_master(ca_hostname)
+        # 4. Check that we are not leaving the installation without CA and/or DNS
+        #    And pick new CA master.
+        ensure_last_services(thisrepl.conn, hostname, masters, options)
     else:
         print "Skipping calculation to determine if one or more masters would be orphaned."
 
@@ -749,19 +863,7 @@ def del_master(realm, hostname, options):
         print "You may need to manually remove them from the tree"
 
     # 7. And clean up the removed replica DNS entries if any.
-    try:
-        if bindinstance.dns_container_exists(options.host, thisrepl.suffix,
-                                             dm_password=options.dirman_passwd):
-            bind = bindinstance.BindInstance()
-            bind.remove_master_dns_records(hostname, realm, realm.lower())
-            bind.remove_ipa_ca_dns_records(hostname, realm.lower())
-            bind.remove_server_ns_records(hostname)
-
-            keysyncd = dnskeysyncinstance.DNSKeySyncInstance()
-            keysyncd.remove_replica_public_keys(hostname)
-    except Exception, e:
-        print "Failed to cleanup %s DNS entries: %s" % (hostname, e)
-        print "You may need to manually remove them from the tree"
+    cleanup_server_dns_entries(realm, hostname, thisrepl.suffix, options)
 
 def add_link(realm, replica1, replica2, dirman_passwd, options):
 
-- 
2.1.0

From eb47836532125c393fa83db88daf1925afed8c5e Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Thu, 11 Jun 2015 15:38:32 +0200
Subject: [PATCH] server: add "del" command

this command is internal and is supposed to be used by ipa-replica-managed to
delete replica.
---
 API.txt                  | 8 ++++++++
 VERSION                  | 4 ++--
 ipalib/plugins/server.py | 7 +++++++
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/API.txt b/API.txt
index 853d26a59bb5bb1ebff698924a36a30b7757c398..ff53e9457ebaa36004556feebd88515aea2a7a8d 100644
--- a/API.txt
+++ b/API.txt
@@ -3799,6 +3799,14 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: server_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
 command: server_find
 args: 1,10,4
 arg: Str('criteria?', noextrawhitespace=False)
diff --git a/VERSION b/VERSION
index a5f40747aa7aa5a19073e077cb3a0607ae42e146..cb5492a02c5a431c81756dd07df5e44f7d0f8add 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=131
-# Last change: pvoborni - toposegment direction restrictions
+IPA_API_VERSION_MINOR=132
+# Last change: pvoborni - add server-del internal command
diff --git a/ipalib/plugins/server.py b/ipalib/plugins/server.py
index d22f1ea368ad09ab2cff00429f509c99d92f0f60..7fc44197343dbb651782fbf79993cbbe8818efed 100644
--- a/ipalib/plugins/server.py
+++ b/ipalib/plugins/server.py
@@ -87,3 +87,10 @@ class server_find(LDAPSearch):
 @register()
 class server_show(LDAPRetrieve):
     __doc__ = _('Show IPA server.')
+
+
+@register()
+class server_del(LDAPDelete):
+    __doc__ = _('Delete IPA server.')
+    NO_CLI = True
+    msg_summary = _('Deleted IPA server "%(value)s"')
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to