On 06/07/2016 07:01 PM, Pavel Vomacka wrote:


On 06/07/2016 12:07 PM, Martin Babinsky wrote:
On 06/03/2016 05:25 PM, Martin Babinsky wrote:
I am sending rebased patches implementing
http://www.freeipa.org/page/V4/Server_Roles

I hope the patches work since I have had a lot of fun rebasing them on
top of thin client and DNS locations effort.

https://fedorahosted.org/freeipa/ticket/5181




Sending updated patches according to Jan's interactive review.

Since the name of attributes returned by API commands and signature of
`server-role-find` have changed, a small update in WebUI patches is
required.



NACK, why did you remove sizelimit from server_role_find command's? Is
it possible to return it back? It breaks WebUI.

Indeed, this was caused by changing the base class of the command. It is fixed in updated patches.

--
Martin^3 Babinsky
From 735403be2b42356acb978815d30163221cc21c2d Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 26 May 2016 19:24:22 +0200
Subject: [PATCH 1/7] Server Roles: definitions of server roles and attributes

This patch introduces classes which define the properties of server roles and
attributes and their relationship to LDAP attributes representing the
role/attribute.

A brief documentation about defining and using roles is given at the beginning
of the module.

http://www.freeipa.org/page/V4/Server_Roles
https://fedorahosted.org/freeipa/ticket/5181
---
 ipaserver/servroles.py | 586 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 586 insertions(+)
 create mode 100644 ipaserver/servroles.py

