6d47df
From 67875c3b75ad1af493ff5930f9c5fd5e9797b775 Mon Sep 17 00:00:00 2001
6d47df
From: Thomas Woerner <twoerner@redhat.com>
6d47df
Date: Oct 12 2018 07:50:29 +0000
6d47df
Subject: Find orphan automember rules
6d47df
6d47df
6d47df
If groups or hostgroups have been removed after automember rules have been
6d47df
created using them, then automember-rebuild, automember-add, host-add and
6d47df
more commands could fail.
6d47df
6d47df
A new command has been added to the ipa tool:
6d47df
6d47df
  ipa automember-find-orphans --type={hostgroup,group} [--remove]
6d47df
6d47df
This command retuns the list of orphan automember rules in the same way as
6d47df
automember-find. With the --remove option the orphan rules are also removed.
6d47df
6d47df
The IPA API version has been increased and a test case has been added.
6d47df
6d47df
Using ideas from a patch by: Rob Crittenden <rcritten@redhat.com>
6d47df
6d47df
See: https://pagure.io/freeipa/issue/6476
6d47df
Signed-off-by: Thomas Woerner <twoerner@redhat.com>
6d47df
Reviewed-By: Christian Heimes <cheimes@redhat.com>
6d47df
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
6d47df
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
6d47df
6d47df
---
6d47df
6d47df
diff --git a/API.txt b/API.txt
6d47df
index 49216cb..93e1a38 100644
6d47df
--- a/API.txt
6d47df
+++ b/API.txt
6d47df
@@ -186,6 +186,20 @@ output: Output('count', type=[<type 'int'>])
6d47df
 output: ListOfEntries('result')
6d47df
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
6d47df
 output: Output('truncated', type=[<type 'bool'>])
6d47df
+command: automember_find_orphans/1
6d47df
+args: 1,7,4
6d47df
+arg: Str('criteria?')
6d47df
+option: Flag('all', autofill=True, cli_name='all', default=False)
6d47df
+option: Str('description?', autofill=False, cli_name='desc')
6d47df
+option: Flag('pkey_only?', autofill=True, default=False)
6d47df
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
6d47df
+option: Flag('remove?', autofill=True, default=False)
6d47df
+option: StrEnum('type', values=[u'group', u'hostgroup'])
6d47df
+option: Str('version?')
6d47df
+output: Output('count', type=[<type 'int'>])
6d47df
+output: ListOfEntries('result')
6d47df
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
6d47df
+output: Output('truncated', type=[<type 'bool'>])
6d47df
 command: automember_mod/1
6d47df
 args: 1,9,3
6d47df
 arg: Str('cn', cli_name='automember_rule')
6d47df
@@ -6503,6 +6517,7 @@ default: automember_default_group_set/1
6d47df
 default: automember_default_group_show/1
6d47df
 default: automember_del/1
6d47df
 default: automember_find/1
6d47df
+default: automember_find_orphans/1
6d47df
 default: automember_mod/1
6d47df
 default: automember_rebuild/1
6d47df
 default: automember_remove_condition/1
6d47df
diff --git a/VERSION.m4 b/VERSION.m4
6d47df
index f437ef0..9d5532c 100644
6d47df
--- a/VERSION.m4
6d47df
+++ b/VERSION.m4
6d47df
@@ -83,8 +83,8 @@ define(IPA_DATA_VERSION, 20100614120000)
6d47df
 #                                                      #
6d47df
 ########################################################
6d47df
 define(IPA_API_VERSION_MAJOR, 2)
6d47df
-define(IPA_API_VERSION_MINOR, 229)
6d47df
-# Last change: Added the Certificate parameter
6d47df
+define(IPA_API_VERSION_MINOR, 230)
6d47df
+# Last change: Added `automember-find-orphans' command
6d47df
 
6d47df
 
6d47df
 ########################################################
6d47df
diff --git a/ipaserver/plugins/automember.py b/ipaserver/plugins/automember.py
6d47df
index a502aea..a7f468d 100644
6d47df
--- a/ipaserver/plugins/automember.py
6d47df
+++ b/ipaserver/plugins/automember.py
6d47df
@@ -117,6 +117,11 @@ EXAMPLES:
6d47df
  Find all of the automember rules:
6d47df
     ipa automember-find
6d47df
 """) + _("""
6d47df
+ Find all of the orphan automember rules:
6d47df
+    ipa automember-find-orphans --type=hostgroup
6d47df
+ Find all of the orphan automember rules and remove them:
6d47df
+    ipa automember-find-orphans --type=hostgroup --remove
6d47df
+""") + _("""
6d47df
  Display a automember rule:
6d47df
     ipa automember-show --type=hostgroup webservers
6d47df
     ipa automember-show --type=group devel
6d47df
@@ -817,3 +822,58 @@ class automember_rebuild(Method):
6d47df
             result=result,
6d47df
             summary=unicode(summary),
6d47df
             value=pkey_to_value(None, options))
