# # Copyright (C) 2015 FreeIPA Contributors see COPYING for license # import dbus import dbus.mainloop.glib import ldap import time from ipalib import api, crud, errors, messages from ipalib import Int, Flag, Str, DNSNameParam from ipalib.plugable import Registry from .baseldap import ( LDAPSearch, LDAPRetrieve, LDAPDelete, LDAPObject, LDAPUpdate, ) from ipalib.request import context from ipalib import _, ngettext from ipalib import output from ipaplatform import services from ipapython.dn import DN from ipapython.dnsutil import DNSName from ipaserver import topology from ipaserver.servroles import ENABLED from ipaserver.install import bindinstance, dnskeysyncinstance __doc__ = _(""" IPA servers """) + _(""" Get information about installed IPA servers. """) + _(""" EXAMPLES: """) + _(""" Find all servers: ipa server-find """) + _(""" Show specific server: ipa server-show ipa.example.com """) register = Registry() @register() class server(LDAPObject): """ IPA server """ container_dn = api.env.container_masters object_name = _('server') object_name_plural = _('servers') object_class = ['top'] possible_objectclasses = ['ipaLocationMember'] search_attributes = ['cn'] default_attributes = [ 'cn', 'iparepltopomanagedsuffix', 'ipamindomainlevel', 'ipamaxdomainlevel', 'ipalocation', 'ipaserviceweight' ] label = _('IPA Servers') label_singular = _('IPA Server') attribute_members = { 'iparepltopomanagedsuffix': ['topologysuffix'], 'ipalocation': ['location'], 'role': ['servrole'], } relationships = { 'iparepltopomanagedsuffix': ('Managed', '', 'no_'), 'ipalocation': ('IPA', 'in_', 'not_in_'), 'role': ('Enabled', '', 'no_'), } permission_filter_objectclasses = ['ipaConfigObject'] managed_permissions = { 'System: Read Locations of IPA Servers': { 'ipapermright': {'read', 'search', 'compare'}, 'ipapermdefaultattr': { 'objectclass', 'cn', 'ipalocation', 'ipaserviceweight', }, 'default_privileges': {'DNS Administrators'}, }, 'System: Read Status of Services on IPA Servers': { 'ipapermright': {'read', 'search', 'compare'}, 'ipapermdefaultattr': {'objectclass', 'cn', 'ipaconfigstring'}, 'default_privileges': {'DNS Administrators'}, } } takes_params = ( Str( 'cn', cli_name='name', primary_key=True, label=_('Server name'), doc=_('IPA server hostname'), ), Str( 'iparepltopomanagedsuffix*', flags={'no_create', 'no_update', 'no_search'}, ), Str( 'iparepltopomanagedsuffix_topologysuffix*', label=_('Managed suffixes'), flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, ), Int( 'ipamindomainlevel', cli_name='minlevel', label=_('Min domain level'), doc=_('Minimum domain level'), flags={'no_create', 'no_update'}, ), Int( 'ipamaxdomainlevel', cli_name='maxlevel', label=_('Max domain level'), doc=_('Maximum domain level'), flags={'no_create', 'no_update'}, ), DNSNameParam( 'ipalocation_location?', cli_name='location', label=_('Location'), doc=_('Server location'), only_relative=True, flags={'no_search'}, ), Int( 'ipaserviceweight?', cli_name='service_weight', label=_('Service weight'), doc=_('Weight for server services'), minvalue=0, maxvalue=65535, flags={'no_search'}, ), Str( 'service_relative_weight', label=_('Service relative weight'), doc=_('Relative weight for server services (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): suffixes = self.api.Command.topologysuffix_find( all=True, raw=True, )['result'] suffixes = [(s['iparepltopoconfroot'][0], s['dn']) for s in suffixes] return suffixes def _apply_suffixes(self, entry, suffixes): # change suffix DNs to topologysuffix entry DNs # this fixes LDAPObject.convert_attribute_members() for suffixes suffixes = dict(suffixes) if 'iparepltopomanagedsuffix' in entry: entry['iparepltopomanagedsuffix'] = [ suffixes.get(m, m) for m in entry['iparepltopomanagedsuffix'] ] def normalize_location(self, kw, **options): """ Return the DN of location """ if 'ipalocation_location' in kw: location = kw.pop('ipalocation_location') kw['ipalocation'] = ( [self.api.Object.location.get_dn(location)] if location is not None else location ) def convert_location(self, entry_attrs, **options): """ Return a location name from DN """ if options.get('raw'): return converted_locations = [ DNSName(location_dn['idnsname']) for location_dn in entry_attrs.pop('ipalocation', []) ] if converted_locations: entry_attrs['ipalocation_location'] = converted_locations def get_enabled_roles(self, entry_attrs, **options): if not options.get('all', False) and options.get('no_members', False): return if options.get('raw', 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): __doc__ = _('Modify information about an IPA server.') msg_summary = _('Modified IPA server "%(value)s"') def args_options_2_entry(self, *args, **options): kw = super(server_mod, self).args_options_2_entry( *args, **options) self.obj.normalize_location(kw, **options) return kw def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): assert isinstance(dn, DN) if entry_attrs.get('ipalocation'): if not ldap.entry_exists(entry_attrs['ipalocation'][0]): self.api.Object.location.handle_not_found( options['ipalocation_location']) if 'ipalocation' in entry_attrs or 'ipaserviceweight' in entry_attrs: server_entry = ldap.get_entry(dn, ['objectclass']) # we need to extend object with ipaLocationMember objectclass entry_attrs['objectclass'] = ( server_entry['objectclass'] + ['ipalocationmember'] ) return dn def post_callback(self, ldap, dn, entry_attrs, *keys, **options): assert isinstance(dn, DN) self.obj.get_enabled_roles(entry_attrs) if 'ipalocation_location' in options: ipalocation = entry_attrs.get('ipalocation') if ipalocation: ipalocation = ipalocation[0]['idnsname'] else: ipalocation = u'' try: self.api.Command.dnsserver_mod( keys[0], setattr=[ u'idnsSubstitutionVariable;ipalocation={loc}'.format( loc=ipalocation) ] ) except errors.EmptyModlist: pass except errors.NotFound: # server is not DNS server pass if 'ipalocation_location' or 'ipaserviceweight' in options: self.add_message(messages.ServiceRestartRequired( service=services.service('named').systemd_name, server=keys[0], )) result = self.api.Command.dns_update_system_records() if not result.get('value'): self.add_message(messages.AutomaticDNSRecordsUpdateFailed()) self.obj.convert_location(entry_attrs, **options) ipalocation = entry_attrs.get('ipalocation_location', [None])[0] if ipalocation: servers_in_loc = self.api.Command.server_find( in_location=ipalocation, no_members=False)['result'] dns_server_in_loc = False for server in servers_in_loc: if 'DNS server' in server.get('enabled_role_servrole', ()): dns_server_in_loc = True break if not dns_server_in_loc: self.add_message(messages.LocationWithoutDNSServer( location=ipalocation )) return dn @register() class server_find(LDAPSearch): __doc__ = _('Search for IPA servers.') msg_summary = ngettext( '%(count)d IPA server matched', '%(count)d IPA servers matched', 0 ) member_attributes = ['iparepltopomanagedsuffix', 'ipalocation', 'role'] def args_options_2_entry(self, *args, **options): kw = super(server_find, self).args_options_2_entry( *args, **options) self.obj.normalize_location(kw, **options) return kw def get_options(self): for option in super(server_find, self).get_options(): if option.name == 'topologysuffix': 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) 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): included = options.get('topologysuffix') excluded = options.get('no_topologysuffix') if included or excluded: topologysuffix = self.api.Object.topologysuffix suffixes = self.obj._get_suffixes() suffixes = {s[1]: s[0] for s in suffixes} if included: included = [topologysuffix.get_dn(pk) for pk in included] try: included = [suffixes[dn] for dn in included] except KeyError: # force empty result filter = '(!(objectclass=*))' else: filter = ldap.make_filter_from_attr( 'iparepltopomanagedsuffix', included, ldap.MATCH_ALL ) filters = ldap.combine_filters( (filters, filter), ldap.MATCH_ALL ) if excluded: excluded = [topologysuffix.get_dn(pk) for pk in excluded] excluded = [suffixes[dn] for dn in excluded if dn in suffixes] filter = ldap.make_filter_from_attr( 'iparepltopomanagedsuffix', excluded, ldap.MATCH_NONE ) filters = ldap.combine_filters( (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): if not options.get('raw', False): suffixes = self.obj._get_suffixes() for entry in entries: self.obj._apply_suffixes(entry, suffixes) for entry in entries: self.obj.convert_location(entry, **options) self.obj.get_enabled_roles(entry, **options) return truncated @register() class server_show(LDAPRetrieve): __doc__ = _('Show IPA server.') def post_callback(self, ldap, dn, entry, *keys, **options): 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 @register() class server_del(LDAPDelete): __doc__ = _('Delete IPA server.') msg_summary = _('Deleted IPA server "%(value)s"') takes_options = LDAPDelete.takes_options + ( Flag( 'ignore_topology_disconnect?', label=_('Ignore topology errors'), doc=_('Ignore topology connectivity problems after removal'), default=False, ), Flag( 'ignore_last_of_role?', label=_('Ignore check for last remaining CA or DNS server'), doc=_('Skip a check whether the last CA master or DNS server is ' 'removed'), default=False, ), Flag( 'force?', label=_('Force server removal'), doc=_('Force server removal even if it does not exist'), default=False, ), ) def _ensure_last_of_role(self, hostname, ignore_last_of_role=False): """ 1. When deleting server, check if there will be at least one remaining DNS and CA server. 2. Pick CA renewal master """ def handler(msg, ignore_last_of_role): if ignore_last_of_role: self.add_message( messages.ServerRemovalWarning( message=msg ) ) else: raise errors.ServerRemovalError(reason=_(msg)) ipa_config = self.api.Command.config_show()['result'] ipa_masters = ipa_config['ipa_master_server'] # skip these checks if the last master is being removed if ipa_masters == [hostname]: return if self.api.Command.dns_is_enabled()['result']: dns_config = self.api.Command.dnsconfig_show()['result'] dns_servers = dns_config.get('dns_server_server', []) dnssec_keymaster = dns_config.get('dnssec_key_master_server', []) if dnssec_keymaster == hostname: handler( _("Replica is active DNSSEC key master. Uninstall " "could break your DNS system. Please disable or " "replace DNSSEC key master first."), ignore_last_of_role) if dns_servers == [hostname]: handler( _("Deleting this server will leave your installation " "without a DNS."), ignore_last_of_role) if self.api.Command.ca_is_enabled()['result']: ca_servers = ipa_config.get('ca_server_server', []) ca_renewal_master = ipa_config.get( 'ca_renewal_master_server', []) if ca_servers == [hostname]: raise errors.ServerRemovalError( reason=_("Deleting this server is not allowed as it would " "leave your installation without a CA.")) if ca_renewal_master == hostname: other_cas = [ca for ca in ca_servers if ca != hostname] # if this is the last CA there is no other server to become # renewal master if not other_cas: return self.api.Command.config_mod( ca_renewal_master_server=other_cas[0]) if ignore_last_of_role: self.add_message( messages.ServerRemovalWarning( message=_("Ignoring these warnings and proceeding with " "removal"))) def _check_topology_connectivity(self, topology_connectivity, master_cn): try: topology_connectivity.check_current_state() except ValueError as e: raise errors.ServerRemovalError(reason=e) try: topology_connectivity.check_state_after_removal(master_cn) except ValueError as e: raise errors.ServerRemovalError(reason=e) def _remove_server_principal_references(self, master): """ This method removes information about the replica in parts of the shared tree that expose it, so clients stop trying to use this replica. """ conn = self.Backend.ldap2 env = self.api.env master_principal = "{}@{}".format(master, env.realm) # remove replica memberPrincipal from s4u2proxy configuration s4u2proxy_subtree = DN(env.container_s4u2proxy, env.basedn) dn1 = DN(('cn', 'ipa-http-delegation'), s4u2proxy_subtree) member_principal1 = "HTTP/{}".format(master_principal) dn2 = DN(('cn', 'ipa-ldap-delegation-targets'), s4u2proxy_subtree) member_principal2 = "ldap/{}".format(master_principal) dn3 = DN(('cn', 'ipa-cifs-delegation-targets'), s4u2proxy_subtree) member_principal3 = "cifs/{}".format(master_principal) for (dn, member_principal) in ((dn1, member_principal1), (dn2, member_principal2), (dn3, member_principal3)): try: mod = [(ldap.MOD_DELETE, 'memberPrincipal', member_principal)] conn.conn.modify_s(str(dn), mod) except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE): self.log.debug( "Replica (%s) memberPrincipal (%s) not found in %s" % (master, member_principal, dn)) except Exception as e: self.add_message( messages.ServerRemovalWarning( message=_("Failed to clean memberPrincipal " "%(principal)s from s4u2proxy entry %(dn)s: " "%(err)s") % dict( principal=member_principal, dn=dn, err=e))) try: etc_basedn = DN(('cn', 'etc'), env.basedn) filter = '(dnaHostname=%s)' % master entries = conn.get_entries( etc_basedn, ldap.SCOPE_SUBTREE, filter=filter) if len(entries) != 0: for entry in entries: conn.delete_entry(entry) except errors.NotFound: pass except Exception as e: self.add_message( messages.ServerRemovalWarning( message=_( "Failed to clean up DNA hostname entries for " "%(master)s: %(err)s") % dict(master=master, err=e))) try: dn = DN(('cn', 'default'), ('ou', 'profile'), env.basedn) ret = conn.get_entry(dn) srvlist = ret.single_value.get('defaultServerList', '') srvlist = srvlist[0].split() if master in srvlist: srvlist.remove(master) attr = ' '.join(srvlist) mod = [(ldap.MOD_REPLACE, 'defaultServerList', attr)] conn.conn.modify_s(str(dn), mod) except (errors.NotFound, ldap.NO_SUCH_ATTRIBUTE, ldap.TYPE_OR_VALUE_EXISTS): pass except Exception as e: self.add_message( messages.ServerRemovalWarning( message=_("Failed to remove server %(master)s from server " "list: %(err)s") % dict(master=master, err=e))) def _remove_server_host_services(self, ldap, master): """ delete server kerberos key and all its svc principals """ try: entries = ldap.get_entries( self.api.env.basedn, ldap.SCOPE_SUBTREE, filter='(krbprincipalname=*/{}@{})'.format( master, self.api.env.realm)) if entries: entries.sort(key=lambda x: len(x.dn), reverse=True) for entry in entries: ldap.delete_entry(entry) except errors.NotFound: pass except Exception as e: self.add_message( messages.ServerRemovalWarning( message=_("Failed to cleanup server principals/keys: " "%(err)s") % dict(err=e))) def _cleanup_server_dns_records(self, hostname, **options): if not self.api.Command.dns_is_enabled( **options): return try: bindinstance.remove_master_dns_records( hostname, self.api.env.realm) dnskeysyncinstance.remove_replica_public_keys(hostname) except Exception as e: self.add_message( messages.ServerRemovalWarning( message=_( "Failed to cleanup %(hostname)s DNS entries: " "%(err)s") % dict(hostname=hostname, err=e))) self.add_message( messages.ServerRemovalWarning( message=_("You may need to manually remove them from the " "tree"))) def pre_callback(self, ldap, dn, *keys, **options): pkey = self.obj.get_primary_key_from_dn(dn) if options.get('force', False): self.add_message( messages.ServerRemovalWarning( message=_("Forcing removal of %(hostname)s") % dict( hostname=pkey))) # check the topology errors before and after removal self.context.topology_connectivity = topology.TopologyConnectivity( self.api) if options.get('ignore_topology_disconnect', False): self.add_message( messages.ServerRemovalWarning( message=_("Ignoring topology connectivity errors."))) else: self._check_topology_connectivity( self.context.topology_connectivity, pkey) # ensure that we are not removing last CA/DNS server, DNSSec master and # CA renewal master self._ensure_last_of_role( pkey, ignore_last_of_role=options.get('ignore_last_of_role', False) ) # remove the references to master's ldap/http principals self._remove_server_principal_references(pkey) # finally destroy all Kerberos principals self._remove_server_host_services(ldap, pkey) # try to clean up the leftover DNS entries self._cleanup_server_dns_records(pkey) return dn def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): if (options.get('force', False) and isinstance(exc, errors.NotFound) and call_func.__name__ == 'delete_entry'): self.add_message( message=messages.ServerRemovalWarning( message=_("Server has already been deleted"))) return raise exc def _check_deleted_segments(self, hostname, topology_connectivity, starting_host): def wait_for_segment_removal(hostname, master_cns, suffix_name, orig_errors, new_errors): i = 0 while True: left = self.api.Command.topologysegment_find( suffix_name, iparepltoposegmentleftnode=hostname, sizelimit=0 )['result'] right = self.api.Command.topologysegment_find( suffix_name, iparepltoposegmentrightnode=hostname, sizelimit=0 )['result'] # Relax check if topology was or is disconnected. Disconnected # topology can contain segments with already deleted servers # Check only if segments of servers, which can contact this # server, and the deleted server were removed. # This code should handle a case where there was a topology # with a central node(B): A <-> B <-> C, where A is current # server. After removal of B, topology will be disconnected and # removal of segment B <-> C won't be replicated back to server # A, therefore presence of the segment has to be ignored. if orig_errors or new_errors: # use errors after deletion because we don't care if some # server can't contact the deleted one cant_contact_me = [e[0] for e in new_errors if starting_host in e[2]] can_contact_me = set(master_cns) - set(cant_contact_me) left = [ s for s in left if s['iparepltoposegmentrightnode'][0] in can_contact_me ] right = [ s for s in right if s['iparepltoposegmentleftnode'][0] in can_contact_me ] if not left and not right: self.add_message( messages.ServerRemovalInfo( message=_("Agreements deleted") )) return time.sleep(2) if i == 2: # taking too long, something is wrong, report self.log.info( "Waiting for removal of replication agreements") if i > 90: self.log.info("Taking too long, skipping") self.log.info("Following segments were not deleted:") self.add_message(messages.ServerRemovalWarning( message=_("Following segments were not deleted:"))) for s in left: self.add_message(messages.ServerRemovalWarning( message=u" %s" % s['cn'][0])) for s in right: self.add_message(messages.ServerRemovalWarning( message=u" %s" % s['cn'][0])) return i += 1 topology_graphs = topology_connectivity.graphs orig_errors = topology_connectivity.errors new_errors = topology_connectivity.errors_after_master_removal( hostname ) for suffix_name in topology_graphs: suffix_members = topology_graphs[suffix_name].vertices if hostname not in suffix_members: # If the server was already deleted, we can expect that all # removals had been done in previous run and dangling segments # were not deleted. self.log.info( "Skipping replication agreement deletion check for " "suffix '{0}'".format(suffix_name)) continue self.log.info( "Checking for deleted segments in suffix '{0}'".format( suffix_name)) wait_for_segment_removal( hostname, list(suffix_members), suffix_name, orig_errors[suffix_name], new_errors[suffix_name]) def post_callback(self, ldap, dn, *keys, **options): # there is no point in checking deleted segment on local host # we should do this only when removing other masters if self.api.env.host != keys[-1]: self._check_deleted_segments( keys[-1], self.context.topology_connectivity, self.api.env.host) return super(server_del, self).post_callback( ldap, dn, *keys, **options) @register() class server_conncheck(crud.PKQuery): __doc__ = _("Check connection to remote IPA server.") NO_CLI = True takes_args = ( Str( 'remote_cn', cli_name='remote_name', label=_('Remote server name'), doc=_('Remote IPA server hostname'), ), ) has_output = output.standard_value def execute(self, *keys, **options): # the server must be the local host if keys[-2] != api.env.host: raise errors.ValidationError( name='cn', error=_("must be \"%s\"") % api.env.host) # the server entry must exist try: self.obj.get_dn_if_exists(*keys[:-1]) except errors.NotFound: self.obj.handle_not_found(keys[-2]) # the user must have the Replication Administrators privilege privilege = u'Replication Administrators' privilege_dn = self.api.Object.privilege.get_dn(privilege) ldap = self.obj.backend filter = ldap.make_filter({ 'krbprincipalname': context.principal, # pylint: disable=no-member 'memberof': privilege_dn}, rules=ldap.MATCH_ALL) try: ldap.find_entries(base_dn=self.api.env.basedn, filter=filter) except errors.NotFound: raise errors.ACIError( info=_("not allowed to perform server connection check")) dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() obj = bus.get_object('org.freeipa.server', '/', follow_name_owner_changes=True) server = dbus.Interface(obj, 'org.freeipa.server') ret, stdout, stderr = server.conncheck(keys[-1]) result = dict( result=(ret == 0), value=keys[-2], ) for line in stdout.splitlines(): messages.add_message(options['version'], result, messages.ExternalCommandOutput(line=line)) return result