diff --git a/ipaserver/servroles.py b/ipaserver/servroles.py
new file mode 100644
index 0000000000000000000000000000000000000000..8628cd625f897da5c1a8539ef860ae70a44de2d8
--- /dev/null
+++ b/ipaserver/servroles.py
@@ -0,0 +1,586 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+
+"""
+This module contains the set of classes which abstract various bits and pieces
+of information present in the LDAP tree about functionalities such as DNS
+server, Active Directory trust controller etc. These properties come in two
+distinct groups:
+
+    server roles
+        this group represents a genral functionality provided by one or more
+        IPA servers, such as DNS server, certificate authority and such. In
+        this case there is a many-to-many mapping between the roles and the
+        masters which provide them.
+
+    server attributes
+        these represent a functionality associated with the whole topology,
+        such as CA renewal master or DNSSec key master.
+
+See the corresponding design page (http://www.freeipa.org/page/V4/Server_Roles)
+for more info.
+
+Both of these groups use `LDAPBasedProperty` class as a base.
+
+Server Roles
+============
+
+Server role objects are usually consuming information from the master's service
+container (cn=FQDN,cn=masters,cn=ipa,cn=etc,$SUFFIX) are represented by
+`ServiceBasedRole`class. To create an instance of such role, you only need to
+specify role name and individual services comprising the role (more systemd
+services may be enabled to provide some function):
+
+>>> example_role = ServiceBasedRole(
+...     "Example Role",
+...     component_services = ['SERVICE1', 'SERVICE2'])
+>>> example_role.name
+'Example Role'
+
+The role object can then be queried for the status of the role in the whole
+topology or on a single master by using its `status` method. This method
+returns a list of dictionaries akin to LDAP entries comprised from server name,
+role name and role status (enabled if role is enabled, configured if the
+service entries are present but not marked as enabled by 'enabledService'
+config string, absent if the service entries are not present).
+
+Note that 'AD trust agent' role is based on membership of the master in the
+'adtrust agents' sysaccount group and is thus an instance of different class
+(`ADTrustBasedRole`). This role also does not have 'configured' status, since
+the master is either member of the group ('enabled') or not ('absent')
+
+Server Attributes
+=================
+
+Server attributes are implemented as instances of `ServerAttribute` class. The
+attribute is defined by some flag set on 'ipaConfigString' attribute of some
+service entry. To create your own server attribute, see the following example:
+
+>>> example_attribute = ServerAttribute("Example Attribute", example_role,
+...                                     "SERVICE1", "roleMaster")
+>>> example_attribute.name
+'Example Attribute'
+
+The FQDN of master with the attribute set can be requested using `get()`
+method. The attribute master can be changed by the `set()` method
+which accepts FQDN of a new master hosting the attribute.
+
+The available role/attribute instances are stored in
+`role_instances`/`attribute_instances` tuples.
+"""
+
+import abc
+from collections import namedtuple, defaultdict
+
+from ldap import SCOPE_ONELEVEL
+import six
+
+from ipalib import _, errors
+from ipapython.dn import DN
+
+
+if six.PY3:
+    unicode = str
+
+
+ENABLED = u'enabled'
+CONFIGURED = u'configured'
+ABSENT = u'absent'
+
+
+@six.add_metaclass(abc.ABCMeta)
+class LDAPBasedProperty(object):
+    """
+    base class for all master properties defined by LDAP content
+    :param attr_name: attribute name
+    :param name: user-friendly name of the property
+    :param attrs_list: list of attributes to retrieve during search, defaults
+        to all
+    """
+
+    def __init__(self, attr_name, name):
+        self.attr_name = attr_name
+        self.name = name
+
+
+@six.add_metaclass(abc.ABCMeta)
+class BaseServerRole(LDAPBasedProperty):
+    """
+    Server role hierarchy apex. All other server role definition should either
+    inherit from it or at least provide the 'status' method for querying role
+    status
+    property
+    """
+
+    def create_role_status_dict(self, server, status):
+        """
+        the output of `status()` method should be a list of dictionaries having
+        the following keys:
+            * role_servrole: name of role
+            * server_server: server FQDN
+            * status: role status on server
+
+        this methods returns such a dict given server and role status
+        """
+        return {
+            u'role_servrole': self.name,
+            u'server_server': server,
+            u'status': status}
+
+    @abc.abstractmethod
+    def create_search_params(self, ldap, api_instance, server=None):
+        """
+        create search base and filter
+        :param ldap: ldap connection
+        :param api_instance: API instance
+        :param server: server FQDN. if given, the method should generate
+        filter and search base matching only the status on this server
+        :returns: tuple of search base (a DN) and search filter
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_result_from_entries(self, entries):
+        """
+        Get role status from returned LDAP entries
+
+        :param entries: LDAPEntry objects returned by `search()`
+        :returns: list of dicts generated by `create_role_status_dict()`
+                  method
+        """
+        pass
+
+    def _fill_in_absent_masters(self, ldap2, api_instance, result):
+        """
+        get all masters on which the role is absent
+
+        :param ldap2: LDAP connection
+        :param api_instance: API instance
+        :param result: output of `get_result_from_entries` method
+
+        :returns: list of masters on which the role is absent
+        """
+        search_base = DN(api_instance.env.container_masters,
+                         api_instance.env.basedn)
+        search_filter = '(objectclass=ipaConfigObject)'
+        attrs_list = ['cn']
+
+        all_masters = ldap2.get_entries(
+            search_base,
+            filter=search_filter,
+            scope=SCOPE_ONELEVEL,
+            attrs_list=attrs_list)
+
+        all_master_cns = set(m['cn'][0] for m in all_masters)
+        enabled_configured_masters = set(r[u'server_server'] for r in result)
+
+        absent_masters = all_master_cns.difference(enabled_configured_masters)
+
+        return [self.create_role_status_dict(m, ABSENT) for m in
+                absent_masters]
+
+    def status(self, api_instance, server=None, attrs_list=("*",)):
+        """
+        probe and return status of the role either on single server or on the
+        whole topology
+
+        :param api_instance: API instance
+        :param server: server FQDN. If given, only the status of the role on
+                       this master will be returned
+        :returns: * 'enabled' if the role is enabled on the master
+                  * 'configured' if it is not enabled but has
+                    been configured by installer
+                  * 'absent' otherwise
+        """
+        ldap2 = api_instance.Backend.ldap2
+        search_base, search_filter = self.create_search_params(
+            ldap2, api_instance, server=server)
+
+        try:
+            entries = ldap2.get_entries(
+                search_base,
+                filter=search_filter,
+                attrs_list=attrs_list)
+        except errors.EmptyResult:
+            entries = []
+
+        if not entries and server is not None:
+            return [self.create_role_status_dict(server, ABSENT)]
+
+        result = self.get_result_from_entries(entries)
+
+        if server is None:
+            result.extend(
+                self._fill_in_absent_masters(ldap2, api_instance, result))
+
+        return sorted(result, key=lambda x: x[u'server_server'])
+
+
+class ServerAttribute(LDAPBasedProperty):
+    """
+    Class from which server attributes should be instantiated
+
+    :param associated_role_name: name of a role which must be enabled
+        on the provider
+    :param associated_service_name: name of LDAP service on which the
+        attribute is set. Does not need to belong to the service entries
+        of associate role
+    :param ipa_config_string_value: value of `ipaConfigString` attribute
+        associated with the presence of server attribute
+    """
+
+    def __init__(self, attr_name, name, associated_role_name,
+                 associated_service_name,
+                 ipa_config_string_value):
+        super(ServerAttribute, self).__init__(attr_name, name)
+
+        self.associated_role_name = associated_role_name
+        self.associated_service_name = associated_service_name
+        self.ipa_config_string_value = ipa_config_string_value
+
+    @property
+    def associated_role(self):
+        for inst in role_instances:
+            if self.associated_role_name == inst.attr_name:
+                return inst
+
+        raise NotImplementedError(
+            "{}: no valid associated role found".format(self.attr_name))
+
+    def create_search_filter(self, ldap):
+        """
+        Create search filter which matches LDAP data corresponding to the
+        attribute
+        """
+        svc_filter = ldap.make_filter_from_attr(
+            'cn', self.associated_service_name)
+
+        configstring_filter = ldap.make_filter_from_attr(
+            'ipaConfigString', self.ipa_config_string_value)
+        return ldap.combine_filters(
+            [svc_filter, configstring_filter], rules=ldap.MATCH_ALL)
+
+    def get(self, api_instance):
+        """
+        get the master which has the attribute set
+        :param api_instance: API instance
+        :returns: master FQDN
+        """
+        ldap2 = api_instance.Backend.ldap2
+        search_base = DN(api_instance.env.container_masters,
+                         api_instance.env.basedn)
+
+        search_filter = self.create_search_filter(ldap2)
+
+        try:
+            entries = ldap2.get_entries(search_base, filter=search_filter)
+        except errors.EmptyResult:
+            return
+
+        master_cn = entries[0].dn[1]['cn']
+
+        associated_role_providers = set(
+            self._get_assoc_role_providers(api_instance))
+
+        if master_cn not in associated_role_providers:
+            raise errors.ValidationError(
+                name=self.name,
+                error=_("all masters must have %(role)s role enabled" %
+                        {'role': self.associated_role.name})
+            )
+
+        return master_cn
+
+    def _get_master_dn(self, api_instance, server):
+        return DN(('cn', server), api_instance.env.container_masters,
+                  api_instance.env.basedn)
+
+    def _get_masters_service_entry(self, ldap, master_dn):
+        service_dn = DN(('cn', self.associated_service_name), master_dn)
+        return ldap.get_entry(service_dn)
+
+    def _add_attribute_to_svc_entry(self, ldap, service_entry):
+        """
+        add the server attribute to the entry of associated service
+
+        :param ldap: LDAP connection object
+        :param service_entry: associated service entry
+        """
+        ipa_config_string = service_entry.get('ipaConfigString', [])
+
+        ipa_config_string.append(self.ipa_config_string_value)
+
+        service_entry['ipaConfigString'] = ipa_config_string
+        ldap.update_entry(service_entry)
+
+    def _remove_attribute_from_svc_entry(self, ldap, service_entry):
+        """
+        remove the server attribute to the entry of associated service
+
+        single ipaConfigString attribute is case-insensitive, we must handle
+        arbitrary case of target value
+
+        :param ldap: LDAP connection object
+        :param service_entry: associated service entry
+        """
+        ipa_config_string = service_entry.get('ipaConfigString', [])
+
+        for value in ipa_config_string:
+            if value.lower() == self.ipa_config_string_value.lower():
+                service_entry['ipaConfigString'].remove(value)
+
+        ldap.update_entry(service_entry)
+
+    def _get_assoc_role_providers(self, api_instance):
+        """
+        get list of all servers on which the associated role is enabled
+        """
+        return [
+            r[u'server_server'] for r in self.associated_role.status(
+                api_instance) if r[u'status'] == ENABLED]
+
+    def _remove(self, api_instance, master):
+        """
+        remove attribute from the master
+
+        :param api_instance: API instance
+        :param master: master FQDN
+        """
+
+        ldap = api_instance.Backend.ldap2
+
+        master_dn = self._get_master_dn(api_instance, master)
+        service_entry = self._get_masters_service_entry(ldap, master_dn)
+        self._remove_attribute_from_svc_entry(ldap, service_entry)
+
+    def _add(self, api_instance, master):
+        """
+        add attribute to the master
+        :param api_instance: API instance
+        :param master: master FQDN
+
+        :raises: * errors.ValidationError if the associated role is not enabled
+                   on the master
+        """
+
+        assoc_role_providers = self._get_assoc_role_providers(api_instance)
+        ldap = api_instance.Backend.ldap2
+
+        if master not in assoc_role_providers:
+            raise errors.ValidationError(
+                name=master,
+                error=_("must have %(role)s role enabled" %
+                        {'role': self.associated_role.name})
+            )
+
+        master_dn = self._get_master_dn(api_instance, master)
+        service_entry = self._get_masters_service_entry(ldap, master_dn)
+        self._add_attribute_to_svc_entry(ldap, service_entry)
+
+    def set(self, api_instance, master):
+        """
+        set the attribute on master
+
+        :param api_instance: API instance
+        :param master: FQDN of the new master
+
+        the attribute is automatically unset from previous master if present
+
+        :raises: errors.EmptyModlist if the new masters is the same as
+                 the original on
+        """
+        old_master = self.get(api_instance)
+
+        if old_master == master:
+            raise errors.EmptyModlist
+
+        self._add(api_instance, master)
+
+        if old_master is not None:
+            self._remove(api_instance, old_master)
+
+
+_Service = namedtuple('Service', ['name', 'enabled'])
+
+
+class ServiceBasedRole(BaseServerRole):
+    """
+    class for all role instances whose status is defined by presence of one or
+    more entries in LDAP and/or their attributes
+    """
+
+    def __init__(self, attr_name, name, component_services):
+        super(ServiceBasedRole, self).__init__(attr_name, name)
+
+        self.component_services = component_services
+
+    def _validate_component_services(self, services):
+        svc_set = {s.name for s in services}
+        if svc_set != set(self.component_services):
+            raise ValueError(
+                "{}: Mismatch between component services and search result "
+                "(expected: {}, got: {})".format(
+                    self.__class__.__name__,
+                    ', '.join(sorted(self.component_services)),
+                    ', '.join(sorted(s.name for s in services))))
+
+    def _get_service(self, entry):
+        entry_cn = entry['cn'][0]
+
+        enabled = self._is_service_enabled(entry)
+
+        return _Service(name=entry_cn, enabled=enabled)
+
+    def _is_service_enabled(self, entry):
+        """
+        determine whether the service is enabled based on the presence of
+        enabledService attribute in ipaConfigString attribute.
+        Since the attribute is case-insensitive, we must first lowercase its
+        values and do the comparison afterwards.
+
+        :param entry: LDAPEntry of the service
+        :returns: True if the service entry is enabled, False otherwise
+        """
+        enabled_value = 'enabledservice'
+        ipaconfigstring_values = set(
+            e.lower() for e in entry.get('ipaConfigString', []))
+
+        return enabled_value in ipaconfigstring_values
+
+    def _get_services_by_masters(self, entries):
+        """
+        given list of entries, return a dictionary keyed by master FQDNs which
+        contains list of service entries belonging to the master
+        """
+        services_by_master = defaultdict(list)
+        for e in entries:
+            service = self._get_service(e)
+            master_cn = e.dn[1]['cn']
+
+            services_by_master[master_cn].append(service)
+
+        return services_by_master
+
+    def get_result_from_entries(self, entries):
+        result = []
+        services_by_master = self._get_services_by_masters(entries)
+        for master, services in services_by_master.items():
+            try:
+                self._validate_component_services(services)
+            except ValueError:
+                continue
+
+            status = (
+                ENABLED if all(s.enabled for s in services) else
+                CONFIGURED)
+
+            result.append(self.create_role_status_dict(master, status))
+
+        return result
+
+    def create_search_params(self, ldap, api_instance, server=None):
+        search_base = DN(api_instance.env.container_masters,
+                         api_instance.env.basedn)
+
+        search_filter = ldap.make_filter_from_attr(
+            'cn',
+            self.component_services,
+            rules=ldap.MATCH_ANY,
+            exact=True
+        )
+
+        if server is not None:
+            search_base = DN(('cn', server), search_base)
+
+        return search_base, search_filter
+
+    def status(self, api_instance, server=None):
+        return super(ServiceBasedRole, self).status(
+            api_instance, server=server, attrs_list=('ipaConfigString', 'cn'))
+
+
+class ADtrustBasedRole(BaseServerRole):
+    """
+    Class which should instantiate roles besed on membership in 'adtrust agent'
+    sysaccount group.
+    """
+
+    def get_result_from_entries(self, entries):
+        result = []
+
+        for e in entries:
+            result.append(
+                self.create_role_status_dict(e['fqdn'][0], ENABLED)
+            )
+        return result
+
+    def create_search_params(self, ldap, api_instance, server=None):
+        search_base = DN(
+            api_instance.env.container_host, api_instance.env.basedn)
+
+        search_filter = ldap.make_filter_from_attr(
+            "memberof",
+            DN(('cn', 'adtrust agents'), ('cn', 'sysaccounts'),
+               ('cn', 'etc'), api_instance.env.basedn)
+        )
+        if server is not None:
+            server_filter = ldap.make_filter_from_attr(
+                'fqdn',
+                server,
+                exact=True
+            )
+            search_filter = ldap.combine_filters(
+                [search_filter, server_filter],
+                rules=ldap.MATCH_ALL
+            )
+
+        return search_base, search_filter
+
+
+role_instances = (
+    ADtrustBasedRole(u"ad_trust_agent_server", u"AD trust agent"),
+    ServiceBasedRole(
+        u"ad_trust_controller_server",
+        u"AD trust controller",
+        component_services=['ADTRUST']
+    ),
+    ServiceBasedRole(
+        u"ca_server_server",
+        u"CA server",
+        component_services=['CA']
+    ),
+    ServiceBasedRole(
+        u"dns_server_server",
+        u"DNS server",
+        component_services=['DNS', 'DNSKeySync']
+    ),
+    ServiceBasedRole(
+        u"ipa_master_server",
+        u"IPA master",
+        component_services=['HTTP', 'KDC', 'KPASSWD']
+    ),
+    ServiceBasedRole(
+        u"kra_server_server",
+        u"KRA server",
+        component_services=['KRA']
+    ),
+)
+
+attribute_instances = (
+    ServerAttribute(
+        u"ca_renewal_master_server",
+        u"CA renewal master",
+        u"ca_server_server",
+        u"CA",
+        u"caRenewalMaster",
+    ),
+    ServerAttribute(
+        u"dnssec_key_master_server",
+        u"DNSSec key master",
+        u"dns_server_server",
+        u"DNSSEC",
+        u"dnssecKeyMaster",
+    ),
+)
-- 
2.5.5

From db000ba745d8594f19b7393792a441921d6883b3 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 19 May 2016 11:24:18 +0200
Subject: [PATCH 2/7] Server Roles: Backend plugin to query roles and
 attributes

`serverroles` backend consumes the role/attribute instances defined in
`ipaserver/servroles.py` module to provide low-level API for querying
role/attribute status in the topology. This plugin shall be used to implement
higher-level API commands.

https://www.freeipa.org/page/V4/Server_Roles
https://fedorahosted.org/freeipa/ticket/5181
---
 ipaserver/plugins/serverroles.py | 149 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 149 insertions(+)
 create mode 100644 ipaserver/plugins/serverroles.py

diff --git a/ipaserver/plugins/serverroles.py b/ipaserver/plugins/serverroles.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a44fca5a82038452a9e33d3df8919754b8bf2f3
--- /dev/null
+++ b/ipaserver/plugins/serverroles.py
@@ -0,0 +1,149 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+
+"""
+serverroles backend
+=======================================
+
+The `serverroles` backend has access to all roles and attributes stored in
+module-level lists exposed in `ipaserver/servroles.py` module. It uses these
+lists to populate populate its internal stores with instances of the
+roles/attributes. The information contained in them can be accessed by
+the following methods:
+
+    *api.Backend.serverroles.server_role_search(
+            server_server=None, role_servrole=None status=None)
+        search for roles matching the given substrings and return the status of
+        the matched roles. Optionally filter the result by role status. If
+        `server_erver` is not None, the search is limited to a single master.
+        Otherwise, the status is computed for all masters in the topology. If
+        `role_servrole` is None, the all configured roled are queried
+
+    *api.Backend.serverroles.server_role_retrieve(server_server, role_servrole)
+        retrieve the status of a single role on a given master
+
+    *api.Backend.serverroles.config_retrieve(role_servrole)
+        return a configuration object given role name. This object is a
+        dictionary containing a list of enabled masters and all attributes
+        associated with the role along with master(s) on which they are set.
+
+    *api.Backend.serverroles.config_update(**attrs_values)
+        update configuration object. Since server roles are currently
+        immutable, only attributes can be set
+
+Note that attribute/role names are searched/matched case-insensitively. Also
+note that the `serverroles` backend does not create/destroy any LDAP connection
+by itself, so make sure `ldap2` backend connections are taken care of
+in the calling code
+"""
+
+
+import six
+
+from ipalib import errors, _
+from ipalib.backend import Backend
+from ipalib.plugable import Registry
+from ipaserver.servroles import (attribute_instances, ENABLED, role_instances)
+
+
+if six.PY3:
+    unicode = str
+
+
+register = Registry()
+
+
+@register()
+class serverroles(Backend):
+    """
+    This Backend can be used to query various information about server roles
+    and attributes configured in the topology.
+    """
+
+    def __init__(self, api_instance):
+        super(serverroles, self).__init__(api_instance)
+
+        self.role_names = {
+            obj.name.lower(): obj for obj in role_instances}
+
+        self.attributes = {
+            attr.attr_name: attr for attr in attribute_instances}
+
+    def _get_role(self, role_name):
+        key = role_name.lower()
+
+        try:
+            return self.role_names[key]
+        except KeyError:
+            raise errors.NotFound(
+                reason=_("{role}: role not found".format(role=role_name)))
+
+    def _get_enabled_masters(self, role_name):
+        role = self._get_role(role_name)
+
+        enabled_masters = [
+            r[u'server_server'] for r in role.status(self.api, server=None) if
+            r[u'status'] == ENABLED]
+
+        return {role.attr_name: enabled_masters}
+
+    def _get_assoc_attributes(self, role_name):
+        role = self._get_role(role_name)
+        assoc_attributes = {
+            name: attr for name, attr in self.attributes.items() if
+            attr.associated_role is role}
+
+        if not assoc_attributes:
+            raise NotImplementedError(
+                "Role {} has no associated attribute to set".format(role.name))
+
+        return assoc_attributes
+
+    def server_role_search(self, server_server=None, role_servrole=None,
+                           status=None):
+        if role_servrole is None:
+            found_roles = self.role_names.values()
+        else:
+            try:
+                found_roles = [self._get_role(role_servrole)]
+            except errors.NotFound:
+                found_roles = []
+
+        result = []
+        for found_role in found_roles:
+            role_status = found_role.status(self.api, server=server_server)
+
+            result.extend(role_status)
+
+        if status is not None:
+            return [r for r in result if r[u'status'] == status]
+
+        return result
+
+    def server_role_retrieve(self, server_server, role_servrole):
+        return self._get_role(role_servrole).status(
+            self.api, server=server_server)
+
+    def config_retrieve(self, servrole):
+        result = self._get_enabled_masters(servrole)
+
+        try:
+            assoc_attributes = self._get_assoc_attributes(servrole)
+        except NotImplementedError:
+            return result
+
+        result.update(
+            {name: attr.get(self.api) for name, attr in
+             assoc_attributes.items()})
+
+        return result
+
+    def config_update(self, **attrs_values):
+        for attr, value in attrs_values.items():
+            try:
+                self.attributes[attr].set(self.api, value)
+            except KeyError:
+                raise errors.NotFound(
+                    reason=_('{attr}: no such attribute'.format(attr=attr)))
-- 
2.5.5

From 476d705f7ac3d82dbb8f328b63f1cafbd7943db6 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 19 May 2016 13:40:12 +0200
Subject: [PATCH 3/7] Test suite for `serverroles` backend

Tests retrieving roles/attributes and setting server attributes in various
scenarios.

https://fedorahosted.org/freeipa/ticket/5181
---
 ipatests/test_ipaserver/test_serverroles.py | 745 ++++++++++++++++++++++++++++
 1 file changed, 745 insertions(+)
 create mode 100644 ipatests/test_ipaserver/test_serverroles.py

diff --git a/ipatests/test_ipaserver/test_serverroles.py b/ipatests/test_ipaserver/test_serverroles.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef8cca3d978ddb0fec131c0d23dd8cf4816f5bd0
--- /dev/null
+++ b/ipatests/test_ipaserver/test_serverroles.py
@@ -0,0 +1,745 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+"""
+Tests for the serverroles backend
+"""
+
+from collections import namedtuple
+
+import ldap
+import pytest
+
+from ipalib import api, create_api, errors
+from ipapython.dn import DN
+from ipatests.util import MockLDAP
+
+
+def _make_service_entry_mods(enabled=True, other_config=None):
+    mods = {
+        b'objectClass': [b'top', b'nsContainer', b'ipaConfigObject'],
+    }
+    if enabled:
+        mods.update({b'ipaConfigString': [b'enabledService']})
+
+    if other_config is not None:
+        mods.setdefault(b'ipaConfigString', [])
+        mods[b'ipaConfigString'].extend(other_config)
+
+    return mods
+
+
+def _make_master_entry_mods(ca=False):
+    mods = {
+        b'objectClass': [
+            b'top',
+            b'nsContainer',
+            b'ipaReplTopoManagedServer',
+            b'ipaSupportedDomainLevelConfig',
+            b'ipaConfigObject',
+        ],
+        b'ipaMaxDomainLevel': [b'1'],
+        b'ipaMinDomainLevel': [b'0'],
+        b'ipaReplTopoManagedsuffix': [str(api.env.basedn)]
+    }
+    if ca:
+        mods[b'ipaReplTopoManagedsuffix'].append(b'o=ipaca')
+
+    return mods
+
+_adtrust_agents = DN(
+    ('cn', 'adtrust agents'),
+    ('cn', 'sysaccounts'),
+    ('cn', 'etc'),
+    api.env.basedn
+)
+
+
+master_data = {
+    'ca-dns-dnssec-keymaster': {
+        'services': {
+            'CA': {
+                'enabled': True,
+            },
+            'DNS': {
+                'enabled': True,
+            },
+            'DNSKeySync': {
+                'enabled': True,
+            },
+            'DNSSEC': {
+                'enabled': True,
+                'config': ['DNSSecKeyMaster']
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master', 'CA server', 'DNS server']
+        },
+        'expected_attributes': {'DNS server': 'dnssec_key_master_server'}
+    },
+    'ca-kra-renewal-master': {
+        'services': {
+            'CA': {
+                'enabled': True,
+                'config': ['caRenewalMaster']
+            },
+            'KRA': {
+                'enabled': True,
+            },
+        },
+        'expected_roles': {
+            'enabled': ['IPA master', 'CA server', 'KRA server']
+        },
+        'expected_attributes': {'CA server': 'ca_renewal_master_server'}
+    },
+    'dns-trust-agent': {
+        'services': {
+            'DNS': {
+                'enabled': True,
+            },
+            'DNSKeySync': {
+                'enabled': True,
+            }
+        },
+        'attributes': {
+            _adtrust_agents: {
+                'member': ['host']
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master', 'DNS server', 'AD trust agent']
+        }
+    },
+    'trust-agent': {
+        'attributes': {
+            _adtrust_agents: {
+                'member': ['host']
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master', 'AD trust agent']
+        }
+    },
+    'trust-controller-dns': {
+        'services': {
+            'ADTRUST': {
+                'enabled': True,
+            },
+            'DNS': {
+                'enabled': True,
+            },
+            'DNSKeySync': {
+                'enabled': True,
+            }
+        },
+        'attributes': {
+            _adtrust_agents: {
+                'member': ['host', 'cifs']
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master', 'AD trust agent', 'AD trust controller',
+                        'DNS server']
+        }
+    },
+    'trust-controller-ca': {
+        'services': {
+            'ADTRUST': {
+                'enabled': True,
+            },
+            'CA': {
+                'enabled': True,
+            },
+        },
+        'attributes': {
+            _adtrust_agents: {
+                'member': ['host', 'cifs']
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master', 'AD trust agent', 'AD trust controller',
+                        'CA server']
+        }
+    },
+    'configured-ca': {
+        'services': {
+            'CA': {
+                'enabled': False,
+            },
+        },
+        'expected_roles': {
+            'enabled': ['IPA master'],
+            'configured': ['CA server']
+        }
+    },
+    'configured-dns': {
+        'services': {
+            'DNS': {
+                'enabled': False,
+            },
+            'DNSKeySync': {
+                'enabled': False,
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master'],
+            'configured': ['DNS server']
+        }
+    },
+    'mixed-state-dns': {
+        'services': {
+            'DNS': {
+                'enabled': False
+            },
+            'DNSKeySync': {
+                'enabled': True
+            }
+        },
+        'expected_roles': {
+            'enabled': ['IPA master'],
+            'configured': ['DNS server']
+        }
+    },
+}
+
+
+class MockMasterTopology(object):
+    """
+    object that will set up and tear down entries in LDAP backend to mimic
+    a presence of real IPA masters with services running on them.
+    """
+
+    ipamaster_services = [u'KDC', u'HTTP', u'KPASSWD']
+
+    def __init__(self, api_instance, domain_data):
+        self.api = api_instance
+
+        self.domain = self.api.env.domain
+        self.domain_data = domain_data
+        self.masters_base = DN(
+            self.api.env.container_masters, self.api.env.basedn)
+
+        self.test_master_dn = DN(
+            ('cn', self.api.env.host), self.api.env.container_masters,
+            self.api.env.basedn)
+
+        self.ldap = MockLDAP()
+
+        self.existing_masters = {
+            m['cn'][0] for m in self.api.Command.server_find(
+                u'', sizelimit=0,
+                pkey_only=True,
+                no_members=True,
+                raw=True)['result']}
+
+        self.existing_attributes = self._check_test_host_attributes()
+
+    def iter_domain_data(self):
+        MasterData = namedtuple('MasterData',
+                                ['dn', 'fqdn', 'services', 'attrs'])
+        for name in self.domain_data:
+            fqdn = self.get_fqdn(name)
+            master_dn = self.get_master_dn(name)
+            master_services = self.domain_data[name].get('services', {})
+
+            master_attributes = self.domain_data[name].get('attributes', {})
+
+            yield MasterData(
+                dn=master_dn,
+                fqdn=fqdn,
+                services=master_services,
+                attrs=master_attributes
+            )
+
+    def get_fqdn(self, name):
+        return '.'.join([name, self.domain])
+
+    def get_master_dn(self, name):
+        return DN(('cn', self.get_fqdn(name)), self.masters_base)
+
+    def get_service_dn(self, name, master_dn):
+        return DN(('cn', name), master_dn)
+
+    def _add_host_entry(self, fqdn):
+        self.api.Command.host_add(fqdn, force=True)
+        self.api.Command.hostgroup_add_member(u'ipaservers', host=fqdn)
+
+    def _del_host_entry(self, fqdn):
+        try:
+            self.api.Command.host_del(fqdn)
+        except errors.NotFound:
+            pass
+
+    def _add_service_entry(self, service, fqdn):
+        return self.api.Command.service_add(
+            '/'.join([service, fqdn]),
+            force=True
+        )
+
+    def _del_service_entry(self, service, fqdn):
+        try:
+            self.api.Command.service_del(
+                '/'.join([service, fqdn]),
+            )
+        except errors.NotFound:
+            pass
+
+    def _add_svc_entries(self, master_dn, svc_desc):
+        self._add_ipamaster_services(master_dn)
+        for name in svc_desc:
+            svc_dn = self.get_service_dn(name, master_dn)
+            svc_mods = svc_desc[name]
+
+            self.ldap.add_entry(
+                str(svc_dn),
+                _make_service_entry_mods(
+                    enabled=svc_mods['enabled'],
+                    other_config=svc_mods.get('config', None)))
+
+    def _remove_svc_master_entries(self, master_dn):
+        try:
+            entries = self.ldap.connection.search_s(
+                str(master_dn), ldap.SCOPE_SUBTREE
+            )
+        except ldap.NO_SUCH_OBJECT:
+            return
+
+        if entries:
+            entries.sort(key=lambda x: len(x[0]), reverse=True)
+            for entry_dn, attrs in entries:
+                self.ldap.del_entry(str(entry_dn))
+
+    def _add_ipamaster_services(self, master_dn):
+        """
+        add all the service entries which are part of the IPA Master role
+        """
+        for svc_name in self.ipamaster_services:
+            svc_dn = self.get_service_dn(svc_name, master_dn)
+            self.ldap.add_entry(str(svc_dn), _make_service_entry_mods())
+
+    def _add_members(self, dn, fqdn, member_attrs):
+        entry, attrs = self.ldap.connection.search_s(
+            str(dn), ldap.SCOPE_SUBTREE)[0]
+        mods = []
+        value = attrs.get('member', [])
+        mod_op = ldap.MOD_REPLACE
+        if not value:
+            mod_op = ldap.MOD_ADD
+
+        for a in member_attrs:
+
+            if a == 'host':
+                value.append(
+                    str(self.api.Object.host.get_dn(fqdn)))
+            else:
+                result = self._add_service_entry(a, fqdn)['result']
+                value.append(str(result['dn']))
+
+        mods.append(
+            (mod_op, 'member', value)
+        )
+
+        self.ldap.connection.modify_s(str(dn), mods)
+
+    def _remove_members(self, dn, fqdn, member_attrs):
+        entry, attrs = self.ldap.connection.search_s(
+            str(dn), ldap.SCOPE_SUBTREE)[0]
+        mods = []
+        for a in member_attrs:
+            value = set(attrs.get('member', []))
+            if not value:
+                continue
+
+            if a == 'host':
+                try:
+                    value.remove(
+                        str(self.api.Object.host.get_dn(fqdn)))
+                except KeyError:
+                    pass
+            else:
+                try:
+                    value.remove(
+                        str(self.api.Object.service.get_dn(
+                            '/'.join([a, fqdn]))))
+                except KeyError:
+                    pass
+                self._del_service_entry(a, fqdn)
+
+        mods.append(
+            (ldap.MOD_REPLACE, 'member', list(value))
+        )
+
+        try:
+            self.ldap.connection.modify_s(str(dn), mods)
+        except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE):
+            pass
+
+    def _check_test_host_attributes(self):
+        existing_attributes = set()
+
+        for service, value, attr_name in (
+                ('CA', 'caRenewalMaster', 'ca renewal master'),
+                ('DNSSEC', 'DNSSecKeyMaster', 'dnssec key master')):
+
+            svc_dn = DN(('cn', service), self.test_master_dn)
+            try:
+                svc_entry = self.api.Backend.ldap2.get_entry(svc_dn)
+            except errors.NotFound:
+                continue
+            else:
+                config_string_val = svc_entry.get('ipaConfigString', [])
+
+                if value in config_string_val:
+                    existing_attributes.add(attr_name)
+
+        return existing_attributes
+
+    def _remove_ca_renewal_master(self):
+        if 'ca renewal master' not in self.existing_attributes:
+            return
+
+        ca_dn = DN(('cn', 'CA'), self.test_master_dn)
+        ca_entry = self.api.Backend.ldap2.get_entry(ca_dn)
+
+        config_string_val = ca_entry.get('ipaConfigString', [])
+        try:
+            config_string_val.remove('caRenewalMaster')
+        except KeyError:
+            return
+
+        ca_entry.update({'ipaConfigString': config_string_val})
+        self.api.Backend.ldap2.update_entry(ca_entry)
+
+    def _restore_ca_renewal_master(self):
+        if 'ca renewal master' not in self.existing_attributes:
+            return
+
+        ca_dn = DN(('cn', 'CA'), self.test_master_dn)
+        ca_entry = self.api.Backend.ldap2.get_entry(ca_dn)
+
+        config_string_val = ca_entry.get('ipaConfigString', [])
+        config_string_val.append('caRenewalMaster')
+
+        ca_entry.update({'ipaConfigString': config_string_val})
+        self.api.Backend.ldap2.update_entry(ca_entry)
+
+    def setup_data(self):
+        self._remove_ca_renewal_master()
+        for master_data in self.iter_domain_data():
+            # create host
+            self._add_host_entry(master_data.fqdn)
+
+            # create master
+            self.ldap.add_entry(
+                str(master_data.dn), _make_master_entry_mods(
+                    ca='CA' in master_data.services))
+
+            # now add service entries
+            self._add_svc_entries(master_data.dn, master_data.services)
+
+            # optionally add some attributes required e.g. by AD trust roles
+            for entry_dn, attrs in master_data.attrs.items():
+                if 'member' in attrs:
+                    self._add_members(
+                        entry_dn,
+                        master_data.fqdn,
+                        attrs['member']
+                    )
+
+    def teardown_data(self):
+        self._restore_ca_renewal_master()
+        for master_data in self.iter_domain_data():
+            # first remove the master entries and service containers
+            self._remove_svc_master_entries(master_data.dn)
+
+            # optionally clean up leftover attributes
+            for entry_dn, attrs in master_data.attrs.items():
+                if 'member' in attrs:
+                    self._remove_members(
+                        entry_dn,
+                        master_data.fqdn,
+                        attrs['member'],
+                    )
+
+            # finally remove host entry
+            self._del_host_entry(master_data.fqdn)
+
+
+@pytest.fixture(scope='module')
+def mock_api(request):
+    test_api = create_api(mode=None)
+    test_api.bootstrap(in_server=True, in_tree=True)
+    test_api.finalize()
+
+    if not test_api.Backend.ldap2.isconnected():
+        test_api.Backend.ldap2.connect()
+
+    return test_api
+
+
+@pytest.fixture(scope='module')
+def mock_masters(request, mock_api):
+    """
+    Populate the LDAP backend with test data
+    """
+
+    if not api.Backend.rpcclient.isconnected():
+        api.Backend.rpcclient.connect()
+
+    master_topo = MockMasterTopology(mock_api, master_data)
+
+    def finalize():
+        master_topo.teardown_data()
+        if api.Backend.rpcclient.isconnected():
+            api.Backend.rpcclient.disconnect()
+
+    request.addfinalizer(finalize)
+
+    master_topo.setup_data()
+
+    return master_topo
+
+
+def enabled_role_iter(master_data):
+    for m, data in master_data.items():
+        for role in data['expected_roles']['enabled']:
+            yield m, role
+
+
+def provided_role_iter(master_data):
+    for m, data in master_data.items():
+        yield m, data['expected_roles']['enabled']
+
+
+def configured_role_iter(master_data):
+    for m, data in master_data.items():
+        if 'configured' in data['expected_roles']:
+            for role in data['expected_roles']['configured']:
+                yield m, role
+
+
+def role_provider_iter(master_data):
+    result = {}
+    for m, data in master_data.items():
+        for role in data['expected_roles']['enabled']:
+            if role not in result:
+                result[role] = []
+
+            result[role].append(m)
+
+    for role_name, masters in result.items():
+        yield role_name, masters
+
+
+def attribute_masters_iter(master_data):
+    for m, data in master_data.items():
+        if 'expected_attributes' in data:
+            for assoc_role, attr in data['expected_attributes'].items():
+                yield m, assoc_role, attr
+
+
+def dns_servers_iter(master_data):
+    for m, data in master_data.items():
+        if "DNS server" in data['expected_roles']['enabled']:
+            yield m
+
+
+@pytest.fixture(params=list(enabled_role_iter(master_data)),
+                ids=['role: {}, master: {}, enabled'.format(role, m)
+                     for m, role in enabled_role_iter(master_data)])
+def enabled_role(request):
+    return request.param
+
+
+@pytest.fixture(params=list(provided_role_iter(master_data)),
+                ids=["{}: {}".format(m, ', '.join(roles)) for m, roles in
+                     provided_role_iter(master_data)])
+def provided_roles(request):
+    return request.param
+
+
+@pytest.fixture(params=list(configured_role_iter(master_data)),
+                ids=['role: {}, master: {}, configured'.format(role, m)
+                     for m, role in configured_role_iter(master_data)])
+def configured_role(request):
+    return request.param
+
+
+@pytest.fixture(params=list(role_provider_iter(master_data)),
+                ids=['{} providers'.format(role_name)
+                     for role_name, m in
+                     role_provider_iter(master_data)])
+def role_providers(request):
+    return request.param
+
+
+@pytest.fixture(params=list(attribute_masters_iter(master_data)),
+                ids=['{} of {}: {}'.format(attr, role, m) for m, role, attr in
+                     attribute_masters_iter(master_data)])
+def attribute_providers(request):
+    return request.param
+
+
+@pytest.fixture(params=list(dns_servers_iter(master_data)),
+                ids=list(dns_servers_iter(master_data)))
+def dns_server(request):
+    return request.param
+
+
+class TestServerRoleStatusRetrieval(object):
+    def retrieve_role(self, master, role, mock_api, mock_masters):
+        fqdn = mock_masters.get_fqdn(master)
+        return mock_api.Backend.serverroles.server_role_retrieve(
+            server_server=fqdn, role_servrole=role)
+
+    def find_role(self, role_name, mock_api, mock_masters, master=None):
+        if master is not None:
+            hostname = mock_masters.get_fqdn(master)
+        else:
+            hostname = None
+
+        result = mock_api.Backend.serverroles.server_role_search(
+            server_server=hostname,
+            role_servrole=role_name)
+
+        return [
+            r for r in result if r[u'server_server'] not in
+            mock_masters.existing_masters]
+
+    def get_enabled_roles_on_master(self, master, mock_api, mock_masters):
+        fqdn = mock_masters.get_fqdn(master)
+        result = mock_api.Backend.serverroles.server_role_search(
+            server_server=fqdn, role_servrole=None, status=u'enabled'
+        )
+        return sorted(set(r[u'role_servrole'] for r in result))
+
+    def get_masters_with_enabled_role(self, role_name, mock_api, mock_masters):
+        result = mock_api.Backend.serverroles.server_role_search(
+            server_server=None, role_servrole=role_name)
+
+        return sorted(
+            r[u'server_server'] for r in result if
+            r[u'status'] == u'enabled' and r[u'server_server'] not in
+            mock_masters.existing_masters)
+
+    def test_listing_of_enabled_role(
+            self, mock_api, mock_masters, enabled_role):
+        master, role_name = enabled_role
+        result = self.retrieve_role(master, role_name, mock_api, mock_masters)
+
+        assert result[0][u'status'] == u'enabled'
+
+    def test_listing_of_configured_role(
+            self, mock_api, mock_masters, configured_role):
+        master, role_name = configured_role
+        result = self.retrieve_role(master, role_name, mock_api, mock_masters)
+
+        assert result[0][u'status'] == u'configured'
+
+    def test_role_providers(
+            self, mock_api, mock_masters, role_providers):
+        role_name, providers = role_providers
+        expected_masters = sorted(mock_masters.get_fqdn(m) for m in providers)
+
+        actual_masters = self.get_masters_with_enabled_role(
+            role_name, mock_api, mock_masters)
+
+        assert expected_masters == actual_masters
+
+    def test_provided_roles_on_master(
+            self, mock_api, mock_masters, provided_roles):
+        master, expected_roles = provided_roles
+        expected_roles.sort()
+        actual_roles = self.get_enabled_roles_on_master(
+            master, mock_api, mock_masters)
+        assert expected_roles == actual_roles
+
+    def test_unknown_role_status_raises_notfound(self, mock_api, mock_masters):
+        unknown_role = 'IAP maestr'
+        fqdn = mock_masters.get_fqdn('ca-dns-dnssec-keymaster')
+        with pytest.raises(errors.NotFound):
+            mock_api.Backend.serverroles.server_role_retrieve(
+                fqdn, unknown_role)
+
+    def test_no_servrole_queries_all_roles_on_server(self, mock_api,
+                                                     mock_masters):
+        master_name = 'ca-dns-dnssec-keymaster'
+        enabled_roles = master_data[master_name]['expected_roles']['enabled']
+        result = self.find_role(None, mock_api, mock_masters,
+                                master=master_name)
+
+        for r in result:
+            if r[u'role_servrole'] in enabled_roles:
+                assert r[u'status'] == u'enabled'
+            else:
+                assert r[u'status'] == u'absent'
+
+    def test_invalid_substring_search_returns_nothing(self, mock_api,
+                                                      mock_masters):
+        invalid_substr = 'fwfgbb'
+
+        assert (not self.find_role(invalid_substr, mock_api, mock_masters,
+                                   'ca-dns-dnssec-keymaster'))
+
+
+class TestServerAttributes(object):
+    def config_retrieve(self, assoc_role_name, mock_api):
+        return mock_api.Backend.serverroles.config_retrieve(
+            assoc_role_name)
+
+    def config_update(self, mock_api, **attrs_values):
+        return mock_api.Backend.serverroles.config_update(**attrs_values)
+
+    def test_attribute_master(self, mock_api, mock_masters,
+                              attribute_providers):
+        master, assoc_role, attr_name = attribute_providers
+        fqdn = mock_masters.get_fqdn(master)
+        actual_attr_masters = self.config_retrieve(
+            assoc_role, mock_api)[attr_name]
+
+        assert actual_attr_masters == fqdn
+
+    def test_set_attribute_on_the_same_provider_raises_emptymodlist(
+            self, mock_api, mock_masters):
+        attr_name = "ca_renewal_master_server"
+        role_name = "CA server"
+
+        existing_renewal_master = self.config_retrieve(
+            role_name, mock_api)[attr_name]
+
+        with pytest.raises(errors.EmptyModlist):
+            self.config_update(
+                mock_api, **{attr_name: existing_renewal_master})
+
+    def test_set_attribute_on_master_without_assoc_role_raises_validationerror(
+            self, mock_api, mock_masters):
+        attr_name = "ca_renewal_master_server"
+
+        non_ca_fqdn = mock_masters.get_fqdn('trust-controller-dns')
+
+        with pytest.raises(errors.ValidationError):
+            self.config_update(mock_api, **{attr_name: non_ca_fqdn})
+
+    def test_set_unknown_attribute_on_master_raises_notfound(
+            self, mock_api, mock_masters):
+        attr_name = "ca_renuwal_maztah"
+        fqdn = mock_masters.get_fqdn('trust-controller-ca')
+
+        with pytest.raises(errors.NotFound):
+            self.config_update(mock_api, **{attr_name: fqdn})
+
+    def test_set_ca_renewal_master_on_other_ca_and_back(self, mock_api,
+                                                        mock_masters):
+        attr_name = "ca_renewal_master_server"
+        role_name = "CA server"
+        original_renewal_master = self.config_retrieve(
+            role_name, mock_api)[attr_name]
+
+        other_ca_server = mock_masters.get_fqdn('trust-controller-ca')
+
+        for host in (other_ca_server, original_renewal_master):
+            self.config_update(mock_api, **{attr_name: host})
+
+            assert (
+                self.config_retrieve(role_name, mock_api)[attr_name] == host)
-- 
2.5.5

From b8b0539edb703f993b17cf680c1601cc6778344f Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:18:38 +0200
Subject: [PATCH 4/7] Server Roles: public API for server roles

This patch implements the `serverroles` API plugin which introduces the
following commands:

    * server-role-show SERVER ROLE: show status of a single role on a server
    * server-role-find [--server SERVER [--role SERVROLE [--status=STATUS]]]:
      find role(s) SERVROLE and return their status on IPA
      masters. If --server option is given, the query is limited to this
      server. --status options filters the output by status [enabled vs.
      configurer vs. absent]

https://fedorahosted.org/freeipa/ticket/5181
http://www.freeipa.org/page/V4/Server_Roles
---
 API.txt                         |  23 ++++++
 VERSION                         |   4 +-
 ipaserver/plugins/serverrole.py | 178 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 203 insertions(+), 2 deletions(-)
 create mode 100644 ipaserver/plugins/serverrole.py

diff --git a/API.txt b/API.txt
index f17093022d54d5cd0ccbf1863f6def0589bbf8c9..a76b71bc556f013cd4100d64bcb24100331f65be 100644
--- a/API.txt
+++ b/API.txt
@@ -4043,6 +4043,29 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: server_role_find
+args: 1,6,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('role_servrole?', autofill=False, cli_name='role')
+option: Str('server_server?', autofill=False, cli_name='server')
+option: StrEnum('status?', autofill=False, cli_name='status', default=u'enabled', values=[u'enabled', u'configured', u'absent'])
+option: Str('version?')
+output: Output('count', type=[<type 'int'>])
+output: ListOfEntries('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: Output('truncated', type=[<type 'bool'>])
+command: server_role_show
+args: 2,3,3
+arg: Str('server_server', cli_name='server')
+arg: Str('role_servrole', cli_name='role')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: server_show
 args: 1,5,3
 arg: Str('cn', cli_name='name')
diff --git a/VERSION b/VERSION
index 4ada7467a646b4b7162fd56248399a65f8600663..9e84512304e21745ca0032c67668c8f5702f6f9b 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=176
-# Last change: mbasti - location-show: list servers in the location
+IPA_API_VERSION_MINOR=177
+# Last change: mbabinsk - Server Roles: public API for server roles
diff --git a/ipaserver/plugins/serverrole.py b/ipaserver/plugins/serverrole.py
new file mode 100644
index 0000000000000000000000000000000000000000..91f7e7a51dd49e2a7eaa5b079fff5722c49c67dd
--- /dev/null
+++ b/ipaserver/plugins/serverrole.py
@@ -0,0 +1,178 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+from ipalib.crud import Retrieve, Search
+from ipalib.errors import NotFound
+from ipalib.frontend import Object
+from ipalib.parameters import Int, Str, StrEnum
+from ipalib.plugable import Registry
+from ipalib import _, ngettext
+
+
+__doc__ = _("""
+IPA server roles
+""") + _("""
+Get status of roles (DNS server, CA, etc. )provided by IPA masters.
+""") + _("""
+EXAMPLES:
+""") + _("""
+  Show status of 'DNS server' role on a server:
+    ipa server-role-show ipa.example.com "DNS server"
+""") + _("""
+  Show status of all roles containing 'AD' on a server:
+    ipa server-role-find --server ipa.example.com --role='AD'
+""") + _("""
+  Show status of all configured roles on a server:
+    ipa server-role-find ipa.example.com
+""")
+
+
+register = Registry()
+
+
+@register()
+class server_role(Object):
+    """
+    association between certain role (e.g. DNS server) and its status with
+    an IPA master
+    """
+    backend_name = 'serverroles'
+    object_name = _('server role')
+    object_name_plural = _('server roles')
+    default_attributes = [
+        'role', 'status'
+    ]
+    label = _('IPA Server Roles')
+    label_singular = _('IPA Server Role')
+
+    takes_params = (
+        Str(
+            'server_server',
+            cli_name='server',
+            label=_('Server name'),
+            doc=_('IPA server hostname'),
+        ),
+        Str(
+            'role_servrole',
+            cli_name='role',
+            label=_("Role name"),
+            doc=_("IPA server role name"),
+            flags={u'virtual_attribute'}
+        ),
+        StrEnum(
+            'status?',
+            cli_name='status',
+            label=_('Role status'),
+            doc=_('Status of the role'),
+            values=(u'enabled', u'configured', u'absent'),
+            default=u'enabled',
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        )
+    )
+
+    def ensure_master_exists(self, fqdn):
+        server_obj = self.api.Object.server
+        try:
+            server_obj.get_dn_if_exists(fqdn)
+        except NotFound:
+            server_obj.handle_not_found(fqdn)
+
+
+@register()
+class server_role_show(Retrieve):
+    __doc__ = _('Show role status on a server')
+
+    obj_name = 'server_role'
+    attr_name = 'show'
+
+    def get_args(self):
+        for arg in super(server_role_show, self).get_args():
+            yield arg
+
+        for param in self.obj.params():
+            if param.name != u'status':
+                yield param.clone()
+
+    def execute(self, *keys, **options):
+        self.obj.ensure_master_exists(keys[0])
+
+        role_status = self.obj.backend.server_role_retrieve(
+            server_server=keys[0], role_servrole=keys[1])
+
+        return dict(result=role_status[0], value=None)
+
+
+@register()
+class server_role_find(Search):
+    __doc__ = _('Find a server role on a server(s)')
+
+    obj_name = 'server_role'
+    attr_name = 'find'
+
+    msg_summary = ngettext('%(count)s server role matched',
+                           '%(count)s server roles matched', 0)
+    takes_options = Search.takes_options + (
+        Int(
+            'timelimit?',
+            label=_('Time Limit'),
+            doc=_('Time limit of search in seconds (0 is unlimited)'),
+            flags=['no_display'],
+            minvalue=0,
+            autofill=False,
+        ),
+        Int(
+            'sizelimit?',
+            label=_('Size Limit'),
+            doc=_('Maximum number of entries returned (0 is unlimited)'),
+            flags=['no_display'],
+            minvalue=0,
+            autofill=False,
+        ),
+    )
+
+    def execute(self, *keys, **options):
+        if keys:
+            return dict(
+                result=[],
+                count=0,
+                truncated=False
+            )
+
+        server = options.get('server_server', None)
+        role_name = options.get('role_servrole', None)
+        status = options.get('status', None)
+
+        if server is not None:
+            self.obj.ensure_master_exists(server)
+
+        role_status = self.obj.backend.server_role_search(
+            server_server=server,
+            role_servrole=role_name,
+            status=status)
+
+        result = [
+            r for r in role_status if r[u'role_servrole'] != "IPA master"]
+        return dict(
+            result=result,
+            count=len(result),
+            truncated=False,
+        )
+
+
+@register()
+class servrole(Object):
+    """
+    Server role object
+    """
+    object_name = _('role')
+    object_name_plural = _('roles')
+    takes_params = (
+        Str(
+            'name',
+            primary_key=True,
+            label=_("Role name"),
+            doc=_("IPA role name"),
+            flags=(u'virtual_attribute',)
+        )
+    )
-- 
2.5.5

From 7847abcae13ac5ab52a415a9a9e7733753f06820 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:27:38 +0200
Subject: [PATCH 5/7] Server Roles: make server-{show,find} utilize role
 information

server-show command will now display list of roles enabled on the master
(unless `--raw` is given).

server-find gained `--servroles` options which facilitate search for server
having one or more enabled roles.

http://www.freeipa.org/page/V4/Server_Roles
https://fedorahosted.org/freeipa/ticket/5181
---
 API.txt                     |  4 ++-
 VERSION                     |  4 +--
 ipaserver/plugins/server.py | 80 ++++++++++++++++++++++++++++++++++++++++++---
 3 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/API.txt b/API.txt
index a76b71bc556f013cd4100d64bcb24100331f65be..8ecea435e969e349e93531d1b0fb6144a01131e6 100644
--- a/API.txt
+++ b/API.txt
@@ -4007,7 +4007,7 @@ output: Output('result', type=[<type 'dict'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: ListOfPrimaryKeys('value')
 command: server_find
-args: 1,14,4
+args: 1,16,4
 arg: Str('criteria?')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('cn?', autofill=False, cli_name='name')
@@ -4015,10 +4015,12 @@ option: DNSNameParam('in_location*', cli_name='in_locations')
 option: Int('ipamaxdomainlevel?', autofill=False, cli_name='maxlevel')
 option: Int('ipamindomainlevel?', autofill=False, cli_name='minlevel')
 option: Flag('no_members', autofill=True, default=True)
+option: Str('no_servrole*', cli_name='no_servroles')
 option: Str('no_topologysuffix*', cli_name='no_topologysuffixes')
 option: DNSNameParam('not_in_location*', cli_name='not_in_locations')
 option: Flag('pkey_only?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('servrole*', cli_name='servroles')
 option: Int('sizelimit?', autofill=False)
 option: Int('timelimit?', autofill=False)
 option: Str('topologysuffix*', cli_name='topologysuffixes')
diff --git a/VERSION b/VERSION
index 9e84512304e21745ca0032c67668c8f5702f6f9b..5214f24a1ff525529a7eedd69665ea9033194467 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=177
-# Last change: mbabinsk - Server Roles: public API for server roles
+IPA_API_VERSION_MINOR=178
+# Last change: mbabinsk - Server Roles: make server-{show,find} utilize role information
diff --git a/ipaserver/plugins/server.py b/ipaserver/plugins/server.py
index 511f8135b14edb8d6af7fc6a0a04ccd8a110a00b..de54e4340a58d62284906aae91bc4f7192c448f1 100644
--- a/ipaserver/plugins/server.py
+++ b/ipaserver/plugins/server.py
@@ -20,7 +20,7 @@ from ipalib import _, ngettext
 from ipalib import output
 from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
-
+from ipaserver.servroles import ENABLED
 
 __doc__ = _("""
 IPA servers
