ac7d03
From a3801d66f5cf3f08062c3aa67a7b33d13fae56b7 Mon Sep 17 00:00:00 2001
ac7d03
From: Martin Babinsky <mbabinsk@redhat.com>
ac7d03
Date: Thu, 11 May 2017 15:55:53 +0200
ac7d03
Subject: [PATCH] Allow for multivalued server attributes
ac7d03
ac7d03
In order to achieve the task, the following changes were required:
ac7d03
ac7d03
* vectorize the base class for server attributes
ac7d03
* add a child class that enforces single-value attributes. It still
ac7d03
  accepts/returns single-value lists in order to not break Liskov
ac7d03
  substitution principle
ac7d03
* Existing attributes inherit from the child class
ac7d03
ac7d03
https://pagure.io/freeipa/issue/6937
ac7d03
ac7d03
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
ac7d03
Reviewed-By: Stanislav Laznicka <slaznick@redhat.com>
ac7d03
---
ac7d03
 ipaserver/plugins/serverroles.py            |   4 +-
ac7d03
 ipaserver/servroles.py                      | 109 +++++++++++++++++++---------
ac7d03
 ipatests/test_ipaserver/test_serverroles.py |  10 +--
ac7d03
 3 files changed, 79 insertions(+), 44 deletions(-)
ac7d03
ac7d03
diff --git a/ipaserver/plugins/serverroles.py b/ipaserver/plugins/serverroles.py
ac7d03
index e22eadd7b163469cc9fc4472640aa64d21c9d38f..e81635c3315cc3fca84450f43fb7df883aae57d9 100644
ac7d03
--- a/ipaserver/plugins/serverroles.py
ac7d03
+++ b/ipaserver/plugins/serverroles.py
ac7d03
@@ -136,9 +136,7 @@ class serverroles(Backend):
ac7d03
 
ac7d03
         for name, attr in assoc_attributes.items():
ac7d03
             attr_value = attr.get(self.api)
ac7d03
-
ac7d03
-            if attr_value is not None:
ac7d03
-                result.update({name: attr_value})
ac7d03
+            result.update({name: attr_value})
ac7d03
 
ac7d03
         return result
ac7d03
 
ac7d03
diff --git a/ipaserver/servroles.py b/ipaserver/servroles.py
ac7d03
index cf4599995118c589a5b51236c68e13f14ac1257b..84fed1046b3b46dc9f5f8bbe6e03354725f1136c 100644
ac7d03
--- a/ipaserver/servroles.py
ac7d03
+++ b/ipaserver/servroles.py
ac7d03
@@ -277,29 +277,33 @@ class ServerAttribute(LDAPBasedProperty):
ac7d03
         try:
ac7d03
             entries = ldap2.get_entries(search_base, filter=search_filter)
ac7d03
         except errors.EmptyResult:
ac7d03
-            return
ac7d03
+            return []
ac7d03
 
ac7d03
-        master_cn = entries[0].dn[1]['cn']
ac7d03
+        master_cns = {e.dn[1]['cn'] for e in entries}
ac7d03
 
ac7d03
         associated_role_providers = set(
ac7d03
             self._get_assoc_role_providers(api_instance))
ac7d03
 
ac7d03
-        if master_cn not in associated_role_providers:
ac7d03
+        if not master_cns.issubset(associated_role_providers):
ac7d03
             raise errors.ValidationError(
ac7d03
                 name=self.name,
ac7d03
                 error=_("all masters must have %(role)s role enabled" %
ac7d03
                         {'role': self.associated_role.name})
ac7d03
             )
ac7d03
 
ac7d03
-        return master_cn
ac7d03
+        return sorted(master_cns)
ac7d03
 
ac7d03
-    def _get_master_dn(self, api_instance, server):
ac7d03
-        return DN(('cn', server), api_instance.env.container_masters,
ac7d03
-                  api_instance.env.basedn)
ac7d03
+    def _get_master_dns(self, api_instance, servers):
ac7d03
+        return [
ac7d03
+            DN(('cn', server), api_instance.env.container_masters,
ac7d03
+               api_instance.env.basedn) for server in servers]
ac7d03
+
ac7d03
+    def _get_masters_service_entries(self, ldap, master_dns):
ac7d03
+        service_dns = [
ac7d03
+            DN(('cn', self.associated_service_name), master_dn) for master_dn
ac7d03
+            in master_dns]
ac7d03
 
ac7d03
-    def _get_masters_service_entry(self, ldap, master_dn):
ac7d03
-        service_dn = DN(('cn', self.associated_service_name), master_dn)
ac7d03
-        return ldap.get_entry(service_dn)
ac7d03
+        return [ldap.get_entry(service_dn) for service_dn in service_dns]
ac7d03
 
ac7d03
     def _add_attribute_to_svc_entry(self, ldap, service_entry):
