On 06/10/2016 05:42 PM, Martin Babinsky wrote:
On 06/10/2016 02:22 PM, Jan Cholasta wrote:
On 9.6.2016 17:06, Martin Babinsky wrote:
On 06/09/2016 03:54 PM, Petr Vobornik wrote:
On 06/09/2016 01:02 PM, Martin Babinsky wrote:
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.


NACK

Option timelimit? of command server_role_find in ipalib, not in API
file:
Int('timelimit?', autofill=False)
Option sizelimit? of command server_role_find in ipalib, not in API
file:
Int('sizelimit?', autofill=False)

There are one or more changes to the API.
Either undo the API changes or update API.txt and increment the major
version in VERSION.
Makefile:159: recipe for target 'version-update' failed
make: *** [version-update] Error 1


Oops, seems like a missed API.txt update.

Fixed.

"ipa server-role-find" does not return the "IPA master" role for my
server ("ipa-server-role $HOSTNAME 'IPA master'" does).

This is intentional since we discussed during the design phase[1] that
"IPA master" role should be implicit and not shown to the user in
server-show and server-role-find operation. This however does not
preclude you to query its status manually if you know the role name.

[1] http://www.freeipa.org/page/V4/Server_Roles#Server_Roles

I would rather skip the option altogether rather than hide it:

+            # 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'})

So something like:

    elif option.name == 'no_servrole':
        continue

should do the trick?
The patches need a rebase (VERSION).

Otherwise LGTM.


Ok I will send fixed patches ASAP.


Attaching rebased patches. 'no_servrole' option is now skipped and does not show in the API.

--
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 297885c1f187bcd994a7b1d4890dd48d79b38448 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:18:38 +0200
Subject: [PATCH 04/07] 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                         |  25 ++++++
 VERSION                         |   4 +-
 ipaserver/plugins/serverrole.py | 178 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 205 insertions(+), 2 deletions(-)
 create mode 100644 ipaserver/plugins/serverrole.py

diff --git a/API.txt b/API.txt
index 4247dd77c38fc17be8639a988049803d5b1f558f..f52f23f131f61eb6a890ad3f46e98a0692bfaa96 100644
--- a/API.txt
+++ b/API.txt
@@ -4043,6 +4043,31 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: server_role_find
+args: 1,8,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: Int('sizelimit?', autofill=False)
+option: StrEnum('status?', autofill=False, cli_name='status', default=u'enabled', values=[u'enabled', u'configured', u'absent'])
+option: Int('timelimit?', autofill=False)
+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 8945ae54888a21f6335389c26859ea2cb6353cd6..50cf35e72dbcbfdf449500b0baf67b2a635a7d71 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: abbra - adtrust: remove nttrustpartner parameter
+IPA_API_VERSION_MINOR=178
+# 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 06b97241beb3f507559d7e911903ba9b09e63b1d Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:27:38 +0200
Subject: [PATCH 05/07] 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                     |  3 +-
 VERSION                     |  4 +--
 ipaserver/plugins/server.py | 78 ++++++++++++++++++++++++++++++++++++++++++---
 3 files changed, 78 insertions(+), 7 deletions(-)

diff --git a/API.txt b/API.txt
index f52f23f131f61eb6a890ad3f46e98a0692bfaa96..5a5e20b3b97103a3c97956d6be2c737df50006ef 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,15,4
 arg: Str('criteria?')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('cn?', autofill=False, cli_name='name')
@@ -4019,6 +4019,7 @@ 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 50cf35e72dbcbfdf449500b0baf67b2a635a7d71..c28fd1929b0c91f3959defeafafd0ebfa3842372 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: public API for server roles
+IPA_API_VERSION_MINOR=179
+# 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..a2c2752d94913eda0636cd5a360921eb002282d3 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,50 @@ 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
+            elif option.name == 'no_servrole':
+                continue
             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)
+
+        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 +333,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 +349,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 +361,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 40eaca26ae4960d0a59a00e7d14ba0f44bd29b62 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:42:01 +0200
Subject: [PATCH 06/07] 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 f9b48f3a3e231ccd3fe36904f6c63d0afe1c37c2..02d2e0e81998e04f737fc9d531b4c03d069d6ded 100644
--- a/ipaserver/plugins/trust.py
+++ b/ipaserver/plugins/trust.py
@@ -1179,6 +1179,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):
@@ -1249,6 +1261,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):
@@ -1268,6 +1296,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
 
 
@@ -1285,6 +1314,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 00665de62728baafbeca1e6c7ff54546217f9fcc Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Mon, 30 May 2016 18:51:48 +0200
Subject: [PATCH 07/07] 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 5a5e20b3b97103a3c97956d6be2c737df50006ef..68ce3560d17c1fb6b6c50a91b5bf6ba810204922 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 c28fd1929b0c91f3959defeafafd0ebfa3842372..7c3e46a98607f3b94a0c98406ed13aa278440875 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=179
-# Last change: mbabinsk - Server Roles: make server-{show,find} utilize role information
+IPA_API_VERSION_MINOR=180
+# 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