6d47df
+
6d47df
+
6d47df
+@register()
6d47df
+class automember_find_orphans(LDAPSearch):
6d47df
+    __doc__ = _("""
6d47df
+    Search for orphan automember rules. The command might need to be run as
6d47df
+    a privileged user user to get all orphan rules.
6d47df
+    """)
6d47df
+    takes_options = group_type + (
6d47df
+        Flag(
6d47df
+            'remove?',
6d47df
+            doc=_("Remove orphan automember rules"),
6d47df
+        ),
6d47df
+    )
6d47df
+
6d47df
+    msg_summary = ngettext(
6d47df
+        '%(count)d rules matched', '%(count)d rules matched', 0
6d47df
+    )
6d47df
+
6d47df
+    def execute(self, *keys, **options):
6d47df
+        results = super(automember_find_orphans, self).execute(*keys,
6d47df
+                                                               **options)
6d47df
+
6d47df
+        remove_option = options.get('remove')
6d47df
+        pkey_only = options.get('pkey_only', False)
6d47df
+        ldap = self.obj.backend
6d47df
+        orphans = []
6d47df
+        for entry in results["result"]:
6d47df
+            am_dn_entry = entry['automembertargetgroup'][0]
6d47df
+            # Make DN for --raw option
6d47df
+            if not isinstance(am_dn_entry, DN):
6d47df
+                am_dn_entry = DN(am_dn_entry)
6d47df
+            try:
6d47df
+                ldap.get_entry(am_dn_entry)
6d47df
+            except errors.NotFound:
6d47df
+                if pkey_only:
6d47df
+                    # For pkey_only remove automembertargetgroup
6d47df
+                    del(entry['automembertargetgroup'])
6d47df
+                orphans.append(entry)
6d47df
+                if remove_option:
6d47df
+                    ldap.delete_entry(entry['dn'])
6d47df
+
6d47df
+        results["result"][:] = orphans
6d47df
+        results["count"] = len(orphans)
6d47df
+        return results
6d47df
+
6d47df
+    def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args,
6d47df
+                     **options):
6d47df
+        assert isinstance(base_dn, DN)
6d47df
+        scope = ldap.SCOPE_SUBTREE
6d47df
+        ndn = DN(('cn', options['type']), base_dn)
6d47df
+        if options.get('pkey_only', False):
6d47df
+            # For pkey_only add automembertargetgroup
6d47df
+            attrs_list.append('automembertargetgroup')
6d47df
+        return filters, ndn, scope
6d47df
diff --git a/ipatests/test_xmlrpc/test_automember_plugin.py b/ipatests/test_xmlrpc/test_automember_plugin.py
6d47df
index ffbc911..c83e11a 100644
6d47df
--- a/ipatests/test_xmlrpc/test_automember_plugin.py
6d47df
+++ b/ipatests/test_xmlrpc/test_automember_plugin.py
6d47df
@@ -715,3 +715,51 @@ class TestMultipleAutomemberConditions(XMLRPC_test):
6d47df
 
6d47df
         defaultgroup1.ensure_missing()
6d47df
         defaulthostgroup1.ensure_missing()
6d47df
+
6d47df
+
6d47df
+@pytest.mark.tier1
6d47df
+class TestAutomemberFindOrphans(XMLRPC_test):
6d47df
+    def test_create_deps_for_find_orphans(self, hostgroup1, host1,
6d47df
+                                          automember_hostgroup):
6d47df
+        """ Create host, hostgroup, and automember tracker for this class
6d47df
+        of tests. """
6d47df
+
6d47df
+        # Create hostgroup1 and automember rule with condition
6d47df
+        hostgroup1.ensure_exists()
6d47df
+        host1.ensure_exists()
6d47df
+
6d47df
+        # Manually create automember rule and condition, racker will try to
6d47df
+        # remove the automember rule in the end, which is failing as the rule
6d47df
+        # is already removed
6d47df
+        api.Command['automember_add'](hostgroup1.cn, type=u'hostgroup')
6d47df
+        api.Command['automember_add_condition'](
6d47df
+            hostgroup1.cn,
6d47df
+            key=u'fqdn', type=u'hostgroup',
6d47df
+            automemberinclusiveregex=[hostgroup_include_regex]
6d47df
+        )
6d47df
+
6d47df
+        hostgroup1.retrieve()
6d47df
+
6d47df
+    def test_find_orphan_automember_rules(self, hostgroup1):
6d47df
+        """ Remove hostgroup1, find and remove obsolete automember rules. """
6d47df
+        # Remove hostgroup1
6d47df
+
6d47df
+        hostgroup1.ensure_missing()
6d47df
+
6d47df
+        # Find obsolete automember rules
6d47df
+        result = api.Command['automember_find_orphans'](type=u'hostgroup')
6d47df
+        assert result['count'] == 1
6d47df
+
6d47df
+        # Find and remove obsolete automember rules
6d47df
+        result = api.Command['automember_find_orphans'](type=u'hostgroup',
6d47df
+                                                        remove=True)
6d47df
+        assert result['count'] == 1
6d47df
+
6d47df
+        # Find obsolete automember rules
6d47df
+        result = api.Command['automember_find_orphans'](type=u'hostgroup')
6d47df
+        assert result['count'] == 0
6d47df
+
6d47df
+        # Final cleanup of automember rule if it still exists
6d47df
+        with raises_exact(errors.NotFound(
6d47df
+                reason=u'%s: Automember rule not found' % hostgroup1.cn)):
6d47df
+            api.Command['automember_del'](hostgroup1.cn, type=u'hostgroup')
6d47df