ac7d03
         """
ac7d03
@@ -341,65 +345,98 @@ class ServerAttribute(LDAPBasedProperty):
ac7d03
             r[u'server_server'] for r in self.associated_role.status(
ac7d03
                 api_instance) if r[u'status'] == ENABLED]
ac7d03
 
ac7d03
-    def _remove(self, api_instance, master):
ac7d03
+    def _remove(self, api_instance, masters):
ac7d03
         """
ac7d03
-        remove attribute from the master
ac7d03
+        remove attribute from one or more masters
ac7d03
 
ac7d03
         :param api_instance: API instance
ac7d03
-        :param master: master FQDN
ac7d03
+        :param master: list or iterable containing master FQDNs
ac7d03
         """
ac7d03
 
ac7d03
         ldap = api_instance.Backend.ldap2
ac7d03
 
ac7d03
-        master_dn = self._get_master_dn(api_instance, master)
ac7d03
-        service_entry = self._get_masters_service_entry(ldap, master_dn)
ac7d03
-        self._remove_attribute_from_svc_entry(ldap, service_entry)
ac7d03
+        master_dns = self._get_master_dns(api_instance, masters)
ac7d03
+        service_entries = self._get_masters_service_entries(ldap, master_dns)
ac7d03
+
ac7d03
+        for service_entry in service_entries:
ac7d03
+            self._remove_attribute_from_svc_entry(ldap, service_entry)
ac7d03
 
ac7d03
-    def _add(self, api_instance, master):
ac7d03
+    def _add(self, api_instance, masters):
ac7d03
         """
ac7d03
         add attribute to the master
ac7d03
         :param api_instance: API instance
ac7d03
-        :param master: master FQDN
ac7d03
+        :param master: iterable containing master FQDNs
ac7d03
 
ac7d03
         :raises: * errors.ValidationError if the associated role is not enabled
ac7d03
                    on the master
ac7d03
         """
ac7d03
 
ac7d03
-        assoc_role_providers = self._get_assoc_role_providers(api_instance)
ac7d03
+        assoc_role_providers = set(
ac7d03
+            self._get_assoc_role_providers(api_instance))
ac7d03
+        masters_set = set(masters)
ac7d03
         ldap = api_instance.Backend.ldap2
ac7d03
 
ac7d03
-        if master not in assoc_role_providers:
ac7d03
+        masters_without_role = masters_set - assoc_role_providers
ac7d03
+
ac7d03
+        if masters_without_role:
ac7d03
             raise errors.ValidationError(
ac7d03
-                name=master,
ac7d03
+                name=', '.join(sorted(masters_without_role)),
ac7d03
                 error=_("must have %(role)s role enabled" %
ac7d03
                         {'role': self.associated_role.name})
ac7d03
             )
ac7d03
 
ac7d03
-        master_dn = self._get_master_dn(api_instance, master)
ac7d03
-        service_entry = self._get_masters_service_entry(ldap, master_dn)
ac7d03
-        self._add_attribute_to_svc_entry(ldap, service_entry)
ac7d03
+        master_dns = self._get_master_dns(api_instance, masters)
ac7d03
+        service_entries = self._get_masters_service_entries(ldap, master_dns)
ac7d03
+        for service_entry in service_entries:
ac7d03
+            self._add_attribute_to_svc_entry(ldap, service_entry)
ac7d03
 
ac7d03
-    def set(self, api_instance, master):
ac7d03
+    def set(self, api_instance, masters):
ac7d03
         """
ac7d03
-        set the attribute on master
ac7d03
+        set the attribute on masters
ac7d03
 
ac7d03
         :param api_instance: API instance
ac7d03
-        :param master: FQDN of the new master
ac7d03
+        :param masters: an interable with FQDNs of the new masters
ac7d03
 
ac7d03
-        the attribute is automatically unset from previous master if present
ac7d03
+        the attribute is automatically unset from previous masters if present
ac7d03
 
ac7d03
         :raises: errors.EmptyModlist if the new masters is the same as