@@ -59,10 +59,12 @@ class server(LDAPObject):
     attribute_members = {
         'iparepltopomanagedsuffix': ['topologysuffix'],
         'ipalocation': ['location'],
+        'role': ['servrole'],
     }
     relationships = {
         'iparepltopomanagedsuffix': ('Managed', '', 'no_'),
         'ipalocation': ('IPA', 'in_', 'not_in_'),
+        'role': ('Enabled', '', 'no_'),
     }
     permission_filter_objectclasses = ['ipaLocationMember']
     managed_permissions = {
@@ -74,6 +76,7 @@ class server(LDAPObject):
             'default_privileges': {'DNS Administrators'},
         },
     }
+
     takes_params = (
         Str(
             'cn',
@@ -127,7 +130,13 @@ class server(LDAPObject):
             label=_('Location relative weight'),
             doc=_('Location relative weight for server (counts per location)'),
             flags={'virtual_attribute','no_create', 'no_update', 'no_search'},
-        )
+        ),
+        Str(
+            'enabled_role_servrole*',
+            label=_('Enabled server roles'),
+            doc=_('List of enabled roles'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}
+        ),
     )
 
     def _get_suffixes(self):
@@ -172,6 +181,17 @@ class server(LDAPObject):
         if converted_locations:
             entry_attrs['ipalocation_location'] = converted_locations
 