ac7d03
-                 the original on
ac7d03
+                 the original ones
ac7d03
         """
ac7d03
-        old_master = self.get(api_instance)
ac7d03
+        old_masters = self.get(api_instance)
ac7d03
 
ac7d03
-        if old_master == master:
ac7d03
+        if sorted(old_masters) == sorted(masters):
ac7d03
             raise errors.EmptyModlist
ac7d03
 
ac7d03
-        self._add(api_instance, master)
ac7d03
+        if old_masters:
ac7d03
+            self._remove(api_instance, old_masters)
ac7d03
+
ac7d03
+        self._add(api_instance, masters)
ac7d03
+
ac7d03
+
ac7d03
+class SingleValuedServerAttribute(ServerAttribute):
ac7d03
+    """
ac7d03
+    Base class for server attributes that are forced to be single valued
ac7d03
+
ac7d03
+    this means that `get` method will return a one-element list, and `set`
ac7d03
+    method will accept only one-element list
ac7d03
+    """
ac7d03
+
ac7d03
+    def set(self, api_instance, masters):
ac7d03
+        if len(masters) > 1:
ac7d03
+            raise errors.ValidationError(
ac7d03
+                name=self.attr_name,
ac7d03
+                error=_("must be enabled only on a single master"))
ac7d03
+
ac7d03
+        super(SingleValuedServerAttribute, self).set(api_instance, masters)
ac7d03
+
ac7d03
+    def get(self, api_instance):
ac7d03
+        masters = super(SingleValuedServerAttribute, self).get(api_instance)
ac7d03
+        num_masters = len(masters)
ac7d03
+
ac7d03
+        if num_masters > 1:
ac7d03
+            raise errors.SingleMatchExpected(found=num_masters)
ac7d03
 
ac7d03
-        if old_master is not None:
ac7d03
-            self._remove(api_instance, old_master)
ac7d03
+        return masters
ac7d03
 
ac7d03
 
ac7d03
 _Service = namedtuple('Service', ['name', 'enabled'])
ac7d03
@@ -574,14 +611,14 @@ role_instances = (
ac7d03
 )
ac7d03
 
ac7d03
 attribute_instances = (
ac7d03
-    ServerAttribute(
ac7d03
+    SingleValuedServerAttribute(
ac7d03
         u"ca_renewal_master_server",
ac7d03
         u"CA renewal master",
ac7d03
         u"ca_server_server",
ac7d03
         u"CA",
ac7d03
         u"caRenewalMaster",
ac7d03
     ),
ac7d03
-    ServerAttribute(
ac7d03
+    SingleValuedServerAttribute(
ac7d03
         u"dnssec_key_master_server",
ac7d03
         u"DNSSec key master",
ac7d03
         u"dns_server_server",
ac7d03
diff --git a/ipatests/test_ipaserver/test_serverroles.py b/ipatests/test_ipaserver/test_serverroles.py
ac7d03
index d8844df3007f076532a34d625379ab552ef45363..e671272783d8d71c2ee56074459433b98b79dd0a 100644
ac7d03
--- a/ipatests/test_ipaserver/test_serverroles.py
ac7d03
+++ b/ipatests/test_ipaserver/test_serverroles.py
ac7d03
@@ -706,7 +706,7 @@ class TestServerAttributes(object):
ac7d03
         actual_attr_masters = self.config_retrieve(
ac7d03
             assoc_role, mock_api)[attr_name]
ac7d03
 
ac7d03
-        assert actual_attr_masters == fqdn
ac7d03
+        assert actual_attr_masters == [fqdn]
ac7d03
 
ac7d03
     def test_set_attribute_on_the_same_provider_raises_emptymodlist(
ac7d03
             self, mock_api, mock_masters):
ac7d03
@@ -727,7 +727,7 @@ class TestServerAttributes(object):
ac7d03
         non_ca_fqdn = mock_masters.get_fqdn('trust-controller-dns')
ac7d03
 
ac7d03
         with pytest.raises(errors.ValidationError):
ac7d03
-            self.config_update(mock_api, **{attr_name: non_ca_fqdn})
ac7d03
+            self.config_update(mock_api, **{attr_name: [non_ca_fqdn]})
ac7d03
 
ac7d03
     def test_set_unknown_attribute_on_master_raises_notfound(
ac7d03
             self, mock_api, mock_masters):
ac7d03
@@ -735,7 +735,7 @@ class TestServerAttributes(object):
ac7d03
         fqdn = mock_masters.get_fqdn('trust-controller-ca')
ac7d03
 
ac7d03
         with pytest.raises(errors.NotFound):
ac7d03
-            self.config_update(mock_api, **{attr_name: fqdn})
ac7d03
+            self.config_update(mock_api, **{attr_name: [fqdn]})
ac7d03
 
ac7d03
     def test_set_ca_renewal_master_on_other_ca_and_back(self, mock_api,
ac7d03
                                                         mock_masters):
ac7d03
@@ -747,7 +747,7 @@ class TestServerAttributes(object):
ac7d03
         other_ca_server = mock_masters.get_fqdn('trust-controller-ca')
ac7d03
 
ac7d03
         for host in (other_ca_server, original_renewal_master):
ac7d03
-            self.config_update(mock_api, **{attr_name: host})
ac7d03
+            self.config_update(mock_api, **{attr_name: [host]})
ac7d03
 
ac7d03
             assert (
ac7d03
-                self.config_retrieve(role_name, mock_api)[attr_name] == host)
ac7d03
+                self.config_retrieve(role_name, mock_api)[attr_name] == [host])
ac7d03
-- 
ac7d03
2.9.4
ac7d03