+    def get_enabled_roles(self, entry_attrs, **options):
+        if options.get('raw', False) or options.get('no_members', False):
+            return
+
+        enabled_roles = self.api.Command.server_role_find(
+            server_server=entry_attrs['cn'][0], status=ENABLED)['result']
+
+        enabled_role_names = [r[u'role_servrole'] for r in enabled_roles]
+
+        entry_attrs['enabled_role_servrole'] = enabled_role_names
+
 
 @register()
 class server_mod(LDAPUpdate):
@@ -206,8 +226,10 @@ class server_mod(LDAPUpdate):
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         assert isinstance(dn, DN)
         self.obj.convert_location(entry_attrs, **options)
+        self.obj.get_enabled_roles(entry_attrs)
         return dn
 
+
 @register()
 class server_find(LDAPSearch):
     __doc__ = _('Search for IPA servers.')
@@ -216,7 +238,8 @@ class server_find(LDAPSearch):
         '%(count)d IPA server matched',
         '%(count)d IPA servers matched', 0
     )
-    member_attributes = ['iparepltopomanagedsuffix', 'ipalocation']
+
+    member_attributes = ['iparepltopomanagedsuffix', 'ipalocation', 'role']
 
     def args_options_2_entry(self, *args, **options):
         kw = super(server_find, self).args_options_2_entry(
@@ -230,13 +253,52 @@ class server_find(LDAPSearch):
                 option = option.clone(cli_name='topologysuffixes')
             elif option.name == 'no_topologysuffix':
                 option = option.clone(cli_name='no_topologysuffixes')
+            # we do not want to test negative membership for roles
+            # hide it from CLI
+            elif option.name == 'no_servrole':
+                option = option.clone(flags={'no_option'})
             yield option
 
     def get_member_filter(self, ldap, **options):
         options.pop('topologysuffix', None)
         options.pop('no_topologysuffix', None)
 
-        return super(server_find, self).get_member_filter(ldap, **options)
+        options.pop('servrole', None)
+        options.pop('no_servrole', None)
+
+        return super(server_find, self).get_member_filter(
+            ldap, **options)
+
+    def _get_enabled_servrole_filter(self, ldap, servroles):
+        """
+        return a filter matching any master which has all the specified roles
+        enabled.
+        """
+        def _get_masters_with_enabled_servrole(role):
+            role_status = self.api.Command.server_role_find(
+                server_server=None,
+                role_servrole=role,
+                status=ENABLED)['result']
+
+            return set(
+                r[u'server_server'] for r in role_status)
+
+        enabled_masters = _get_masters_with_enabled_servrole(
+            servroles[0])
+
+        for role in servroles[1:]:
+            enabled_masters.intersection_update(
+                _get_masters_with_enabled_servrole(role)
+            )
+
+        if not enabled_masters:
+            return '(!(objectclass=*))'
+
+        return ldap.make_filter_from_attr(
+            'cn',
+            list(enabled_masters),
+            rules=ldap.MATCH_ANY
+        )
 
     def pre_callback(self, ldap, filters, attrs_list, base_dn, scope,
                      *args, **options):
@@ -273,6 +335,12 @@ class server_find(LDAPSearch):
                     (filters, filter), ldap.MATCH_ALL
                 )
 
+        if options.get('servrole', []):
+            servrole_filter = self._get_enabled_servrole_filter(
+                ldap, options['servrole'])
+            filters = ldap.combine_filters(
+                (filters, servrole_filter), ldap.MATCH_ALL)
+
         return (filters, base_dn, scope)
 
     def post_callback(self, ldap, entries, truncated, *args, **options):
@@ -283,6 +351,7 @@ class server_find(LDAPSearch):
 
         for entry in entries:
             self.obj.convert_location(entry, **options)
+            self.obj.get_enabled_roles(entry, **options)
         return truncated
 
 
@@ -294,7 +363,10 @@ class server_show(LDAPRetrieve):
         if not options.get('raw', False):
             suffixes = self.obj._get_suffixes()
             self.obj._apply_suffixes(entry, suffixes)
+
         self.obj.convert_location(entry, **options)
+        self.obj.get_enabled_roles(entry, **options)
+
         return dn
 
 
-- 
2.5.5

From 9a489a5257b4dd9311f2afaa404d3b8f4a08d888 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:42:01 +0200
Subject: [PATCH 6/7] Server Roles: make *config-show consume relevant
 roles/attributes

This patch modifies config objects so that the roles/attributes relevant to
the configuration are shown in the output:

* config-{show,mod} will show list of all IPA masters, CA servers and CA
  renewal master

* dnsconfig-{show,mod} will list all DNS server and DNS key master

* trustconfig-{show,mod} will list all AD trust controllers and agents

* vaultconfig-show will list all Key Recovery Agents

http://www.freeipa.org/page/V4/Server_Roles
https://fedorahosted.org/freeipa/ticket/5181
---
 ipaserver/install/bindinstance.py |  8 ++++++--
 ipaserver/plugins/config.py       | 35 +++++++++++++++++++++++++++++++++++
 ipaserver/plugins/dns.py          | 34 +++++++++++++++++++++++++++++++++-
 ipaserver/plugins/trust.py        | 31 +++++++++++++++++++++++++++++++
 ipaserver/plugins/vault.py        | 15 ++++++++++++---
 5 files changed, 117 insertions(+), 6 deletions(-)

diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index afcb6b0c10e54b1aae975fd1e81f37144e6b97ed..78e75359266bbefe7954242b98922272fb0c9194 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -1230,8 +1230,12 @@ class BindInstance(service.Service):
         set and thus overrides his configured options in named.conf.
         """
         result = self.api.Command.dnsconfig_show()
-        global_conf_set = any(param in result['result'] for \
-                              param in self.api.Object['dnsconfig'].params)
+
+        global_conf_set = any(
+            param.name in result['result'] for param in
+            self.api.Object['dnsconfig'].params() if
+            u'virtual_attribute' not in param.flags
+        )
 
         if not global_conf_set:
             print("Global DNS configuration in LDAP server is empty")
diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py
index 46a40ddf7810bac723563cee8d174d8499d21465..95d1d6409abd43dbe7771d4403097c8c354a406e 100644
--- a/ipaserver/plugins/config.py
+++ b/ipaserver/plugins/config.py
@@ -227,11 +227,40 @@ class config(LDAPObject):
             doc=_('Default types of supported user authentication'),
             values=(u'password', u'radius', u'otp', u'disabled'),
         ),
+        Str(
+            'ipa_master_server*',
+            label=_('IPA masters'),
+            doc=_('List of all IPA masters'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        ),
+        Str(
+            'ca_server_server*',
+            label=_('IPA CA servers'),
+            doc=_('IPA servers configured as certificate authority'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        ),
+        Str(
+            'ca_renewal_master_server?',
+            label=_('IPA CA renewal master'),
+            doc=_('Renewal master for IPA certificate authority'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        )
     )
 
     def get_dn(self, *keys, **kwargs):
         return DN(('cn', 'ipaconfig'), ('cn', 'etc'), api.env.basedn)
 
+    def show_servroles_attributes(self, entry_attrs, **options):
+        if options.get('raw', False):
+            return
+
+        backend = self.api.Backend.serverroles
+
+        ca_config = backend.config_retrieve("CA server")
+        master_config = backend.config_retrieve("IPA master")
+
+        entry_attrs.update(ca_config)
+        entry_attrs.update(master_config)
 
 
 @register()
@@ -350,9 +379,15 @@ class config_mod(LDAPUpdate):
 
         return dn
 
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        self.obj.show_servroles_attributes(entry_attrs, **options)
+        return dn
 
 
 @register()
 class config_show(LDAPRetrieve):
     __doc__ = _('Show the current configuration.')
 
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        self.obj.show_servroles_attributes(entry_attrs, **options)
+        return dn
diff --git a/ipaserver/plugins/dns.py b/ipaserver/plugins/dns.py
index 9cca07c6d33a49307de2d2110ac57ccc6b4b7509..db179079319a98ed27801ca55ae7a699c458e767 100644
--- a/ipaserver/plugins/dns.py
+++ b/ipaserver/plugins/dns.py
@@ -4064,6 +4064,18 @@ class dnsconfig(LDAPObject):
         Int('ipadnsversion?',  # available only in installer/upgrade
             label=_('IPA DNS version'),
         ),
+        Str(
+            'dns_server_server*',
+            label=_('IPA DNS servers'),
+            doc=_('List of IPA masters configured as DNS servers'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        ),
+        Str(
+            'dnssec_key_master_server?',
+            label=_('IPA DNSSec key master'),
+            doc=_('IPA server configured as DNSSec key master'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        )
     )
     managed_permissions = {
         'System: Write DNS Configuration': {
@@ -4107,9 +4119,22 @@ class dnsconfig(LDAPObject):
         return entry
 
     def postprocess_result(self, result):
-        if not any(param in result['result'] for param in self.params):
+        is_config_empty = not any(
+            param.name in result['result'] for param in self.params() if
+            u'virtual_attribute' not in param.flags
+        )
+        if is_config_empty:
             result['summary'] = unicode(_('Global DNS configuration is empty'))
 
+    def show_servroles_attributes(self, entry_attrs, **options):
+        if options.get('raw', False):
+            return
+
+        backend = self.api.Backend.serverroles
+        entry_attrs.update(
+            backend.config_retrieve("DNS server")
+        )
+
 
 @register()
 class dnsconfig_mod(LDAPUpdate):
@@ -4163,6 +4188,9 @@ class dnsconfig_mod(LDAPUpdate):
 
         return result
 
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        self.obj.show_servroles_attributes(entry_attrs, **options)
+        return dn
 
 
 @register()
@@ -4174,6 +4202,10 @@ class dnsconfig_show(LDAPRetrieve):
         self.obj.postprocess_result(result)
         return result
 
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        self.obj.show_servroles_attributes(entry_attrs, **options)
+        return dn
+
 
 
 @register()
diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py
index ee0ab5d10b83757d6a9b3dc027e6e4c3e7168991..925b9b5c320efaf9469efdc77129032ebfa869cd 100644
--- a/ipaserver/plugins/trust.py
+++ b/ipaserver/plugins/trust.py
@@ -1141,6 +1141,18 @@ class trustconfig(LDAPObject):
             cli_name='fallback_primary_group',
             label=_('Fallback primary group'),
         ),
+        Str(
+            'ad_trust_agent_server*',
+            label=_('IPA AD trust agents'),
+            doc=_('IPA servers configured as AD trust agents'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        ),
+        Str(
+            'ad_trust_controller_server*',
+            label=_('IPA AD trust controllers'),
+            doc=_('IPA servers configured as AD trust controllers'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        ),
     )
 
     def get_dn(self, *keys, **kwargs):
@@ -1211,6 +1223,22 @@ class trustconfig(LDAPObject):
 
         entry_attrs['ipantfallbackprimarygroup'] = [groupdn[0][0].value]
 
+    def show_servroles(self, entry_attrs, **options):
+        if options.get('raw', False):
+            return
+
+        backend = self.api.Backend.serverroles
+
+        adtrust_agents = backend.config_retrieve(
+            "AD trust agent"
+        )
+        adtrust_controllers = backend.config_retrieve(
+            "AD trust controller"
+        )
+
+        entry_attrs.update(adtrust_agents)
+        entry_attrs.update(adtrust_controllers)
+
 
 @register()
 class trustconfig_mod(LDAPUpdate):
@@ -1230,6 +1258,7 @@ class trustconfig_mod(LDAPUpdate):
 
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         self.obj._convert_groupdn(entry_attrs, options)
+        self.obj.show_servroles(entry_attrs, **options)
         return dn
 
 
@@ -1247,6 +1276,8 @@ class trustconfig_show(LDAPRetrieve):
 
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         self.obj._convert_groupdn(entry_attrs, options)
+        self.obj.show_servroles(entry_attrs, **options)
+
         return dn
 
 
diff --git a/ipaserver/plugins/vault.py b/ipaserver/plugins/vault.py
index 05db63cdc0080374742a6bd7828469c4871d3327..380e4d4783d98edd03d9f8c9bac3376fe0f2e5f8 100644
--- a/ipaserver/plugins/vault.py
+++ b/ipaserver/plugins/vault.py
@@ -959,6 +959,12 @@ class vaultconfig(Object):
             'transport_cert',
             label=_('Transport Certificate'),
         ),
+        Str(
+            'kra_server_server*',
+            label=_('IPA KRA servers'),
+            doc=_('IPA servers configured as key recovery agents'),
+            flags={'virtual_attribute', 'no_create', 'no_update'}
+        )
     )
 
 
@@ -981,10 +987,13 @@ class vaultconfig_show(Retrieve):
 
         kra_client = self.api.Backend.kra.get_client()
         transport_cert = kra_client.system_certs.get_transport_cert()
+        config = {'transport_cert': transport_cert.binary}
+        config.update(
+            self.api.Backend.serverroles.config_retrieve("KRA server")
+        )
+
         return {
-            'result': {
-                'transport_cert': transport_cert.binary
-            },
+            'result': config,
             'value': None,
         }
 
-- 
2.5.5

From fdd355916b3364bc4903958a15c4d1b812bae19d Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:51:48 +0200
Subject: [PATCH 7/7] Server Roles: provide an API for setting CA renewal
 master

`ipa config-mod` gained '--ca-renewal-master' options which can be used to
set CA renewal master to a different server. Obviously, this server has to
have CA role enabled.

https://fedorahosted.org/freeipa/ticket/5689
http://www.freeipa.org/page/V4/Server_Roles
---
 API.txt                     |  3 ++-
 VERSION                     |  4 ++--
 ipaserver/plugins/config.py | 23 ++++++++++++++++++++++-
 3 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/API.txt b/API.txt
index 8ecea435e969e349e93531d1b0fb6144a01131e6..cecaa970e6802b177c76d7ff220b98cc45a90d1f 100644
--- a/API.txt
+++ b/API.txt
@@ -789,9 +789,10 @@ args: 0,1,1
 option: Str('version?')
 output: Output('result')
 command: config_mod
-args: 0,25,3
+args: 0,26,3
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('ca_renewal_master_server?', autofill=False)
 option: Str('delattr*', cli_name='delattr')
 option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring', values=[u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', u'KDC:Disable Default Preauth for SPNs'])
 option: Str('ipadefaultemaildomain?', autofill=False, cli_name='emaildomain')
diff --git a/VERSION b/VERSION
index 5214f24a1ff525529a7eedd69665ea9033194467..0d3dbb8148016341eedb3a116965d2c737ca2d87 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=178
-# Last change: mbabinsk - Server Roles: make server-{show,find} utilize role information
+IPA_API_VERSION_MINOR=179
+# Last change: mbabink - Server Roles: provide an API for setting CA renewal master
diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py
index 95d1d6409abd43dbe7771d4403097c8c354a406e..94a48a27de273baf0906e6c75ab0bc18ded8ea6d 100644
--- a/ipaserver/plugins/config.py
+++ b/ipaserver/plugins/config.py
@@ -243,7 +243,7 @@ class config(LDAPObject):
             'ca_renewal_master_server?',
             label=_('IPA CA renewal master'),
             doc=_('Renewal master for IPA certificate authority'),
-            flags={'virtual_attribute', 'no_create', 'no_update'}
+            flags={'virtual_attribute', 'no_create'}
         )
     )
 
@@ -377,8 +377,29 @@ class config_mod(LDAPUpdate):
                 raise errors.ValidationError(name=failedattr,
                     error=_('SELinux user map default user not in order list'))
 
+        if 'ca_renewal_master_server' in options:
+            new_master = options['ca_renewal_master_server']
+
+            try:
+                self.api.Object.server.get_dn_if_exists(new_master)
+            except errors.NotFound:
+                self.api.Object.server.handle_not_found(new_master)
+
+            backend = self.api.Backend.serverroles
+            backend.config_update(ca_renewal_master_server=new_master)
+
         return dn
 
+    def exc_callback(self, keys, options, exc, call_func,
+                     *call_args, **call_kwargs):
+        if (isinstance(exc, errors.EmptyModlist) and
+                call_func.__name__ == 'update_entry' and
+                'ca_renewal_master_server' in options):
+            return
+
+        super(config_mod, self).exc_callback(
+            keys, options, exc, call_func, *call_args, **call_kwargs)
+
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         self.obj.show_servroles_attributes(entry_attrs, **options)
         return dn
-- 
2.5.5

-- 
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