From a29418ea68e66b719ecf7a0b018ff70a9e30b1d8 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Aug 24 2018 10:15:52 +0000 Subject: Rename Python scripts and add dynamic shebang All Python scripts are now generated from a template with a dynamic shebang. ipatests/i18n.py is no longer an executable script with shebang. The module is not executed as script directly, but rather as $(PYTHON) ipatests/i18n.py Fixes: https://pagure.io/freeipa/issue/7680 All Python scripts are now template files with a dynamic shebang line. Reviewed-By: Alexander Bokovoy --- diff --git a/.gitignore b/.gitignore index 8d170e6..178296b 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,56 @@ freeipa2-dev-doc /ipapython/.DEFAULT_PLUGINS /ipatests/.cache/ + +# Python scripts with auto-generated shebang +ipa +makeaci +makeapi +client/ipa-certupdate +client/ipa-client-automount +client/ipa-client-install +daemons/dnssec/ipa-dnskeysyncd +daemons/dnssec/ipa-dnskeysync-replica +daemons/dnssec/ipa-ods-exporter +install/certmonger/dogtag-ipa-ca-renew-agent-submit +install/certmonger/ipa-server-guard +install/oddjob/com.redhat.idm.trust-fetch-domains +install/restart_scripts/renew_ca_cert +install/restart_scripts/renew_kdc_cert +install/restart_scripts/renew_ra_cert +install/restart_scripts/renew_ra_cert_pre +install/restart_scripts/restart_dirsrv +install/restart_scripts/restart_httpd +install/restart_scripts/stop_pkicad +install/tools/ipa-adtrust-install +install/tools/ipa-advise +install/tools/ipa-backup +install/tools/ipa-cacert-manage +install/tools/ipa-ca-install +install/tools/ipa-compat-manage +install/tools/ipa-csreplica-manage +install/tools/ipactl +install/tools/ipa-custodia +install/tools/ipa-custodia-check +install/tools/ipa-dns-install +install/tools/ipa-httpd-kdcproxy +install/tools/ipa-kra-install +install/tools/ipa-ldap-updater +install/tools/ipa-managed-entries +install/tools/ipa-nis-manage +install/tools/ipa-otptoken-import +install/tools/ipa-pkinit-manage +install/tools/ipa-pki-retrieve-key +install/tools/ipa-replica-conncheck +install/tools/ipa-replica-install +install/tools/ipa-replica-manage +install/tools/ipa-replica-prepare +install/tools/ipa-restore +install/tools/ipa-server-certinstall +install/tools/ipa-server-install +install/tools/ipa-server-upgrade +install/tools/ipa-winsync-migrate +ipatests/i18n.py +ipatests/ipa-run-tests +ipatests/ipa-test-config +ipatests/ipa-test-task diff --git a/client/ipa-certupdate b/client/ipa-certupdate deleted file mode 100755 index 1fb530f..0000000 --- a/client/ipa-certupdate +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Jan Cholasta -# -# Copyright (C) 2014 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaclient.install.ipa_certupdate import CertUpdate - -CertUpdate.run_cli() diff --git a/client/ipa-certupdate.in b/client/ipa-certupdate.in new file mode 100644 index 0000000..d4734c7 --- /dev/null +++ b/client/ipa-certupdate.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Jan Cholasta +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaclient.install.ipa_certupdate import CertUpdate + +CertUpdate.run_cli() diff --git a/client/ipa-client-automount b/client/ipa-client-automount deleted file mode 100755 index 26724fb..0000000 --- a/client/ipa-client-automount +++ /dev/null @@ -1,543 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Rob Crittenden -# -# Copyright (C) 2012 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Configure the automount client for ldap. - -from __future__ import print_function - -import logging -import sys -import os -import time -import tempfile -import gssapi - -try: - from xml.etree import cElementTree as etree -except ImportError: - from xml.etree import ElementTree as etree - -import SSSDConfig -# pylint: disable=import-error -from six.moves.urllib.parse import urlsplit -# pylint: enable=import-error - -from optparse import OptionParser # pylint: disable=deprecated-module - -from ipaclient.install import ipachangeconf, ipadiscovery -from ipaclient.install.client import (CLIENT_NOT_CONFIGURED, - CLIENT_ALREADY_CONFIGURED) -from ipalib import api, errors -from ipalib.install import sysrestore -from ipalib.install.kinit import kinit_keytab -from ipalib.util import check_client_configuration -from ipapython import ipautil -from ipapython.ipa_log_manager import standard_logging_setup -from ipapython.dn import DN -from ipaplatform.constants import constants -from ipaplatform.tasks import tasks -from ipaplatform import services -from ipaplatform.paths import paths -from ipapython.admintool import ScriptError - - -logger = logging.getLogger(os.path.basename(__file__)) - - -def parse_options(): - usage = "%prog [options]\n" - parser = OptionParser(usage=usage) - parser.add_option("--server", dest="server", help="FQDN of IPA server") - parser.add_option("--location", dest="location", help="Automount location", - default="default") - parser.add_option("-S", "--no-sssd", dest="sssd", - action="store_false", default=True, - help="Do not configure the client to use SSSD for automount") - parser.add_option("--debug", dest="debug", action="store_true", - default=False, help="enable debugging") - parser.add_option("-U", "--unattended", dest="unattended", - action="store_true", default=False, - help="unattended installation never prompts the user") - parser.add_option("--uninstall", dest="uninstall", action="store_true", - default=False, help="Unconfigure automount") - - options, args = parser.parse_args() - return options, args - -def wait_for_sssd(): - """ - It takes a bit for sssd to get going, lets loop until it is - serving data. - - This function returns nothing. - """ - n = 0 - found = False - time.sleep(1) - while n < 10 and not found: - try: - ipautil.run([paths.GETENT, "passwd", "admin@%s" % api.env.realm]) - found = True - except Exception: - time.sleep(1) - n = n + 1 - - # This should never happen but if it does, may as well warn the user - if not found: - err_msg = ("Unable to find 'admin' user with " - "'getent passwd admin@%s'!" % api.env.realm) - logger.debug('%s', err_msg) - print(err_msg) - print("This may mean that sssd didn't re-start properly after the configuration changes.") - -def configure_xml(fstore): - authconf = paths.AUTOFS_LDAP_AUTH_CONF - fstore.backup_file(authconf) - - try: - tree = etree.parse(authconf) - except IOError as e: - logger.debug('Unable to open file %s', e) - logger.debug('Creating new from template') - tree = etree.ElementTree( - element=etree.Element('autofs_ldap_sasl_conf') - ) - - element = tree.getroot() - if element.tag != 'autofs_ldap_sasl_conf': - raise RuntimeError('Invalid XML root in file %s' % authconf) - - element.set('usetls', 'no') - element.set('tlsrequired', 'no') - element.set('authrequired', 'yes') - element.set('authtype', 'GSSAPI') - element.set('clientprinc', 'host/%s@%s' % (api.env.host, api.env.realm)) - - try: - tree.write(authconf, xml_declaration=True, encoding='UTF-8') - except IOError as e: - print("Unable to write %s: %s" % (authconf, e)) - else: - print("Configured %s" % authconf) - -def configure_nsswitch(fstore, options): - """ - Point automount to ldap in nsswitch.conf. This function is for non-SSSD - setups only - """ - fstore.backup_file(paths.NSSWITCH_CONF) - - conf = ipachangeconf.IPAChangeConf("IPA Installer") - conf.setOptionAssignment(':') - - nss_value = ' files ldap' - - opts = [{'name':'automount', 'type':'option', 'action':'set', 'value':nss_value}, - {'name':'empty', 'type':'empty'}] - - conf.changeConf(paths.NSSWITCH_CONF, opts) - - print("Configured %s" % paths.NSSWITCH_CONF) - -def configure_autofs_sssd(fstore, statestore, autodiscover, options): - try: - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.import_config() - domains = sssdconfig.list_active_domains() - except Exception as e: - sys.exit(e) - - try: - sssdconfig.new_service('autofs') - except SSSDConfig.ServiceAlreadyExists: - pass - except SSSDConfig.ServiceNotRecognizedError: - logger.error("Unable to activate the Autofs service in SSSD config.") - logger.info( - "Please make sure you have SSSD built with autofs support " - "installed.") - logger.info( - "Configure autofs support manually in /etc/sssd/sssd.conf.") - sys.exit("Cannot create the autofs service in sssd.conf") - - sssdconfig.activate_service('autofs') - - domain = None - for name in domains: - domain = sssdconfig.get_domain(name) - try: - provider = domain.get_option('id_provider') - except SSSDConfig.NoOptionError: - continue - if provider == "ipa": - domain.add_provider('ipa', 'autofs') - try: - domain.get_option('ipa_automount_location') - print('An automount location is already configured') - sys.exit(CLIENT_ALREADY_CONFIGURED) - except SSSDConfig.NoOptionError: - domain.set_option('ipa_automount_location', options.location) - break - - if domain is None: - sys.exit('SSSD is not configured.') - - sssdconfig.save_domain(domain) - sssdconfig.write(paths.SSSD_CONF) - statestore.backup_state('autofs', 'sssd', True) - - sssd = services.service('sssd', api) - sssd.restart() - print("Restarting sssd, waiting for it to become available.") - wait_for_sssd() - -def configure_autofs(fstore, statestore, autodiscover, server, options): - """ - fstore: the FileStore to back up files in - options.server: the IPA server to use - options.location: the Automount location to use - """ - if not autodiscover: - ldap_uri = "ldap://%s" % server - else: - ldap_uri = "ldap:///%s" % api.env.basedn - - search_base = str(DN(('cn', options.location), api.env.container_automount, api.env.basedn)) - replacevars = { - 'MAP_OBJECT_CLASS': 'automountMap', - 'ENTRY_OBJECT_CLASS': 'automount', - 'MAP_ATTRIBUTE': 'automountMapName', - 'ENTRY_ATTRIBUTE': 'automountKey', - 'VALUE_ATTRIBUTE': 'automountInformation', - 'SEARCH_BASE': search_base, - 'LDAP_URI': ldap_uri, - } - - ipautil.backup_config_and_replace_variables(fstore, - paths.SYSCONFIG_AUTOFS, replacevars=replacevars) - tasks.restore_context(paths.SYSCONFIG_AUTOFS) - statestore.backup_state('autofs', 'sssd', False) - - print("Configured %s" % paths.SYSCONFIG_AUTOFS) - -def configure_autofs_common(fstore, statestore, options): - autofs = services.knownservices.autofs - statestore.backup_state('autofs', 'enabled', autofs.is_enabled()) - statestore.backup_state('autofs', 'running', autofs.is_running()) - try: - autofs.restart() - print("Started %s" % autofs.service_name) - except Exception as e: - logger.error("%s failed to restart: %s", autofs.service_name, e) - try: - autofs.enable() - except Exception as e: - print("Failed to configure automatic startup of the %s daemon" % (autofs.service_name)) - logger.error("Failed to enable automatic startup of the %s daemon: %s", - autofs.service_name, str(e)) - -def uninstall(fstore, statestore): - RESTORE_FILES=[ - paths.SYSCONFIG_AUTOFS, - paths.NSSWITCH_CONF, - paths.AUTOFS_LDAP_AUTH_CONF, - paths.SYSCONFIG_NFS, - paths.IDMAPD_CONF, - ] - STATES=['autofs', 'rpcidmapd', 'rpcgssd'] - - # automount only touches /etc/nsswitch.conf if LDAP is - # used. Don't restore it otherwise. - if (statestore.get_state('authconfig', 'sssd') or - (statestore.get_state('authselect', 'profile') == 'sssd')): - RESTORE_FILES.remove(paths.NSSWITCH_CONF) - - if (not any(fstore.has_file(f) for f in RESTORE_FILES) or - not any(statestore.has_state(s) for s in STATES)): - print("IPA automount is not configured on this system") - return CLIENT_NOT_CONFIGURED - - print("Restoring configuration") - - for filepath in RESTORE_FILES: - if fstore.has_file(filepath): - fstore.restore_file(filepath) - if statestore.has_state('autofs'): - enabled = statestore.restore_state('autofs', 'enabled') - running = statestore.restore_state('autofs', 'running') - sssd = statestore.restore_state('autofs', 'sssd') - autofs = services.knownservices.autofs - if not enabled: - autofs.disable() - if not running: - autofs.stop() - if sssd: - try: - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.import_config() - sssdconfig.deactivate_service('autofs') - domains = sssdconfig.list_active_domains() - for name in domains: - domain = sssdconfig.get_domain(name) - try: - provider = domain.get_option('id_provider') - except SSSDConfig.NoOptionError: - continue - if provider == "ipa": - domain.remove_option('ipa_automount_location') - domain.remove_provider('autofs') - break - sssdconfig.save_domain(domain) - sssdconfig.write(paths.SSSD_CONF) - sssd = services.service('sssd', api) - sssd.restart() - wait_for_sssd() - except Exception as e: - print('Unable to restore SSSD configuration: %s' % str(e)) - logger.debug('Unable to restore SSSD configuration: %s', - str(e)) - if statestore.has_state('rpcidmapd'): - enabled = statestore.restore_state('rpcidmapd', 'enabled') - running = statestore.restore_state('rpcidmapd', 'running') - rpcidmapd = services.knownservices.rpcidmapd - if not enabled: - rpcidmapd.disable() - if not running: - rpcidmapd.stop() - if statestore.has_state('rpcgssd'): - enabled = statestore.restore_state('rpcgssd', 'enabled') - running = statestore.restore_state('rpcgssd', 'running') - rpcgssd = services.knownservices.rpcgssd - if not enabled: - rpcgssd.disable() - if not running: - rpcgssd.stop() - - return 0 - -def configure_nfs(fstore, statestore): - """ - Configure secure NFS - """ - replacevars = { - constants.SECURE_NFS_VAR: 'yes', - } - ipautil.backup_config_and_replace_variables(fstore, - paths.SYSCONFIG_NFS, replacevars=replacevars) - tasks.restore_context(paths.SYSCONFIG_NFS) - - print("Configured %s" % paths.SYSCONFIG_NFS) - - # Prepare the changes - # We need to use IPAChangeConf as simple regexp substitution - # does not cut it here - conf = ipachangeconf.IPAChangeConf("IPA automount installer") - conf.case_insensitive_sections = False - conf.setOptionAssignment(" = ") - conf.setSectionNameDelimiters(("[", "]")) - - changes = [conf.setOption('Domain', api.env.domain)] - section_with_changes = [conf.setSection('General', changes)] - - # Backup the file and apply the changes - fstore.backup_file(paths.IDMAPD_CONF) - conf.changeConf(paths.IDMAPD_CONF, section_with_changes) - - tasks.restore_context(paths.IDMAPD_CONF) - - print("Configured %s" % paths.IDMAPD_CONF) - - rpcidmapd = services.knownservices.rpcidmapd - statestore.backup_state('rpcidmapd', 'enabled', rpcidmapd.is_enabled()) - statestore.backup_state('rpcidmapd', 'running', rpcidmapd.is_running()) - try: - rpcidmapd.restart() - print("Started %s" % rpcidmapd.service_name) - except Exception as e: - logger.error("%s failed to restart: %s", rpcidmapd.service_name, e) - try: - rpcidmapd.enable() - except Exception as e: - print("Failed to configure automatic startup of the %s daemon" % (rpcidmapd.service_name)) - logger.error("Failed to enable automatic startup of the %s daemon: %s", - rpcidmapd.service_name, str(e)) - - rpcgssd = services.knownservices.rpcgssd - statestore.backup_state('rpcgssd', 'enabled', rpcgssd.is_enabled()) - statestore.backup_state('rpcgssd', 'running', rpcgssd.is_running()) - try: - rpcgssd.restart() - print("Started %s" % rpcgssd.service_name) - except Exception as e: - logger.error("%s failed to restart: %s", rpcgssd.service_name, e) - try: - rpcgssd.enable() - except Exception as e: - print("Failed to configure automatic startup of the %s daemon" % (rpcgssd.service_name)) - logger.error("Failed to enable automatic startup of the %s daemon: %s", - rpcgssd.service_name, str(e)) - -def main(): - try: - check_client_configuration() - except ScriptError as e: - print(e.msg) - sys.exit(e.rval) - - fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) - statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) - - options, _args = parse_options() - - standard_logging_setup( - paths.IPACLIENT_INSTALL_LOG, verbose=False, debug=options.debug, - filemode='a', console_format='%(message)s') - - cfg = dict( - context='cli_installer', - confdir=paths.ETC_IPA, - in_server=False, - debug=options.debug, - verbose=0, - ) - - # Bootstrap API early so that env object is available - api.bootstrap(**cfg) - - if options.uninstall: - return uninstall(fstore, statestore) - - ca_cert_path = None - if os.path.exists(paths.IPA_CA_CRT): - ca_cert_path = paths.IPA_CA_CRT - - if statestore.has_state('autofs'): - print('An automount location is already configured') - sys.exit(CLIENT_ALREADY_CONFIGURED) - - autodiscover = False - ds = ipadiscovery.IPADiscovery() - if not options.server: - print("Searching for IPA server...") - ret = ds.search(ca_cert_path=ca_cert_path) - logger.debug('Executing DNS discovery') - if ret == ipadiscovery.NO_LDAP_SERVER: - logger.debug('Autodiscovery did not find LDAP server') - s = urlsplit(api.env.xmlrpc_uri) - server = [s.netloc] - logger.debug('Setting server to %s', s.netloc) - else: - autodiscover = True - if not ds.servers: - sys.exit('Autodiscovery was successful but didn\'t return a server') - logger.debug('Autodiscovery success, possible servers %s', - ','.join(ds.servers)) - server = ds.servers[0] - else: - server = options.server - logger.debug("Verifying that %s is an IPA server", server) - ldapret = ds.ipacheckldap(server, api.env.realm, ca_cert_path) - if ldapret[0] == ipadiscovery.NO_ACCESS_TO_LDAP: - print("Anonymous access to the LDAP server is disabled.") - print("Proceeding without strict verification.") - print("Note: This is not an error if anonymous access has been explicitly restricted.") - elif ldapret[0] == ipadiscovery.NO_TLS_LDAP: - logger.warning("Unencrypted access to LDAP is not supported.") - elif ldapret[0] != 0: - sys.exit('Unable to confirm that %s is an IPA server' % server) - - if not autodiscover: - print("IPA server: %s" % server) - logger.debug('Using fixed server %s', server) - else: - print("IPA server: DNS discovery") - logger.debug('Configuring to use DNS discovery') - - print("Location: %s" % options.location) - logger.debug('Using automount location %s', options.location) - - ccache_dir = tempfile.mkdtemp() - ccache_name = os.path.join(ccache_dir, 'ccache') - try: - try: - host_princ = str('host/%s@%s' % (api.env.host, api.env.realm)) - kinit_keytab(host_princ, paths.KRB5_KEYTAB, ccache_name) - os.environ['KRB5CCNAME'] = ccache_name - except gssapi.exceptions.GSSError as e: - sys.exit("Failed to obtain host TGT: %s" % e) - - # Finalize API when TGT obtained using host keytab exists - api.finalize() - - # Now we have a TGT, connect to IPA - try: - api.Backend.rpcclient.connect() - except errors.KerberosError as e: - sys.exit('Cannot connect to the server due to ' + str(e)) - try: - # Use the RPC directly so older servers are supported - api.Backend.rpcclient.forward( - 'automountlocation_show', - ipautil.fsdecode(options.location), - version=u'2.0', - ) - except errors.VersionError as e: - sys.exit('This client is incompatible: ' + str(e)) - except errors.NotFound: - sys.exit("Automount location '%s' does not exist" % options.location) - except errors.PublicError as e: - sys.exit("Cannot connect to the server due to generic error: %s" % str(e)) - finally: - os.remove(ccache_name) - os.rmdir(ccache_dir) - - if not options.unattended and not ipautil.user_input("Continue to configure the system with these values?", False): - sys.exit("Installation aborted") - - try: - if not options.sssd: - configure_nsswitch(fstore, options) - configure_nfs(fstore, statestore) - if options.sssd: - configure_autofs_sssd(fstore, statestore, autodiscover, options) - else: - configure_xml(fstore) - configure_autofs(fstore, statestore, autodiscover, server, options) - configure_autofs_common(fstore, statestore, options) - except Exception as e: - logger.debug('Raised exception %s', e) - print("Installation failed. Rolling back changes.") - uninstall(fstore, statestore) - return 1 - - return 0 - -try: - if not os.geteuid()==0: - sys.exit("\nMust be run as root\n") - - sys.exit(main()) -except SystemExit as e: - sys.exit(e) -except RuntimeError as e: - sys.exit(e) -except (KeyboardInterrupt, EOFError): - sys.exit(1) diff --git a/client/ipa-client-automount.in b/client/ipa-client-automount.in new file mode 100644 index 0000000..7348e20 --- /dev/null +++ b/client/ipa-client-automount.in @@ -0,0 +1,543 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Rob Crittenden +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Configure the automount client for ldap. + +from __future__ import print_function + +import logging +import sys +import os +import time +import tempfile +import gssapi + +try: + from xml.etree import cElementTree as etree +except ImportError: + from xml.etree import ElementTree as etree + +import SSSDConfig +# pylint: disable=import-error +from six.moves.urllib.parse import urlsplit +# pylint: enable=import-error + +from optparse import OptionParser # pylint: disable=deprecated-module + +from ipaclient.install import ipachangeconf, ipadiscovery +from ipaclient.install.client import (CLIENT_NOT_CONFIGURED, + CLIENT_ALREADY_CONFIGURED) +from ipalib import api, errors +from ipalib.install import sysrestore +from ipalib.install.kinit import kinit_keytab +from ipalib.util import check_client_configuration +from ipapython import ipautil +from ipapython.ipa_log_manager import standard_logging_setup +from ipapython.dn import DN +from ipaplatform.constants import constants +from ipaplatform.tasks import tasks +from ipaplatform import services +from ipaplatform.paths import paths +from ipapython.admintool import ScriptError + + +logger = logging.getLogger(os.path.basename(__file__)) + + +def parse_options(): + usage = "%prog [options]\n" + parser = OptionParser(usage=usage) + parser.add_option("--server", dest="server", help="FQDN of IPA server") + parser.add_option("--location", dest="location", help="Automount location", + default="default") + parser.add_option("-S", "--no-sssd", dest="sssd", + action="store_false", default=True, + help="Do not configure the client to use SSSD for automount") + parser.add_option("--debug", dest="debug", action="store_true", + default=False, help="enable debugging") + parser.add_option("-U", "--unattended", dest="unattended", + action="store_true", default=False, + help="unattended installation never prompts the user") + parser.add_option("--uninstall", dest="uninstall", action="store_true", + default=False, help="Unconfigure automount") + + options, args = parser.parse_args() + return options, args + +def wait_for_sssd(): + """ + It takes a bit for sssd to get going, lets loop until it is + serving data. + + This function returns nothing. + """ + n = 0 + found = False + time.sleep(1) + while n < 10 and not found: + try: + ipautil.run([paths.GETENT, "passwd", "admin@%s" % api.env.realm]) + found = True + except Exception: + time.sleep(1) + n = n + 1 + + # This should never happen but if it does, may as well warn the user + if not found: + err_msg = ("Unable to find 'admin' user with " + "'getent passwd admin@%s'!" % api.env.realm) + logger.debug('%s', err_msg) + print(err_msg) + print("This may mean that sssd didn't re-start properly after the configuration changes.") + +def configure_xml(fstore): + authconf = paths.AUTOFS_LDAP_AUTH_CONF + fstore.backup_file(authconf) + + try: + tree = etree.parse(authconf) + except IOError as e: + logger.debug('Unable to open file %s', e) + logger.debug('Creating new from template') + tree = etree.ElementTree( + element=etree.Element('autofs_ldap_sasl_conf') + ) + + element = tree.getroot() + if element.tag != 'autofs_ldap_sasl_conf': + raise RuntimeError('Invalid XML root in file %s' % authconf) + + element.set('usetls', 'no') + element.set('tlsrequired', 'no') + element.set('authrequired', 'yes') + element.set('authtype', 'GSSAPI') + element.set('clientprinc', 'host/%s@%s' % (api.env.host, api.env.realm)) + + try: + tree.write(authconf, xml_declaration=True, encoding='UTF-8') + except IOError as e: + print("Unable to write %s: %s" % (authconf, e)) + else: + print("Configured %s" % authconf) + +def configure_nsswitch(fstore, options): + """ + Point automount to ldap in nsswitch.conf. This function is for non-SSSD + setups only + """ + fstore.backup_file(paths.NSSWITCH_CONF) + + conf = ipachangeconf.IPAChangeConf("IPA Installer") + conf.setOptionAssignment(':') + + nss_value = ' files ldap' + + opts = [{'name':'automount', 'type':'option', 'action':'set', 'value':nss_value}, + {'name':'empty', 'type':'empty'}] + + conf.changeConf(paths.NSSWITCH_CONF, opts) + + print("Configured %s" % paths.NSSWITCH_CONF) + +def configure_autofs_sssd(fstore, statestore, autodiscover, options): + try: + sssdconfig = SSSDConfig.SSSDConfig() + sssdconfig.import_config() + domains = sssdconfig.list_active_domains() + except Exception as e: + sys.exit(e) + + try: + sssdconfig.new_service('autofs') + except SSSDConfig.ServiceAlreadyExists: + pass + except SSSDConfig.ServiceNotRecognizedError: + logger.error("Unable to activate the Autofs service in SSSD config.") + logger.info( + "Please make sure you have SSSD built with autofs support " + "installed.") + logger.info( + "Configure autofs support manually in /etc/sssd/sssd.conf.") + sys.exit("Cannot create the autofs service in sssd.conf") + + sssdconfig.activate_service('autofs') + + domain = None + for name in domains: + domain = sssdconfig.get_domain(name) + try: + provider = domain.get_option('id_provider') + except SSSDConfig.NoOptionError: + continue + if provider == "ipa": + domain.add_provider('ipa', 'autofs') + try: + domain.get_option('ipa_automount_location') + print('An automount location is already configured') + sys.exit(CLIENT_ALREADY_CONFIGURED) + except SSSDConfig.NoOptionError: + domain.set_option('ipa_automount_location', options.location) + break + + if domain is None: + sys.exit('SSSD is not configured.') + + sssdconfig.save_domain(domain) + sssdconfig.write(paths.SSSD_CONF) + statestore.backup_state('autofs', 'sssd', True) + + sssd = services.service('sssd', api) + sssd.restart() + print("Restarting sssd, waiting for it to become available.") + wait_for_sssd() + +def configure_autofs(fstore, statestore, autodiscover, server, options): + """ + fstore: the FileStore to back up files in + options.server: the IPA server to use + options.location: the Automount location to use + """ + if not autodiscover: + ldap_uri = "ldap://%s" % server + else: + ldap_uri = "ldap:///%s" % api.env.basedn + + search_base = str(DN(('cn', options.location), api.env.container_automount, api.env.basedn)) + replacevars = { + 'MAP_OBJECT_CLASS': 'automountMap', + 'ENTRY_OBJECT_CLASS': 'automount', + 'MAP_ATTRIBUTE': 'automountMapName', + 'ENTRY_ATTRIBUTE': 'automountKey', + 'VALUE_ATTRIBUTE': 'automountInformation', + 'SEARCH_BASE': search_base, + 'LDAP_URI': ldap_uri, + } + + ipautil.backup_config_and_replace_variables(fstore, + paths.SYSCONFIG_AUTOFS, replacevars=replacevars) + tasks.restore_context(paths.SYSCONFIG_AUTOFS) + statestore.backup_state('autofs', 'sssd', False) + + print("Configured %s" % paths.SYSCONFIG_AUTOFS) + +def configure_autofs_common(fstore, statestore, options): + autofs = services.knownservices.autofs + statestore.backup_state('autofs', 'enabled', autofs.is_enabled()) + statestore.backup_state('autofs', 'running', autofs.is_running()) + try: + autofs.restart() + print("Started %s" % autofs.service_name) + except Exception as e: + logger.error("%s failed to restart: %s", autofs.service_name, e) + try: + autofs.enable() + except Exception as e: + print("Failed to configure automatic startup of the %s daemon" % (autofs.service_name)) + logger.error("Failed to enable automatic startup of the %s daemon: %s", + autofs.service_name, str(e)) + +def uninstall(fstore, statestore): + RESTORE_FILES=[ + paths.SYSCONFIG_AUTOFS, + paths.NSSWITCH_CONF, + paths.AUTOFS_LDAP_AUTH_CONF, + paths.SYSCONFIG_NFS, + paths.IDMAPD_CONF, + ] + STATES=['autofs', 'rpcidmapd', 'rpcgssd'] + + # automount only touches /etc/nsswitch.conf if LDAP is + # used. Don't restore it otherwise. + if (statestore.get_state('authconfig', 'sssd') or + (statestore.get_state('authselect', 'profile') == 'sssd')): + RESTORE_FILES.remove(paths.NSSWITCH_CONF) + + if (not any(fstore.has_file(f) for f in RESTORE_FILES) or + not any(statestore.has_state(s) for s in STATES)): + print("IPA automount is not configured on this system") + return CLIENT_NOT_CONFIGURED + + print("Restoring configuration") + + for filepath in RESTORE_FILES: + if fstore.has_file(filepath): + fstore.restore_file(filepath) + if statestore.has_state('autofs'): + enabled = statestore.restore_state('autofs', 'enabled') + running = statestore.restore_state('autofs', 'running') + sssd = statestore.restore_state('autofs', 'sssd') + autofs = services.knownservices.autofs + if not enabled: + autofs.disable() + if not running: + autofs.stop() + if sssd: + try: + sssdconfig = SSSDConfig.SSSDConfig() + sssdconfig.import_config() + sssdconfig.deactivate_service('autofs') + domains = sssdconfig.list_active_domains() + for name in domains: + domain = sssdconfig.get_domain(name) + try: + provider = domain.get_option('id_provider') + except SSSDConfig.NoOptionError: + continue + if provider == "ipa": + domain.remove_option('ipa_automount_location') + domain.remove_provider('autofs') + break + sssdconfig.save_domain(domain) + sssdconfig.write(paths.SSSD_CONF) + sssd = services.service('sssd', api) + sssd.restart() + wait_for_sssd() + except Exception as e: + print('Unable to restore SSSD configuration: %s' % str(e)) + logger.debug('Unable to restore SSSD configuration: %s', + str(e)) + if statestore.has_state('rpcidmapd'): + enabled = statestore.restore_state('rpcidmapd', 'enabled') + running = statestore.restore_state('rpcidmapd', 'running') + rpcidmapd = services.knownservices.rpcidmapd + if not enabled: + rpcidmapd.disable() + if not running: + rpcidmapd.stop() + if statestore.has_state('rpcgssd'): + enabled = statestore.restore_state('rpcgssd', 'enabled') + running = statestore.restore_state('rpcgssd', 'running') + rpcgssd = services.knownservices.rpcgssd + if not enabled: + rpcgssd.disable() + if not running: + rpcgssd.stop() + + return 0 + +def configure_nfs(fstore, statestore): + """ + Configure secure NFS + """ + replacevars = { + constants.SECURE_NFS_VAR: 'yes', + } + ipautil.backup_config_and_replace_variables(fstore, + paths.SYSCONFIG_NFS, replacevars=replacevars) + tasks.restore_context(paths.SYSCONFIG_NFS) + + print("Configured %s" % paths.SYSCONFIG_NFS) + + # Prepare the changes + # We need to use IPAChangeConf as simple regexp substitution + # does not cut it here + conf = ipachangeconf.IPAChangeConf("IPA automount installer") + conf.case_insensitive_sections = False + conf.setOptionAssignment(" = ") + conf.setSectionNameDelimiters(("[", "]")) + + changes = [conf.setOption('Domain', api.env.domain)] + section_with_changes = [conf.setSection('General', changes)] + + # Backup the file and apply the changes + fstore.backup_file(paths.IDMAPD_CONF) + conf.changeConf(paths.IDMAPD_CONF, section_with_changes) + + tasks.restore_context(paths.IDMAPD_CONF) + + print("Configured %s" % paths.IDMAPD_CONF) + + rpcidmapd = services.knownservices.rpcidmapd + statestore.backup_state('rpcidmapd', 'enabled', rpcidmapd.is_enabled()) + statestore.backup_state('rpcidmapd', 'running', rpcidmapd.is_running()) + try: + rpcidmapd.restart() + print("Started %s" % rpcidmapd.service_name) + except Exception as e: + logger.error("%s failed to restart: %s", rpcidmapd.service_name, e) + try: + rpcidmapd.enable() + except Exception as e: + print("Failed to configure automatic startup of the %s daemon" % (rpcidmapd.service_name)) + logger.error("Failed to enable automatic startup of the %s daemon: %s", + rpcidmapd.service_name, str(e)) + + rpcgssd = services.knownservices.rpcgssd + statestore.backup_state('rpcgssd', 'enabled', rpcgssd.is_enabled()) + statestore.backup_state('rpcgssd', 'running', rpcgssd.is_running()) + try: + rpcgssd.restart() + print("Started %s" % rpcgssd.service_name) + except Exception as e: + logger.error("%s failed to restart: %s", rpcgssd.service_name, e) + try: + rpcgssd.enable() + except Exception as e: + print("Failed to configure automatic startup of the %s daemon" % (rpcgssd.service_name)) + logger.error("Failed to enable automatic startup of the %s daemon: %s", + rpcgssd.service_name, str(e)) + +def main(): + try: + check_client_configuration() + except ScriptError as e: + print(e.msg) + sys.exit(e.rval) + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + options, _args = parse_options() + + standard_logging_setup( + paths.IPACLIENT_INSTALL_LOG, verbose=False, debug=options.debug, + filemode='a', console_format='%(message)s') + + cfg = dict( + context='cli_installer', + confdir=paths.ETC_IPA, + in_server=False, + debug=options.debug, + verbose=0, + ) + + # Bootstrap API early so that env object is available + api.bootstrap(**cfg) + + if options.uninstall: + return uninstall(fstore, statestore) + + ca_cert_path = None + if os.path.exists(paths.IPA_CA_CRT): + ca_cert_path = paths.IPA_CA_CRT + + if statestore.has_state('autofs'): + print('An automount location is already configured') + sys.exit(CLIENT_ALREADY_CONFIGURED) + + autodiscover = False + ds = ipadiscovery.IPADiscovery() + if not options.server: + print("Searching for IPA server...") + ret = ds.search(ca_cert_path=ca_cert_path) + logger.debug('Executing DNS discovery') + if ret == ipadiscovery.NO_LDAP_SERVER: + logger.debug('Autodiscovery did not find LDAP server') + s = urlsplit(api.env.xmlrpc_uri) + server = [s.netloc] + logger.debug('Setting server to %s', s.netloc) + else: + autodiscover = True + if not ds.servers: + sys.exit('Autodiscovery was successful but didn\'t return a server') + logger.debug('Autodiscovery success, possible servers %s', + ','.join(ds.servers)) + server = ds.servers[0] + else: + server = options.server + logger.debug("Verifying that %s is an IPA server", server) + ldapret = ds.ipacheckldap(server, api.env.realm, ca_cert_path) + if ldapret[0] == ipadiscovery.NO_ACCESS_TO_LDAP: + print("Anonymous access to the LDAP server is disabled.") + print("Proceeding without strict verification.") + print("Note: This is not an error if anonymous access has been explicitly restricted.") + elif ldapret[0] == ipadiscovery.NO_TLS_LDAP: + logger.warning("Unencrypted access to LDAP is not supported.") + elif ldapret[0] != 0: + sys.exit('Unable to confirm that %s is an IPA server' % server) + + if not autodiscover: + print("IPA server: %s" % server) + logger.debug('Using fixed server %s', server) + else: + print("IPA server: DNS discovery") + logger.debug('Configuring to use DNS discovery') + + print("Location: %s" % options.location) + logger.debug('Using automount location %s', options.location) + + ccache_dir = tempfile.mkdtemp() + ccache_name = os.path.join(ccache_dir, 'ccache') + try: + try: + host_princ = str('host/%s@%s' % (api.env.host, api.env.realm)) + kinit_keytab(host_princ, paths.KRB5_KEYTAB, ccache_name) + os.environ['KRB5CCNAME'] = ccache_name + except gssapi.exceptions.GSSError as e: + sys.exit("Failed to obtain host TGT: %s" % e) + + # Finalize API when TGT obtained using host keytab exists + api.finalize() + + # Now we have a TGT, connect to IPA + try: + api.Backend.rpcclient.connect() + except errors.KerberosError as e: + sys.exit('Cannot connect to the server due to ' + str(e)) + try: + # Use the RPC directly so older servers are supported + api.Backend.rpcclient.forward( + 'automountlocation_show', + ipautil.fsdecode(options.location), + version=u'2.0', + ) + except errors.VersionError as e: + sys.exit('This client is incompatible: ' + str(e)) + except errors.NotFound: + sys.exit("Automount location '%s' does not exist" % options.location) + except errors.PublicError as e: + sys.exit("Cannot connect to the server due to generic error: %s" % str(e)) + finally: + os.remove(ccache_name) + os.rmdir(ccache_dir) + + if not options.unattended and not ipautil.user_input("Continue to configure the system with these values?", False): + sys.exit("Installation aborted") + + try: + if not options.sssd: + configure_nsswitch(fstore, options) + configure_nfs(fstore, statestore) + if options.sssd: + configure_autofs_sssd(fstore, statestore, autodiscover, options) + else: + configure_xml(fstore) + configure_autofs(fstore, statestore, autodiscover, server, options) + configure_autofs_common(fstore, statestore, options) + except Exception as e: + logger.debug('Raised exception %s', e) + print("Installation failed. Rolling back changes.") + uninstall(fstore, statestore) + return 1 + + return 0 + +try: + if not os.geteuid()==0: + sys.exit("\nMust be run as root\n") + + sys.exit(main()) +except SystemExit as e: + sys.exit(e) +except RuntimeError as e: + sys.exit(e) +except (KeyboardInterrupt, EOFError): + sys.exit(1) diff --git a/client/ipa-client-install b/client/ipa-client-install deleted file mode 100755 index bb8cb1e..0000000 --- a/client/ipa-client-install +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Simo Sorce -# Karl MacMillan -# -# Copyright (C) 2007 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaclient.install import ipa_client_install - -ipa_client_install.run() diff --git a/client/ipa-client-install.in b/client/ipa-client-install.in new file mode 100644 index 0000000..3cfd6ad --- /dev/null +++ b/client/ipa-client-install.in @@ -0,0 +1,24 @@ +@PYTHONSHEBANG@ +# Authors: Simo Sorce +# Karl MacMillan +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaclient.install import ipa_client_install + +ipa_client_install.run() diff --git a/daemons/dnssec/ipa-dnskeysync-replica b/daemons/dnssec/ipa-dnskeysync-replica deleted file mode 100755 index e2be3ea..0000000 --- a/daemons/dnssec/ipa-dnskeysync-replica +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 FreeIPA Contributors see COPYING for license -# -""" -Download keys from LDAP to local HSM. - -This program should be run only on replicas, not on DNSSEC masters. -""" - -from gssapi.exceptions import GSSError -import logging -import os -import sys - -import ipalib -from ipalib.constants import SOFTHSM_DNSSEC_TOKEN_LABEL -from ipalib.install.kinit import kinit_keytab -from ipapython.dn import DN -from ipapython.ipa_log_manager import standard_logging_setup -from ipapython import ipaldap -from ipaplatform.paths import paths -from ipaserver.dnssec.abshsm import (sync_pkcs11_metadata, - ldap2p11helper_api_params, - wrappingmech_name2id) -from ipaserver.dnssec.ldapkeydb import LdapKeyDB, str_hexlify -from ipaserver.dnssec.localhsm import LocalHSM - -logger = logging.getLogger(os.path.basename(__file__)) - -DAEMONNAME = 'ipa-dnskeysyncd' -PRINCIPAL = None # not initialized yet -WORKDIR = '/tmp' - -def hex_set(s): - out = set() - for i in s: - out.add("0x%s" % str_hexlify(i)) - return out - -def update_metadata_set(source_set, target_set): - """sync metadata from source key set to target key set - - Keys not present in both sets are left intact.""" - name = 'sync_metadata' - matching_keys = set(source_set.keys()).intersection(set(target_set.keys())) - logger.info("%s: keys in local HSM & LDAP: %s", - name, hex_set(matching_keys)) - for key_id in matching_keys: - sync_pkcs11_metadata(name, source_set[key_id], target_set[key_id]) - - -def find_unwrapping_key(localhsm, wrapping_key_uri): - wrap_keys = localhsm.find_keys(uri=wrapping_key_uri) - # find usable unwrapping key with matching ID - for key_id in wrap_keys.keys(): - unwrap_keys = localhsm.find_keys(id=key_id, cka_unwrap=True) - if len(unwrap_keys) > 0: - return unwrap_keys.popitem()[1] - -def ldap2replica_master_keys_sync(ldapkeydb, localhsm): - ## LDAP -> replica master key synchronization - # import new master keys from LDAP - new_keys = set(ldapkeydb.master_keys.keys()) \ - - set(localhsm.master_keys.keys()) - logger.debug("master keys in local HSM: %s", - hex_set(localhsm.master_keys.keys())) - logger.debug("master keys in LDAP HSM: %s", - hex_set(ldapkeydb.master_keys.keys())) - logger.debug("new master keys in LDAP HSM: %s", - hex_set(new_keys)) - for mkey_id in new_keys: - mkey_ldap = ldapkeydb.master_keys[mkey_id] - if not mkey_ldap.wrapped_entries: - raise ValueError( - "Master key 0x%s in LDAP is missing key material " - "referenced by ipaSecretKeyRefObject attribute" % - str_hexlify(mkey_id) - ) - for wrapped_ldap in mkey_ldap.wrapped_entries: - unwrapping_key = find_unwrapping_key( - localhsm, wrapped_ldap.single_value['ipaWrappingKey']) - if unwrapping_key: - break - - # TODO: Could it happen in normal cases? - if unwrapping_key is None: - raise ValueError( - "Local HSM does not contain suitable unwrapping key " - "for master key 0x%s" % str_hexlify(mkey_id) - ) - - params = ldap2p11helper_api_params(mkey_ldap) - params['data'] = wrapped_ldap.single_value['ipaSecretKey'] - params['unwrapping_key'] = unwrapping_key.handle - params['wrapping_mech'] = wrappingmech_name2id[wrapped_ldap.single_value['ipaWrappingMech']] - logger.debug('Importing new master key: 0x%s %s', - str_hexlify(mkey_id), params) - localhsm.p11.import_wrapped_secret_key(**params) - - # synchronize metadata about master keys in LDAP - update_metadata_set(ldapkeydb.master_keys, localhsm.master_keys) - -def ldap2replica_zone_keys_sync(ldapkeydb, localhsm): - ## LDAP -> replica zone key synchronization - # import new zone keys from LDAP - new_keys = set(ldapkeydb.zone_keypairs.keys()) \ - - set(localhsm.zone_privkeys.keys()) - - logger.debug("zone keys in local HSM: %s", - hex_set(localhsm.master_keys.keys())) - logger.debug("zone keys in LDAP HSM: %s", - hex_set(ldapkeydb.master_keys.keys())) - logger.debug("new zone keys in LDAP HSM: %s", - hex_set(new_keys)) - for zkey_id in new_keys: - zkey_ldap = ldapkeydb.zone_keypairs[zkey_id] - logger.debug('Looking for unwrapping key "%s" for zone key 0x%s', - zkey_ldap['ipaWrappingKey'], str_hexlify(zkey_id)) - unwrapping_key = find_unwrapping_key( - localhsm, zkey_ldap['ipaWrappingKey']) - if unwrapping_key is None: - raise ValueError( - "Local HSM does not contain suitable unwrapping key for " - "zone key 0x%s" % str_hexlify(zkey_id) - ) - - logger.debug('Importing zone key pair 0x%s', str_hexlify(zkey_id)) - localhsm.import_private_key(zkey_ldap, zkey_ldap['ipaPrivateKey'], - unwrapping_key) - localhsm.import_public_key(zkey_ldap, zkey_ldap['ipaPublicKey']) - - # synchronize metadata about zone keys in LDAP & local HSM - update_metadata_set(ldapkeydb.master_keys, localhsm.master_keys) - - # delete keys removed from LDAP - deleted_keys = set(localhsm.zone_privkeys.keys()) \ - - set(ldapkeydb.zone_keypairs.keys()) - - for zkey_id in deleted_keys: - localhsm.p11.delete_key(localhsm.zone_pubkeys[zkey_id].handle) - localhsm.p11.delete_key(localhsm.zone_privkeys[zkey_id].handle) - - -# IPA framework initialization -standard_logging_setup(verbose=True, debug=True) -ipalib.api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True) -ipalib.api.finalize() - -# Kerberos initialization -PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host)) -logger.debug('Kerberos principal: %s', PRINCIPAL) -ccache_filename = os.path.join(WORKDIR, 'ipa-dnskeysync-replica.ccache') - -try: - kinit_keytab(PRINCIPAL, paths.IPA_DNSKEYSYNCD_KEYTAB, ccache_filename, - attempts=5) -except GSSError as e: - logger.critical('Kerberos authentication failed: %s', e) - sys.exit(1) - -os.environ['KRB5CCNAME'] = ccache_filename -logger.debug('Got TGT') - -# LDAP initialization -ldap = ipaldap.LDAPClient(ipalib.api.env.ldap_uri) -logger.debug('Connecting to LDAP') -ldap.gssapi_bind() -logger.debug('Connected') - - -### DNSSEC master: key synchronization -ldapkeydb = LdapKeyDB(ldap, DN(('cn', 'keys'), - ('cn', 'sec'), - ipalib.api.env.container_dns, - ipalib.api.env.basedn)) - -localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, SOFTHSM_DNSSEC_TOKEN_LABEL, - open(paths.DNSSEC_SOFTHSM_PIN).read()) - -ldap2replica_master_keys_sync(ldapkeydb, localhsm) -ldap2replica_zone_keys_sync(ldapkeydb, localhsm) - -sys.exit(0) diff --git a/daemons/dnssec/ipa-dnskeysync-replica.in b/daemons/dnssec/ipa-dnskeysync-replica.in new file mode 100644 index 0000000..3a7b04c --- /dev/null +++ b/daemons/dnssec/ipa-dnskeysync-replica.in @@ -0,0 +1,184 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# +""" +Download keys from LDAP to local HSM. + +This program should be run only on replicas, not on DNSSEC masters. +""" + +from gssapi.exceptions import GSSError +import logging +import os +import sys + +import ipalib +from ipalib.constants import SOFTHSM_DNSSEC_TOKEN_LABEL +from ipalib.install.kinit import kinit_keytab +from ipapython.dn import DN +from ipapython.ipa_log_manager import standard_logging_setup +from ipapython import ipaldap +from ipaplatform.paths import paths +from ipaserver.dnssec.abshsm import (sync_pkcs11_metadata, + ldap2p11helper_api_params, + wrappingmech_name2id) +from ipaserver.dnssec.ldapkeydb import LdapKeyDB, str_hexlify +from ipaserver.dnssec.localhsm import LocalHSM + +logger = logging.getLogger(os.path.basename(__file__)) + +DAEMONNAME = 'ipa-dnskeysyncd' +PRINCIPAL = None # not initialized yet +WORKDIR = '/tmp' + +def hex_set(s): + out = set() + for i in s: + out.add("0x%s" % str_hexlify(i)) + return out + +def update_metadata_set(source_set, target_set): + """sync metadata from source key set to target key set + + Keys not present in both sets are left intact.""" + name = 'sync_metadata' + matching_keys = set(source_set.keys()).intersection(set(target_set.keys())) + logger.info("%s: keys in local HSM & LDAP: %s", + name, hex_set(matching_keys)) + for key_id in matching_keys: + sync_pkcs11_metadata(name, source_set[key_id], target_set[key_id]) + + +def find_unwrapping_key(localhsm, wrapping_key_uri): + wrap_keys = localhsm.find_keys(uri=wrapping_key_uri) + # find usable unwrapping key with matching ID + for key_id in wrap_keys.keys(): + unwrap_keys = localhsm.find_keys(id=key_id, cka_unwrap=True) + if len(unwrap_keys) > 0: + return unwrap_keys.popitem()[1] + +def ldap2replica_master_keys_sync(ldapkeydb, localhsm): + ## LDAP -> replica master key synchronization + # import new master keys from LDAP + new_keys = set(ldapkeydb.master_keys.keys()) \ + - set(localhsm.master_keys.keys()) + logger.debug("master keys in local HSM: %s", + hex_set(localhsm.master_keys.keys())) + logger.debug("master keys in LDAP HSM: %s", + hex_set(ldapkeydb.master_keys.keys())) + logger.debug("new master keys in LDAP HSM: %s", + hex_set(new_keys)) + for mkey_id in new_keys: + mkey_ldap = ldapkeydb.master_keys[mkey_id] + if not mkey_ldap.wrapped_entries: + raise ValueError( + "Master key 0x%s in LDAP is missing key material " + "referenced by ipaSecretKeyRefObject attribute" % + str_hexlify(mkey_id) + ) + for wrapped_ldap in mkey_ldap.wrapped_entries: + unwrapping_key = find_unwrapping_key( + localhsm, wrapped_ldap.single_value['ipaWrappingKey']) + if unwrapping_key: + break + + # TODO: Could it happen in normal cases? + if unwrapping_key is None: + raise ValueError( + "Local HSM does not contain suitable unwrapping key " + "for master key 0x%s" % str_hexlify(mkey_id) + ) + + params = ldap2p11helper_api_params(mkey_ldap) + params['data'] = wrapped_ldap.single_value['ipaSecretKey'] + params['unwrapping_key'] = unwrapping_key.handle + params['wrapping_mech'] = wrappingmech_name2id[wrapped_ldap.single_value['ipaWrappingMech']] + logger.debug('Importing new master key: 0x%s %s', + str_hexlify(mkey_id), params) + localhsm.p11.import_wrapped_secret_key(**params) + + # synchronize metadata about master keys in LDAP + update_metadata_set(ldapkeydb.master_keys, localhsm.master_keys) + +def ldap2replica_zone_keys_sync(ldapkeydb, localhsm): + ## LDAP -> replica zone key synchronization + # import new zone keys from LDAP + new_keys = set(ldapkeydb.zone_keypairs.keys()) \ + - set(localhsm.zone_privkeys.keys()) + + logger.debug("zone keys in local HSM: %s", + hex_set(localhsm.master_keys.keys())) + logger.debug("zone keys in LDAP HSM: %s", + hex_set(ldapkeydb.master_keys.keys())) + logger.debug("new zone keys in LDAP HSM: %s", + hex_set(new_keys)) + for zkey_id in new_keys: + zkey_ldap = ldapkeydb.zone_keypairs[zkey_id] + logger.debug('Looking for unwrapping key "%s" for zone key 0x%s', + zkey_ldap['ipaWrappingKey'], str_hexlify(zkey_id)) + unwrapping_key = find_unwrapping_key( + localhsm, zkey_ldap['ipaWrappingKey']) + if unwrapping_key is None: + raise ValueError( + "Local HSM does not contain suitable unwrapping key for " + "zone key 0x%s" % str_hexlify(zkey_id) + ) + + logger.debug('Importing zone key pair 0x%s', str_hexlify(zkey_id)) + localhsm.import_private_key(zkey_ldap, zkey_ldap['ipaPrivateKey'], + unwrapping_key) + localhsm.import_public_key(zkey_ldap, zkey_ldap['ipaPublicKey']) + + # synchronize metadata about zone keys in LDAP & local HSM + update_metadata_set(ldapkeydb.master_keys, localhsm.master_keys) + + # delete keys removed from LDAP + deleted_keys = set(localhsm.zone_privkeys.keys()) \ + - set(ldapkeydb.zone_keypairs.keys()) + + for zkey_id in deleted_keys: + localhsm.p11.delete_key(localhsm.zone_pubkeys[zkey_id].handle) + localhsm.p11.delete_key(localhsm.zone_privkeys[zkey_id].handle) + + +# IPA framework initialization +standard_logging_setup(verbose=True, debug=True) +ipalib.api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True) +ipalib.api.finalize() + +# Kerberos initialization +PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host)) +logger.debug('Kerberos principal: %s', PRINCIPAL) +ccache_filename = os.path.join(WORKDIR, 'ipa-dnskeysync-replica.ccache') + +try: + kinit_keytab(PRINCIPAL, paths.IPA_DNSKEYSYNCD_KEYTAB, ccache_filename, + attempts=5) +except GSSError as e: + logger.critical('Kerberos authentication failed: %s', e) + sys.exit(1) + +os.environ['KRB5CCNAME'] = ccache_filename +logger.debug('Got TGT') + +# LDAP initialization +ldap = ipaldap.LDAPClient(ipalib.api.env.ldap_uri) +logger.debug('Connecting to LDAP') +ldap.gssapi_bind() +logger.debug('Connected') + + +### DNSSEC master: key synchronization +ldapkeydb = LdapKeyDB(ldap, DN(('cn', 'keys'), + ('cn', 'sec'), + ipalib.api.env.container_dns, + ipalib.api.env.basedn)) + +localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, SOFTHSM_DNSSEC_TOKEN_LABEL, + open(paths.DNSSEC_SOFTHSM_PIN).read()) + +ldap2replica_master_keys_sync(ldapkeydb, localhsm) +ldap2replica_zone_keys_sync(ldapkeydb, localhsm) + +sys.exit(0) diff --git a/daemons/dnssec/ipa-dnskeysyncd b/daemons/dnssec/ipa-dnskeysyncd deleted file mode 100755 index 83df512..0000000 --- a/daemons/dnssec/ipa-dnskeysyncd +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 FreeIPA Contributors see COPYING for license -# - -import logging -import sys -import ldap -import ldapurl -import os -import signal -import time - -from ipalib import api -from ipalib.install.kinit import kinit_keytab -from ipapython.dn import DN -from ipapython.ipa_log_manager import standard_logging_setup -from ipapython import ipaldap -from ipaplatform.paths import paths -from ipaserver.dnssec.keysyncer import KeySyncer - -logger = logging.getLogger(os.path.basename(__file__)) - - -# IPA framework initialization -standard_logging_setup(verbose=True) -api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True) -api.finalize() -if api.env.debug: - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - -# Global state -watcher_running = True -ldap_connection = False - -DAEMONNAME = 'ipa-dnskeysyncd' -PRINCIPAL = None # not initialized yet -WORKDIR = '/tmp' # private temp -KEYTAB_FB = paths.IPA_DNSKEYSYNCD_KEYTAB - -# Shutdown handler -def commenceShutdown(signum, stack): - # Declare the needed global variables - global watcher_running - global ldap_connection # pylint: disable=global-variable-not-assigned - - logger.info('Signal %s received: Shutting down!', signum) - - # We are no longer running - watcher_running = False - - # Tear down the server connection - if ldap_connection: - ldap_connection.close_db() - del ldap_connection - - # Shutdown - sys.exit(0) - - -os.umask(0o07) - -# Signal handlers -signal.signal(signal.SIGTERM, commenceShutdown) -signal.signal(signal.SIGINT, commenceShutdown) - -# Kerberos initialization -PRINCIPAL = str('%s/%s' % (DAEMONNAME, api.env.host)) -logger.debug('Kerberos principal: %s', PRINCIPAL) -ccache_filename = os.path.join(WORKDIR, 'ipa-dnskeysyncd.ccache') -try: - kinit_keytab(PRINCIPAL, KEYTAB_FB, ccache_filename, attempts=5) -except Exception as ex: - logger.critical("Kerberos authentication failed: %s", ex) - # signal failure and let init system to restart the daemon - sys.exit(1) -os.environ['KRB5CCNAME'] = ccache_filename - -# LDAP initialization -basedn = DN(api.env.container_dns, api.env.basedn) -ldap_url = ldapurl.LDAPUrl(api.env.ldap_uri) -ldap_url.dn = str(basedn) -ldap_url.scope = ldapurl.LDAP_SCOPE_SUBTREE -ldap_url.filterstr = '(|(objectClass=idnsZone)(objectClass=idnsSecKey)(objectClass=ipk11PublicKey))' -logger.debug('LDAP URL: %s', ldap_url.unparse()) - -# Real work -while watcher_running: - # Prepare the LDAP server connection (triggers the connection as well) - ldap_connection = KeySyncer(ldap_url.initializeUrl(), ipa_api=api) - - # Now we login to the LDAP server - try: - logger.info('LDAP bind...') - ldap_connection.sasl_interactive_bind_s("", ipaldap.SASL_GSSAPI) - except ldap.INVALID_CREDENTIALS as e: - logger.exception('Login to LDAP server failed: %s', e) - sys.exit(1) - except ldap.SERVER_DOWN as e: - logger.exception('LDAP server is down, going to retry: %s', e) - time.sleep(5) - continue - - # Commence the syncing - logger.info('Commencing sync process') - ldap_search = ldap_connection.syncrepl_search( - ldap_url.dn, - ldap_url.scope, - mode='refreshAndPersist', - attrlist=ldap_url.attrs, - filterstr=ldap_url.filterstr - ) - - try: - while ldap_connection.syncrepl_poll(all=1, msgid=ldap_search): - pass - except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR) as e: - logger.exception('syncrepl_poll: LDAP error (%s)', e) - sys.exit(1) diff --git a/daemons/dnssec/ipa-dnskeysyncd.in b/daemons/dnssec/ipa-dnskeysyncd.in new file mode 100644 index 0000000..7c669b5 --- /dev/null +++ b/daemons/dnssec/ipa-dnskeysyncd.in @@ -0,0 +1,120 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import logging +import sys +import ldap +import ldapurl +import os +import signal +import time + +from ipalib import api +from ipalib.install.kinit import kinit_keytab +from ipapython.dn import DN +from ipapython.ipa_log_manager import standard_logging_setup +from ipapython import ipaldap +from ipaplatform.paths import paths +from ipaserver.dnssec.keysyncer import KeySyncer + +logger = logging.getLogger(os.path.basename(__file__)) + + +# IPA framework initialization +standard_logging_setup(verbose=True) +api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True) +api.finalize() +if api.env.debug: + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + +# Global state +watcher_running = True +ldap_connection = False + +DAEMONNAME = 'ipa-dnskeysyncd' +PRINCIPAL = None # not initialized yet +WORKDIR = '/tmp' # private temp +KEYTAB_FB = paths.IPA_DNSKEYSYNCD_KEYTAB + +# Shutdown handler +def commenceShutdown(signum, stack): + # Declare the needed global variables + global watcher_running + global ldap_connection # pylint: disable=global-variable-not-assigned + + logger.info('Signal %s received: Shutting down!', signum) + + # We are no longer running + watcher_running = False + + # Tear down the server connection + if ldap_connection: + ldap_connection.close_db() + del ldap_connection + + # Shutdown + sys.exit(0) + + +os.umask(0o07) + +# Signal handlers +signal.signal(signal.SIGTERM, commenceShutdown) +signal.signal(signal.SIGINT, commenceShutdown) + +# Kerberos initialization +PRINCIPAL = str('%s/%s' % (DAEMONNAME, api.env.host)) +logger.debug('Kerberos principal: %s', PRINCIPAL) +ccache_filename = os.path.join(WORKDIR, 'ipa-dnskeysyncd.ccache') +try: + kinit_keytab(PRINCIPAL, KEYTAB_FB, ccache_filename, attempts=5) +except Exception as ex: + logger.critical("Kerberos authentication failed: %s", ex) + # signal failure and let init system to restart the daemon + sys.exit(1) +os.environ['KRB5CCNAME'] = ccache_filename + +# LDAP initialization +basedn = DN(api.env.container_dns, api.env.basedn) +ldap_url = ldapurl.LDAPUrl(api.env.ldap_uri) +ldap_url.dn = str(basedn) +ldap_url.scope = ldapurl.LDAP_SCOPE_SUBTREE +ldap_url.filterstr = '(|(objectClass=idnsZone)(objectClass=idnsSecKey)(objectClass=ipk11PublicKey))' +logger.debug('LDAP URL: %s', ldap_url.unparse()) + +# Real work +while watcher_running: + # Prepare the LDAP server connection (triggers the connection as well) + ldap_connection = KeySyncer(ldap_url.initializeUrl(), ipa_api=api) + + # Now we login to the LDAP server + try: + logger.info('LDAP bind...') + ldap_connection.sasl_interactive_bind_s("", ipaldap.SASL_GSSAPI) + except ldap.INVALID_CREDENTIALS as e: + logger.exception('Login to LDAP server failed: %s', e) + sys.exit(1) + except ldap.SERVER_DOWN as e: + logger.exception('LDAP server is down, going to retry: %s', e) + time.sleep(5) + continue + + # Commence the syncing + logger.info('Commencing sync process') + ldap_search = ldap_connection.syncrepl_search( + ldap_url.dn, + ldap_url.scope, + mode='refreshAndPersist', + attrlist=ldap_url.attrs, + filterstr=ldap_url.filterstr + ) + + try: + while ldap_connection.syncrepl_poll(all=1, msgid=ldap_search): + pass + except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR) as e: + logger.exception('syncrepl_poll: LDAP error (%s)', e) + sys.exit(1) diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter deleted file mode 100755 index 7c05539..0000000 --- a/daemons/dnssec/ipa-ods-exporter +++ /dev/null @@ -1,783 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2014 FreeIPA Contributors see COPYING for license -# -""" -This is FreeIPA's replacement for signerd from OpenDNSSEC suite version 1.4.x. - -This program uses the same socket and protocol as original signerd and should -be activated via systemd socket activation using "ods-signer" command line -utility. - -Alternativelly, it can be called directly and a command can be supplied as -first command line argument. - -Purpose of this replacement is to upload keys generated by OpenDNSSEC to LDAP. -""" -from __future__ import print_function - -from datetime import datetime -import logging -import os -import socket -import select -import sys -import sqlite3 -import traceback - -import dateutil.tz -import dns.dnssec -from gssapi.exceptions import GSSError -import six -import systemd.daemon -import systemd.journal - -import ipalib -from ipalib.constants import SOFTHSM_DNSSEC_TOKEN_LABEL -from ipalib.install.kinit import kinit_keytab -from ipapython.dn import DN -from ipapython import ipaldap -from ipaplatform.paths import paths -from ipaserver.dnssec.abshsm import sync_pkcs11_metadata, wrappingmech_name2id -from ipaserver.dnssec.ldapkeydb import LdapKeyDB, str_hexlify -from ipaserver.dnssec.localhsm import LocalHSM - -logger = logging.getLogger(os.path.basename(__file__)) - -DAEMONNAME = 'ipa-ods-exporter' -PRINCIPAL = None # not initialized yet -WORKDIR = os.path.join(paths.VAR_OPENDNSSEC_DIR ,'tmp') -KEYTAB_FB = paths.IPA_ODS_EXPORTER_KEYTAB - -ODS_SE_MAXLINE = 1024 # from ODS common/config.h -ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock') - -SECRETKEY_WRAPPING_MECH = 'rsaPkcsOaep' -PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad' - -# Constants from OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h -KSM_STATE_PUBLISH = 2 -KSM_STATE_READY = 3 -KSM_STATE_ACTIVE = 4 -KSM_STATE_RETIRE = 5 -KSM_STATE_DEAD = 6 -KSM_STATE_KEYPUBLISH = 10 - -# DNSKEY flag constants -dnskey_flag_by_value = { - 0x0001: 'SEP', - 0x0080: 'REVOKE', - 0x0100: 'ZONE' -} - -def dnskey_flags_to_text_set(flags): - """Convert a DNSKEY flags value to set texts - @rtype: set([string])""" - - flags_set = set() - mask = 0x1 - while mask <= 0x8000: - if flags & mask: - text = dnskey_flag_by_value.get(mask) - if not text: - text = hex(mask) - flags_set.add(text) - mask <<= 1 - return flags_set - -def datetime2ldap(dt): - return dt.strftime(ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT) - -def sql2datetime(sql_time): - """Convert SQL date format from local time zone into UTC.""" - localtz = dateutil.tz.tzlocal() - localtime = datetime.strptime(sql_time, "%Y-%m-%d %H:%M:%S").replace( - tzinfo=localtz) - utctz = dateutil.tz.gettz('UTC') - return localtime.astimezone(utctz) - -def sql2datetimes(row): - row2key_map = {'generate': 'idnsSecKeyCreated', - 'publish': 'idnsSecKeyPublish', - 'active': 'idnsSecKeyActivate', - 'retire': 'idnsSecKeyInactive', - 'dead': 'idnsSecKeyDelete'} - times = {} - for column, key in row2key_map.items(): - if row[column] is not None: - times[key] = sql2datetime(row[column]) - return times - -def sql2ldap_algorithm(sql_algorithm): - return {"idnsSecAlgorithm": dns.dnssec.algorithm_to_text(sql_algorithm)} - -def sql2ldap_flags(sql_flags): - dns_flags = dnskey_flags_to_text_set(sql_flags) - ldap_flags = {} - for flag in dns_flags: - attr = 'idnsSecKey%s' % flag - ldap_flags[attr] = 'TRUE' - return ldap_flags - -def sql2ldap_keyid(sql_keyid): - assert len(sql_keyid) % 2 == 0 - assert len(sql_keyid) > 0 - # TODO: this is huge hack. BIND has some problems with % notation in URIs. - # Workaround: OpenDNSSEC uses same value for ID also for label (but in hex). - uri = "pkcs11:object=%s" % sql_keyid - #uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2)) - return {"idnsSecKeyRef": uri} - -def ods2bind_timestamps(key_state, key_type, ods_times): - """Transform (timestamps and key states) from ODS to set of BIND timestamps - with equivalent meaning. At the same time, remove timestamps - for future/planned state transitions to prevent ODS & BIND - from desynchronizing. - - OpenDNSSEC database may contain timestamps for state transitions planned - in the future, but timestamp itself is not sufficient information because - there could be some additional condition which is guaded by OpenDNSSEC - itself. - - BIND works directly with timestamps without any additional conditions. - This difference causes problem when state transition planned in OpenDNSSEC - does not happen as originally planned for some reason. - - At the same time, this difference causes problem when OpenDNSSEC on DNSSEC - key master and BIND instances on replicas are not synchronized. This - happens when DNSSEC key master is down, or a replication is down. Even - a temporary desynchronization could cause DNSSEC validation failures - which could have huge impact. - - To prevent this problem, this function removes all timestamps corresponding - to future state transitions. As a result, BIND will not do state transition - until it happens in OpenDNSSEC first and until the change is replicated. - - Also, timestamp mapping depends on key type and is not 1:1. - For detailed description of the mapping please see - https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC/OpenDNSSEC2BINDKeyStates - """ - bind_times = {} - # idnsSecKeyCreated is equivalent to SQL column 'created' - bind_times['idnsSecKeyCreated'] = ods_times['idnsSecKeyCreated'] - - # set of key states where publishing in DNS zone is desired is taken from - # opendnssec/enforcer/ksm/ksm_request.c:KsmRequestIssueKeys() - # TODO: support for RFC 5011, requires OpenDNSSEC v1.4.8+ - if ('idnsSecKeyPublish' in ods_times and - key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE, - KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}): - bind_times['idnsSecKeyPublish'] = ods_times['idnsSecKeyPublish'] - - # ZSK and KSK handling differs in enforcerd, see - # opendnssec/enforcer/enforcerd/enforcer.c:commKeyConfig() - if key_type == 'ZSK': - # idnsSecKeyActivate cannot be set before the key reaches ACTIVE state - if ('idnsSecKeyActivate' in ods_times and - key_state in {KSM_STATE_ACTIVE, KSM_STATE_RETIRE, KSM_STATE_DEAD}): - bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyActivate'] - - # idnsSecKeyInactive cannot be set before the key reaches RETIRE state - if ('idnsSecKeyInactive' in ods_times and - key_state in {KSM_STATE_RETIRE, KSM_STATE_DEAD}): - bind_times['idnsSecKeyInactive'] = ods_times['idnsSecKeyInactive'] - - elif key_type == 'KSK': - # KSK is special: it is used for signing as long as it is in zone - if ('idnsSecKeyPublish' in ods_times and - key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE, - KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}): - bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyPublish'] - # idnsSecKeyInactive is ignored for KSK on purpose - - else: - raise ValueError("unsupported key type %s" % key_type) - - # idnsSecKeyDelete is relevant only in DEAD state - if 'idnsSecKeyDelete' in ods_times and key_state == KSM_STATE_DEAD: - bind_times['idnsSecKeyDelete'] = ods_times['idnsSecKeyDelete'] - - return bind_times - -def get_ldap_zone(ldap, dns_base, name): - zone_names = ["%s." % name, name] - - # find zone object: name can optionally end with period - ldap_zone = None - for zone_name in zone_names: - zone_base = DN("idnsname=%s" % zone_name, dns_base) - try: - ldap_zone = ldap.get_entry(dn=zone_base, - attrs_list=["idnsname"]) - break - except ipalib.errors.NotFound: - continue - - if ldap_zone is None: - raise ipalib.errors.NotFound( - reason='DNS zone "%s" not found in LDAP' % name) - - return ldap_zone - -def get_ldap_keys_dn(zone_dn): - """Container DN""" - return DN("cn=keys", zone_dn) - -def get_ldap_keys(ldap, zone_dn): - """Keys objects""" - keys_dn = get_ldap_keys_dn(zone_dn) - ldap_filter = ldap.make_filter_from_attr('objectClass', 'idnsSecKey') - ldap_keys = ldap.get_entries(base_dn=keys_dn, filter=ldap_filter) - - return ldap_keys - -def get_ods_keys(zone_name): - # get zone ID - cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)", - (zone_name,)) - rows = cur.fetchall() - if len(rows) != 1: - raise ValueError("exactly one DNS zone should exist in ODS DB") - zone_id = rows[0][0] - - # get relevant keys for given zone ID: - # ignore keys which were generated but not used yet - # key state check is using constants from - # OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h - # WARNING! OpenDNSSEC version 1 and 2 are using different constants! - cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, " - "dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, " - "dnsk.keytype, dnsk.state " - "FROM keypairs AS kp " - "JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id " - "WHERE dnsk.zone_id = ?", (zone_id,)) - keys = {} - for row in cur: - key_data = sql2ldap_flags(row['keytype']) - if key_data.get('idnsSecKeyZONE') != 'TRUE': - raise ValueError("unexpected key type 0x%x" % row['keytype']) - if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE': - key_type = 'KSK' - else: - key_type = 'ZSK' - - # transform key state to timestamps for BIND with equivalent semantics - ods_times = sql2datetimes(row) - key_data.update( - ods2bind_timestamps(row['state'], key_type, ods_times) - ) - - key_data.update(sql2ldap_algorithm(row['algorithm'])) - key_id = "%s-%s-%s" % ( - key_type, - datetime2ldap(key_data['idnsSecKeyCreated']), - row['HSMkey_id'] - ) - - key_data.update(sql2ldap_keyid(row['HSMkey_id'])) - keys[key_id] = key_data - logger.debug("key %s metadata: %s", key_id, key_data) - - return keys - -def sync_set_metadata_2ldap(name, source_set, target_set): - """sync metadata from source key set to target key set in LDAP - - Keys not present in both sets are left intact.""" - matching_keys = set(source_set.keys()).intersection(set(target_set.keys())) - logger.info("%s: keys in local HSM & LDAP: %s", - name, hex_set(matching_keys)) - for key_id in matching_keys: - sync_pkcs11_metadata(name, source_set[key_id], target_set[key_id]) - -def ldap2master_replica_keys_sync(ldapkeydb, localhsm): - """LDAP=>master's local HSM replica key synchronization""" - # import new replica keys from LDAP - logger.debug("replica pub keys in LDAP: %s", - hex_set(ldapkeydb.replica_pubkeys_wrap)) - logger.debug("replica pub keys in SoftHSM: %s", - hex_set(localhsm.replica_pubkeys_wrap)) - new_replica_keys = set(ldapkeydb.replica_pubkeys_wrap.keys()) \ - - set(localhsm.replica_pubkeys_wrap.keys()) - logger.info("new replica keys in LDAP: %s", - hex_set(new_replica_keys)) - for key_id in new_replica_keys: - new_key_ldap = ldapkeydb.replica_pubkeys_wrap[key_id] - logger.debug('label=%s, id=%s, data=%s', - new_key_ldap['ipk11label'], - str_hexlify(new_key_ldap['ipk11id']), - str_hexlify(new_key_ldap['ipapublickey'])) - localhsm.import_public_key(new_key_ldap, new_key_ldap['ipapublickey']) - - # set CKA_WRAP = FALSE for all replica keys removed from LDAP - removed_replica_keys = set(localhsm.replica_pubkeys_wrap.keys()) \ - - set(ldapkeydb.replica_pubkeys_wrap.keys()) - logger.info("obsolete replica keys in local HSM: %s", - hex_set(removed_replica_keys)) - for key_id in removed_replica_keys: - localhsm.replica_pubkeys_wrap[key_id]['ipk11wrap'] = False - - # synchronize replica key attributes from LDAP to local HSM - sync_set_metadata_2ldap('ldap2master_replica', - localhsm.replica_pubkeys_wrap, - ldapkeydb.replica_pubkeys_wrap) - -def master2ldap_master_keys_sync(ldapkeydb, localhsm): - ## master key -> LDAP synchronization - # export new master keys to LDAP - new_master_keys = set(localhsm.master_keys.keys()) \ - - set(ldapkeydb.master_keys.keys()) - logger.debug("master keys in local HSM: %s", - hex_set(localhsm.master_keys.keys())) - logger.debug("master keys in LDAP HSM: %s", - hex_set(ldapkeydb.master_keys.keys())) - logger.debug("new master keys in local HSM: %s", - hex_set(new_master_keys)) - for mkey_id in new_master_keys: - mkey = localhsm.master_keys[mkey_id] - ldapkeydb.import_master_key(mkey) - - # re-fill cache with keys we just added - ldapkeydb.flush() - logger.debug('master keys in LDAP after flush: %s', - hex_set(ldapkeydb.master_keys)) - - # synchronize master key metadata to LDAP - for mkey_id, mkey_local in localhsm.master_keys.items(): - logger.debug('synchronizing master key metadata: 0x%s', - str_hexlify(mkey_id)) - sync_pkcs11_metadata('master2ldap_master', mkey_local, ldapkeydb.master_keys[mkey_id]) - - # re-wrap all master keys in LDAP with new replica keys (as necessary) - enabled_replica_key_ids = set(localhsm.replica_pubkeys_wrap.keys()) - logger.debug('enabled replica key ids: %s', - hex_set(enabled_replica_key_ids)) - - for mkey_id, mkey_ldap in ldapkeydb.master_keys.items(): - logger.debug('processing master key data: 0x%s', - str_hexlify(mkey_id)) - - # check that all active replicas have own copy of master key - used_replica_keys = set() - for wrapped_entry in mkey_ldap.wrapped_entries: - matching_keys = localhsm.find_keys( - uri=wrapped_entry.single_value['ipaWrappingKey']) - for matching_key in matching_keys.values(): - label = matching_key['ipk11label'] - if not label.startswith(u'dnssec-replica:'): - raise ValueError( - "Wrapped key '%s' refers to PKCS#11 URI '%s' which " - "is not a known DNSSEC replica key: label '%s' " - "does not start with 'dnssec-replica:' prefix" % ( - wrapped_entry.dn, - wrapped_entry['ipaWrappingKey'], - label - ) - ) - used_replica_keys.add(matching_key['ipk11id']) - - new_replica_keys = enabled_replica_key_ids - used_replica_keys - logger.debug('master key 0x%s is not wrapped with replica keys %s', - str_hexlify(mkey_id), hex_set(new_replica_keys)) - - # wrap master key with new replica keys - mkey_local = localhsm.find_keys(id=mkey_id).popitem()[1] - for replica_key_id in new_replica_keys: - logger.info('adding master key 0x%s wrapped with replica key 0x%s', - str_hexlify(mkey_id), str_hexlify(replica_key_id)) - replica_key = localhsm.replica_pubkeys_wrap[replica_key_id] - keydata = localhsm.p11.export_wrapped_key(mkey_local.handle, - replica_key.handle, - wrappingmech_name2id[SECRETKEY_WRAPPING_MECH]) - mkey_ldap.add_wrapped_data(keydata, SECRETKEY_WRAPPING_MECH, - replica_key_id) - - ldapkeydb.flush() - -def master2ldap_zone_keys_sync(ldapkeydb, localhsm): - """add and update zone key material from local HSM to LDAP - - No key material will be removed, only new keys will be added or updated. - Key removal is hanled by master2ldap_zone_keys_purge().""" - keypairs_ldap = ldapkeydb.zone_keypairs - logger.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap)) - - pubkeys_local = localhsm.zone_pubkeys - privkeys_local = localhsm.zone_privkeys - logger.debug("zone keys in local HSM: %s", hex_set(privkeys_local)) - - if set(pubkeys_local) != set(privkeys_local): - raise ValueError( - "IDs of private and public keys for DNS zones in local HSM does " - "not match to key pairs: %s vs. %s" % ( - hex_set(pubkeys_local), hex_set(privkeys_local) - ) - ) - - new_keys = set(pubkeys_local) - set(keypairs_ldap) - logger.debug("new zone keys in local HSM: %s", hex_set(new_keys)) - mkey = localhsm.active_master_key - # wrap each new zone key pair with selected master key - for zkey_id in new_keys: - pubkey = pubkeys_local[zkey_id] - pubkey_data = localhsm.p11.export_public_key(pubkey.handle) - - privkey = privkeys_local[zkey_id] - privkey_data = localhsm.p11.export_wrapped_key(privkey.handle, - wrapping_key=mkey.handle, - wrapping_mech=wrappingmech_name2id[PRIVKEY_WRAPPING_MECH]) - ldapkeydb.import_zone_key(pubkey, pubkey_data, privkey, privkey_data, - PRIVKEY_WRAPPING_MECH, mkey['ipk11id']) - - sync_set_metadata_2ldap('master2ldap_zone_keys', pubkeys_local, keypairs_ldap) - sync_set_metadata_2ldap('master2ldap_zone_keys', privkeys_local, keypairs_ldap) - ldapkeydb.flush() - -def master2ldap_zone_keys_purge(ldapkeydb, localhsm): - """purge removed key material from LDAP (but not metadata) - - Keys which are present in LDAP but not in local HSM will be removed. - Key metadata must be removed first so references to removed key material - are removed before actually removing the keys.""" - keypairs_ldap = ldapkeydb.zone_keypairs - logger.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap)) - - pubkeys_local = localhsm.zone_pubkeys - privkeys_local = localhsm.zone_privkeys - logger.debug("zone keys in local HSM: %s", hex_set(privkeys_local)) - if set(pubkeys_local) != set(privkeys_local): - raise ValueError( - "IDs of private and public keys for DNS zones in local HSM does " - "not match to key pairs: %s vs. %s" % ( - hex_set(pubkeys_local), hex_set(privkeys_local) - ) - ) - - deleted_key_ids = set(keypairs_ldap) - set(pubkeys_local) - logger.debug("zone keys deleted from local HSM but present in LDAP: %s", - hex_set(deleted_key_ids)) - for zkey_id in deleted_key_ids: - keypairs_ldap[zkey_id].schedule_deletion() - ldapkeydb.flush() - -def hex_set(s): - out = set() - for i in s: - out.add("0x%s" % str_hexlify(i)) - return out - - -def receive_systemd_command(): - fds = systemd.daemon.listen_fds() - if len(fds) != 1: - raise KeyError('Exactly one socket is expected.') - - sck = socket.fromfd(fds[0], socket.AF_UNIX, socket.SOCK_STREAM) - timeout = 1 # give the socket a bit of time - rlist, _wlist, _xlist = select.select([sck], [], [], timeout) - if not rlist: - logger.critical( - 'socket activation did not return a readable socket with a ' - 'command.' - ) - sys.exit(1) - - logger.debug('accepting new connection') - conn, _addr = sck.accept() - logger.debug('accepted new connection %s', repr(conn)) - - # this implements cmdhandler_handle_cmd() logic - cmd = conn.recv(ODS_SE_MAXLINE).strip() - # ODS uses an ASCII protocol, the rest of the code expects str - if six.PY3: - cmd = cmd.decode('ascii') - logger.debug('received command "%s" from systemd socket', cmd) - return cmd, conn - -def parse_command(cmd): - """Parse command to (exit code, message, zone_name) tuple. - - Exit code None means that execution should continue. - """ - if cmd == 'ipa-hsm-update': - return ( - 0, - 'HSM synchronization finished, skipping zone synchronization.', - None, - cmd - ) - - elif cmd == 'ipa-full-update': - return ( - None, - 'Synchronization of all zones was finished.', - None, - cmd - ) - - elif cmd.startswith('ldap-cleanup '): - zone_name = cmd2ods_zone_name(cmd) - return ( - None, - 'Zone "%s" metadata will be removed from LDAP.\n' % zone_name, - zone_name, - 'ldap-cleanup' - ) - - elif cmd.startswith('update '): - zone_name = cmd2ods_zone_name(cmd) - return ( - None, - 'Zone "%s" metadata will be updated in LDAP.\n' % zone_name, - zone_name, - 'update' - ) - - else: - return ( - 0, - "Command '%s' is not supported by IPA; HSM synchronization was " - "finished and the command will be ignored." % cmd, - None, - None - ) - - -def send_systemd_reply(conn, reply): - # Reply & close connection early. - # This is necessary to let Enforcer to unlock the ODS DB. - if six.PY3: - reply = reply.encode('ascii') - conn.send(reply + b'\n') - conn.shutdown(socket.SHUT_RDWR) - conn.close() - -def cmd2ods_zone_name(cmd): - # ODS stores zone name without trailing period - zone_name = cmd.split(' ', 1)[1].strip() - if len(zone_name) > 1 and zone_name[-1] == '.': - zone_name = zone_name[:-1] - - return zone_name - -def sync_zone(ldap, dns_dn, zone_name): - """synchronize metadata about zone keys for single DNS zone - - Key material has to be synchronized elsewhere. - Keep in mind that keys could be shared among multiple zones!""" - logger.debug('%s: synchronizing zone "%s"', zone_name, zone_name) - ods_keys = get_ods_keys(zone_name) - ods_keys_id = set(ods_keys.keys()) - - ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name) - zone_dn = ldap_zone.dn - - keys_dn = get_ldap_keys_dn(zone_dn) - try: - ldap_keys = get_ldap_keys(ldap, zone_dn) - except ipalib.errors.NotFound: - # cn=keys container does not exist, create it - ldap_keys = [] - ldap_keys_container = ldap.make_entry(keys_dn, - objectClass=['nsContainer']) - try: - ldap.add_entry(ldap_keys_container) - except ipalib.errors.DuplicateEntry: - # ldap.get_entries() does not distinguish non-existent base DN - # from empty result set so addition can fail because container - # itself exists already - pass - - ldap_keys_dict = {} - for ldap_key in ldap_keys: - cn = ldap_key['cn'][0] - ldap_keys_dict[cn] = ldap_key - - ldap_keys = ldap_keys_dict # shorthand - ldap_keys_id = set(ldap_keys.keys()) - - new_keys_id = ods_keys_id - ldap_keys_id - logger.info('%s: new key metadata from ODS: %s', zone_name, new_keys_id) - for key_id in new_keys_id: - cn = "cn=%s" % key_id - key_dn = DN(cn, keys_dn) - logger.debug('%s: adding key metadata "%s" to LDAP', zone_name, key_dn) - ldap_key = ldap.make_entry(key_dn, - objectClass=['idnsSecKey'], - **ods_keys[key_id]) - ldap.add_entry(ldap_key) - - deleted_keys_id = ldap_keys_id - ods_keys_id - logger.info('%s: deleted key metadata in LDAP: %s', - zone_name, deleted_keys_id) - for key_id in deleted_keys_id: - cn = "cn=%s" % key_id - key_dn = DN(cn, keys_dn) - logger.debug('%s: deleting key metadata "%s" from LDAP', - zone_name, key_dn) - ldap.delete_entry(key_dn) - - update_keys_id = ldap_keys_id.intersection(ods_keys_id) - logger.info('%s: key metadata in LDAP & ODS: %s', - zone_name, update_keys_id) - for key_id in update_keys_id: - ldap_key = ldap_keys[key_id] - ods_key = ods_keys[key_id] - logger.debug('%s: updating key metadata "%s" in LDAP', - zone_name, ldap_key.dn) - ldap_key.update(ods_key) - try: - ldap.update_entry(ldap_key) - except ipalib.errors.EmptyModlist: - continue - -def cleanup_ldap_zone(ldap, dns_dn, zone_name): - """delete all key metadata about zone keys for single DNS zone - - Key material has to be synchronized elsewhere. - Keep in mind that keys could be shared among multiple zones!""" - logger.debug('%s: cleaning up key metadata from zone "%s"', - zone_name, zone_name) - - try: - ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name) - ldap_keys = get_ldap_keys(ldap, ldap_zone.dn) - except ipalib.errors.NotFound as ex: - # zone or cn=keys container does not exist, we are done - logger.debug('%s: %s', zone_name, str(ex)) - return - - for ldap_key in ldap_keys: - logger.debug('%s: deleting key metadata "%s"', zone_name, ldap_key.dn) - ldap.delete_entry(ldap_key) - - -# this service is usually socket-activated -root_logger = logging.getLogger() -root_logger.addHandler(systemd.journal.JournalHandler()) -root_logger.setLevel(level=logging.DEBUG) - -if len(sys.argv) > 2: - print(__doc__) - sys.exit(1) -# program was likely invoked from console, log to it -elif len(sys.argv) == 2: - console = logging.StreamHandler() - root_logger.addHandler(console) - -# IPA framework initialization -ipalib.api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True) -ipalib.api.finalize() - -# Kerberos initialization -PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host)) -logger.debug('Kerberos principal: %s', PRINCIPAL) -ccache_name = paths.IPA_ODS_EXPORTER_CCACHE - -try: - kinit_keytab(PRINCIPAL, paths.IPA_ODS_EXPORTER_KEYTAB, ccache_name, - attempts=5) -except GSSError as e: - logger.critical('Kerberos authentication failed: %s', e) - sys.exit(1) - -os.environ['KRB5CCNAME'] = ccache_name -logger.debug('Got TGT') - -# LDAP initialization -dns_dn = DN(ipalib.api.env.container_dns, ipalib.api.env.basedn) -ldap = ipaldap.LDAPClient(ipalib.api.env.ldap_uri) -logger.debug('Connecting to LDAP') -ldap.gssapi_bind() -logger.debug('Connected') - - -### DNSSEC master: key material upload & synchronization (but not deletion) -ldapkeydb = LdapKeyDB(ldap, DN(('cn', 'keys'), - ('cn', 'sec'), - ipalib.api.env.container_dns, - ipalib.api.env.basedn)) -localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, SOFTHSM_DNSSEC_TOKEN_LABEL, - open(paths.DNSSEC_SOFTHSM_PIN).read()) - -ldap2master_replica_keys_sync(ldapkeydb, localhsm) -master2ldap_master_keys_sync(ldapkeydb, localhsm) -master2ldap_zone_keys_sync(ldapkeydb, localhsm) - - -### DNSSEC master: DNSSEC key metadata upload & synchronization & deletion -# command receive is delayed so the command will stay in socket queue until -# the problem with LDAP server or HSM is fixed -try: - cmd, conn = receive_systemd_command() - if len(sys.argv) != 1: - logger.critical('No additional parameters are accepted when ' - 'socket activation is used.') - sys.exit(1) -# Handle cases where somebody ran the program without systemd. -except KeyError as e: - if len(sys.argv) != 2: - print(__doc__) - print('ERROR: Exactly one parameter or socket activation is required.') - sys.exit(1) - conn = None - cmd = sys.argv[1] - -exitcode, msg, zone_name, cmd = parse_command(cmd) - -if exitcode is not None: - if conn: - send_systemd_reply(conn, msg) - logger.info("%s", msg) - sys.exit(exitcode) -else: - logger.debug("%s", msg) - -# Open DB directly and read key timestamps etc. -db = None -try: - # LOCK WARNING: - # ods-enforcerd is holding kasp.db.our_lock when processing all zones and - # the lock is unlocked only after all calls to ods-signer are finished, - # i.e. when ods-enforcerd receives reply from each ods-signer call. - # - # Consequently, ipa-ods-exporter (ods-signerd implementation) must not - # request kasp.db.our_lock to prevent deadlocks. - # SQLite transaction isolation should suffice. - # Beware: Reply can be sent back only after DB is unlocked and closed - # otherwise ods-enforcerd will fail. - - db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB) - db.row_factory = sqlite3.Row - db.execute('BEGIN') - - if zone_name is not None: - # only one zone should be processed - if cmd == 'update': - sync_zone(ldap, dns_dn, zone_name) - elif cmd == 'ldap-cleanup': - cleanup_ldap_zone(ldap, dns_dn, zone_name) - else: - # process all zones - for zone_row in db.execute("SELECT name FROM zones"): - sync_zone(ldap, dns_dn, zone_row['name']) - - ### DNSSEC master: DNSSEC key material purging - # references to old key material were removed above in sync_zone() - # so now we can purge old key material from LDAP - master2ldap_zone_keys_purge(ldapkeydb, localhsm) - -except Exception as ex: - msg = "ipa-ods-exporter exception: %s" % traceback.format_exc(ex) - logger.exception("%s", ex) - raise ex - -finally: - try: - if db: - db.close() - finally: - if conn: - send_systemd_reply(conn, msg) - -logger.debug('Done') diff --git a/daemons/dnssec/ipa-ods-exporter.in b/daemons/dnssec/ipa-ods-exporter.in new file mode 100644 index 0000000..ffa2d40 --- /dev/null +++ b/daemons/dnssec/ipa-ods-exporter.in @@ -0,0 +1,783 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# +""" +This is FreeIPA's replacement for signerd from OpenDNSSEC suite version 1.4.x. + +This program uses the same socket and protocol as original signerd and should +be activated via systemd socket activation using "ods-signer" command line +utility. + +Alternativelly, it can be called directly and a command can be supplied as +first command line argument. + +Purpose of this replacement is to upload keys generated by OpenDNSSEC to LDAP. +""" +from __future__ import print_function + +from datetime import datetime +import logging +import os +import socket +import select +import sys +import sqlite3 +import traceback + +import dateutil.tz +import dns.dnssec +from gssapi.exceptions import GSSError +import six +import systemd.daemon +import systemd.journal + +import ipalib +from ipalib.constants import SOFTHSM_DNSSEC_TOKEN_LABEL +from ipalib.install.kinit import kinit_keytab +from ipapython.dn import DN +from ipapython import ipaldap +from ipaplatform.paths import paths +from ipaserver.dnssec.abshsm import sync_pkcs11_metadata, wrappingmech_name2id +from ipaserver.dnssec.ldapkeydb import LdapKeyDB, str_hexlify +from ipaserver.dnssec.localhsm import LocalHSM + +logger = logging.getLogger(os.path.basename(__file__)) + +DAEMONNAME = 'ipa-ods-exporter' +PRINCIPAL = None # not initialized yet +WORKDIR = os.path.join(paths.VAR_OPENDNSSEC_DIR ,'tmp') +KEYTAB_FB = paths.IPA_ODS_EXPORTER_KEYTAB + +ODS_SE_MAXLINE = 1024 # from ODS common/config.h +ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock') + +SECRETKEY_WRAPPING_MECH = 'rsaPkcsOaep' +PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad' + +# Constants from OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h +KSM_STATE_PUBLISH = 2 +KSM_STATE_READY = 3 +KSM_STATE_ACTIVE = 4 +KSM_STATE_RETIRE = 5 +KSM_STATE_DEAD = 6 +KSM_STATE_KEYPUBLISH = 10 + +# DNSKEY flag constants +dnskey_flag_by_value = { + 0x0001: 'SEP', + 0x0080: 'REVOKE', + 0x0100: 'ZONE' +} + +def dnskey_flags_to_text_set(flags): + """Convert a DNSKEY flags value to set texts + @rtype: set([string])""" + + flags_set = set() + mask = 0x1 + while mask <= 0x8000: + if flags & mask: + text = dnskey_flag_by_value.get(mask) + if not text: + text = hex(mask) + flags_set.add(text) + mask <<= 1 + return flags_set + +def datetime2ldap(dt): + return dt.strftime(ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT) + +def sql2datetime(sql_time): + """Convert SQL date format from local time zone into UTC.""" + localtz = dateutil.tz.tzlocal() + localtime = datetime.strptime(sql_time, "%Y-%m-%d %H:%M:%S").replace( + tzinfo=localtz) + utctz = dateutil.tz.gettz('UTC') + return localtime.astimezone(utctz) + +def sql2datetimes(row): + row2key_map = {'generate': 'idnsSecKeyCreated', + 'publish': 'idnsSecKeyPublish', + 'active': 'idnsSecKeyActivate', + 'retire': 'idnsSecKeyInactive', + 'dead': 'idnsSecKeyDelete'} + times = {} + for column, key in row2key_map.items(): + if row[column] is not None: + times[key] = sql2datetime(row[column]) + return times + +def sql2ldap_algorithm(sql_algorithm): + return {"idnsSecAlgorithm": dns.dnssec.algorithm_to_text(sql_algorithm)} + +def sql2ldap_flags(sql_flags): + dns_flags = dnskey_flags_to_text_set(sql_flags) + ldap_flags = {} + for flag in dns_flags: + attr = 'idnsSecKey%s' % flag + ldap_flags[attr] = 'TRUE' + return ldap_flags + +def sql2ldap_keyid(sql_keyid): + assert len(sql_keyid) % 2 == 0 + assert len(sql_keyid) > 0 + # TODO: this is huge hack. BIND has some problems with % notation in URIs. + # Workaround: OpenDNSSEC uses same value for ID also for label (but in hex). + uri = "pkcs11:object=%s" % sql_keyid + #uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2)) + return {"idnsSecKeyRef": uri} + +def ods2bind_timestamps(key_state, key_type, ods_times): + """Transform (timestamps and key states) from ODS to set of BIND timestamps + with equivalent meaning. At the same time, remove timestamps + for future/planned state transitions to prevent ODS & BIND + from desynchronizing. + + OpenDNSSEC database may contain timestamps for state transitions planned + in the future, but timestamp itself is not sufficient information because + there could be some additional condition which is guaded by OpenDNSSEC + itself. + + BIND works directly with timestamps without any additional conditions. + This difference causes problem when state transition planned in OpenDNSSEC + does not happen as originally planned for some reason. + + At the same time, this difference causes problem when OpenDNSSEC on DNSSEC + key master and BIND instances on replicas are not synchronized. This + happens when DNSSEC key master is down, or a replication is down. Even + a temporary desynchronization could cause DNSSEC validation failures + which could have huge impact. + + To prevent this problem, this function removes all timestamps corresponding + to future state transitions. As a result, BIND will not do state transition + until it happens in OpenDNSSEC first and until the change is replicated. + + Also, timestamp mapping depends on key type and is not 1:1. + For detailed description of the mapping please see + https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC/OpenDNSSEC2BINDKeyStates + """ + bind_times = {} + # idnsSecKeyCreated is equivalent to SQL column 'created' + bind_times['idnsSecKeyCreated'] = ods_times['idnsSecKeyCreated'] + + # set of key states where publishing in DNS zone is desired is taken from + # opendnssec/enforcer/ksm/ksm_request.c:KsmRequestIssueKeys() + # TODO: support for RFC 5011, requires OpenDNSSEC v1.4.8+ + if ('idnsSecKeyPublish' in ods_times and + key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE, + KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}): + bind_times['idnsSecKeyPublish'] = ods_times['idnsSecKeyPublish'] + + # ZSK and KSK handling differs in enforcerd, see + # opendnssec/enforcer/enforcerd/enforcer.c:commKeyConfig() + if key_type == 'ZSK': + # idnsSecKeyActivate cannot be set before the key reaches ACTIVE state + if ('idnsSecKeyActivate' in ods_times and + key_state in {KSM_STATE_ACTIVE, KSM_STATE_RETIRE, KSM_STATE_DEAD}): + bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyActivate'] + + # idnsSecKeyInactive cannot be set before the key reaches RETIRE state + if ('idnsSecKeyInactive' in ods_times and + key_state in {KSM_STATE_RETIRE, KSM_STATE_DEAD}): + bind_times['idnsSecKeyInactive'] = ods_times['idnsSecKeyInactive'] + + elif key_type == 'KSK': + # KSK is special: it is used for signing as long as it is in zone + if ('idnsSecKeyPublish' in ods_times and + key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE, + KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}): + bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyPublish'] + # idnsSecKeyInactive is ignored for KSK on purpose + + else: + raise ValueError("unsupported key type %s" % key_type) + + # idnsSecKeyDelete is relevant only in DEAD state + if 'idnsSecKeyDelete' in ods_times and key_state == KSM_STATE_DEAD: + bind_times['idnsSecKeyDelete'] = ods_times['idnsSecKeyDelete'] + + return bind_times + +def get_ldap_zone(ldap, dns_base, name): + zone_names = ["%s." % name, name] + + # find zone object: name can optionally end with period + ldap_zone = None + for zone_name in zone_names: + zone_base = DN("idnsname=%s" % zone_name, dns_base) + try: + ldap_zone = ldap.get_entry(dn=zone_base, + attrs_list=["idnsname"]) + break + except ipalib.errors.NotFound: + continue + + if ldap_zone is None: + raise ipalib.errors.NotFound( + reason='DNS zone "%s" not found in LDAP' % name) + + return ldap_zone + +def get_ldap_keys_dn(zone_dn): + """Container DN""" + return DN("cn=keys", zone_dn) + +def get_ldap_keys(ldap, zone_dn): + """Keys objects""" + keys_dn = get_ldap_keys_dn(zone_dn) + ldap_filter = ldap.make_filter_from_attr('objectClass', 'idnsSecKey') + ldap_keys = ldap.get_entries(base_dn=keys_dn, filter=ldap_filter) + + return ldap_keys + +def get_ods_keys(zone_name): + # get zone ID + cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)", + (zone_name,)) + rows = cur.fetchall() + if len(rows) != 1: + raise ValueError("exactly one DNS zone should exist in ODS DB") + zone_id = rows[0][0] + + # get relevant keys for given zone ID: + # ignore keys which were generated but not used yet + # key state check is using constants from + # OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h + # WARNING! OpenDNSSEC version 1 and 2 are using different constants! + cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, " + "dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, " + "dnsk.keytype, dnsk.state " + "FROM keypairs AS kp " + "JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id " + "WHERE dnsk.zone_id = ?", (zone_id,)) + keys = {} + for row in cur: + key_data = sql2ldap_flags(row['keytype']) + if key_data.get('idnsSecKeyZONE') != 'TRUE': + raise ValueError("unexpected key type 0x%x" % row['keytype']) + if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE': + key_type = 'KSK' + else: + key_type = 'ZSK' + + # transform key state to timestamps for BIND with equivalent semantics + ods_times = sql2datetimes(row) + key_data.update( + ods2bind_timestamps(row['state'], key_type, ods_times) + ) + + key_data.update(sql2ldap_algorithm(row['algorithm'])) + key_id = "%s-%s-%s" % ( + key_type, + datetime2ldap(key_data['idnsSecKeyCreated']), + row['HSMkey_id'] + ) + + key_data.update(sql2ldap_keyid(row['HSMkey_id'])) + keys[key_id] = key_data + logger.debug("key %s metadata: %s", key_id, key_data) + + return keys + +def sync_set_metadata_2ldap(name, source_set, target_set): + """sync metadata from source key set to target key set in LDAP + + Keys not present in both sets are left intact.""" + matching_keys = set(source_set.keys()).intersection(set(target_set.keys())) + logger.info("%s: keys in local HSM & LDAP: %s", + name, hex_set(matching_keys)) + for key_id in matching_keys: + sync_pkcs11_metadata(name, source_set[key_id], target_set[key_id]) + +def ldap2master_replica_keys_sync(ldapkeydb, localhsm): + """LDAP=>master's local HSM replica key synchronization""" + # import new replica keys from LDAP + logger.debug("replica pub keys in LDAP: %s", + hex_set(ldapkeydb.replica_pubkeys_wrap)) + logger.debug("replica pub keys in SoftHSM: %s", + hex_set(localhsm.replica_pubkeys_wrap)) + new_replica_keys = set(ldapkeydb.replica_pubkeys_wrap.keys()) \ + - set(localhsm.replica_pubkeys_wrap.keys()) + logger.info("new replica keys in LDAP: %s", + hex_set(new_replica_keys)) + for key_id in new_replica_keys: + new_key_ldap = ldapkeydb.replica_pubkeys_wrap[key_id] + logger.debug('label=%s, id=%s, data=%s', + new_key_ldap['ipk11label'], + str_hexlify(new_key_ldap['ipk11id']), + str_hexlify(new_key_ldap['ipapublickey'])) + localhsm.import_public_key(new_key_ldap, new_key_ldap['ipapublickey']) + + # set CKA_WRAP = FALSE for all replica keys removed from LDAP + removed_replica_keys = set(localhsm.replica_pubkeys_wrap.keys()) \ + - set(ldapkeydb.replica_pubkeys_wrap.keys()) + logger.info("obsolete replica keys in local HSM: %s", + hex_set(removed_replica_keys)) + for key_id in removed_replica_keys: + localhsm.replica_pubkeys_wrap[key_id]['ipk11wrap'] = False + + # synchronize replica key attributes from LDAP to local HSM + sync_set_metadata_2ldap('ldap2master_replica', + localhsm.replica_pubkeys_wrap, + ldapkeydb.replica_pubkeys_wrap) + +def master2ldap_master_keys_sync(ldapkeydb, localhsm): + ## master key -> LDAP synchronization + # export new master keys to LDAP + new_master_keys = set(localhsm.master_keys.keys()) \ + - set(ldapkeydb.master_keys.keys()) + logger.debug("master keys in local HSM: %s", + hex_set(localhsm.master_keys.keys())) + logger.debug("master keys in LDAP HSM: %s", + hex_set(ldapkeydb.master_keys.keys())) + logger.debug("new master keys in local HSM: %s", + hex_set(new_master_keys)) + for mkey_id in new_master_keys: + mkey = localhsm.master_keys[mkey_id] + ldapkeydb.import_master_key(mkey) + + # re-fill cache with keys we just added + ldapkeydb.flush() + logger.debug('master keys in LDAP after flush: %s', + hex_set(ldapkeydb.master_keys)) + + # synchronize master key metadata to LDAP + for mkey_id, mkey_local in localhsm.master_keys.items(): + logger.debug('synchronizing master key metadata: 0x%s', + str_hexlify(mkey_id)) + sync_pkcs11_metadata('master2ldap_master', mkey_local, ldapkeydb.master_keys[mkey_id]) + + # re-wrap all master keys in LDAP with new replica keys (as necessary) + enabled_replica_key_ids = set(localhsm.replica_pubkeys_wrap.keys()) + logger.debug('enabled replica key ids: %s', + hex_set(enabled_replica_key_ids)) + + for mkey_id, mkey_ldap in ldapkeydb.master_keys.items(): + logger.debug('processing master key data: 0x%s', + str_hexlify(mkey_id)) + + # check that all active replicas have own copy of master key + used_replica_keys = set() + for wrapped_entry in mkey_ldap.wrapped_entries: + matching_keys = localhsm.find_keys( + uri=wrapped_entry.single_value['ipaWrappingKey']) + for matching_key in matching_keys.values(): + label = matching_key['ipk11label'] + if not label.startswith(u'dnssec-replica:'): + raise ValueError( + "Wrapped key '%s' refers to PKCS#11 URI '%s' which " + "is not a known DNSSEC replica key: label '%s' " + "does not start with 'dnssec-replica:' prefix" % ( + wrapped_entry.dn, + wrapped_entry['ipaWrappingKey'], + label + ) + ) + used_replica_keys.add(matching_key['ipk11id']) + + new_replica_keys = enabled_replica_key_ids - used_replica_keys + logger.debug('master key 0x%s is not wrapped with replica keys %s', + str_hexlify(mkey_id), hex_set(new_replica_keys)) + + # wrap master key with new replica keys + mkey_local = localhsm.find_keys(id=mkey_id).popitem()[1] + for replica_key_id in new_replica_keys: + logger.info('adding master key 0x%s wrapped with replica key 0x%s', + str_hexlify(mkey_id), str_hexlify(replica_key_id)) + replica_key = localhsm.replica_pubkeys_wrap[replica_key_id] + keydata = localhsm.p11.export_wrapped_key(mkey_local.handle, + replica_key.handle, + wrappingmech_name2id[SECRETKEY_WRAPPING_MECH]) + mkey_ldap.add_wrapped_data(keydata, SECRETKEY_WRAPPING_MECH, + replica_key_id) + + ldapkeydb.flush() + +def master2ldap_zone_keys_sync(ldapkeydb, localhsm): + """add and update zone key material from local HSM to LDAP + + No key material will be removed, only new keys will be added or updated. + Key removal is hanled by master2ldap_zone_keys_purge().""" + keypairs_ldap = ldapkeydb.zone_keypairs + logger.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap)) + + pubkeys_local = localhsm.zone_pubkeys + privkeys_local = localhsm.zone_privkeys + logger.debug("zone keys in local HSM: %s", hex_set(privkeys_local)) + + if set(pubkeys_local) != set(privkeys_local): + raise ValueError( + "IDs of private and public keys for DNS zones in local HSM does " + "not match to key pairs: %s vs. %s" % ( + hex_set(pubkeys_local), hex_set(privkeys_local) + ) + ) + + new_keys = set(pubkeys_local) - set(keypairs_ldap) + logger.debug("new zone keys in local HSM: %s", hex_set(new_keys)) + mkey = localhsm.active_master_key + # wrap each new zone key pair with selected master key + for zkey_id in new_keys: + pubkey = pubkeys_local[zkey_id] + pubkey_data = localhsm.p11.export_public_key(pubkey.handle) + + privkey = privkeys_local[zkey_id] + privkey_data = localhsm.p11.export_wrapped_key(privkey.handle, + wrapping_key=mkey.handle, + wrapping_mech=wrappingmech_name2id[PRIVKEY_WRAPPING_MECH]) + ldapkeydb.import_zone_key(pubkey, pubkey_data, privkey, privkey_data, + PRIVKEY_WRAPPING_MECH, mkey['ipk11id']) + + sync_set_metadata_2ldap('master2ldap_zone_keys', pubkeys_local, keypairs_ldap) + sync_set_metadata_2ldap('master2ldap_zone_keys', privkeys_local, keypairs_ldap) + ldapkeydb.flush() + +def master2ldap_zone_keys_purge(ldapkeydb, localhsm): + """purge removed key material from LDAP (but not metadata) + + Keys which are present in LDAP but not in local HSM will be removed. + Key metadata must be removed first so references to removed key material + are removed before actually removing the keys.""" + keypairs_ldap = ldapkeydb.zone_keypairs + logger.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap)) + + pubkeys_local = localhsm.zone_pubkeys + privkeys_local = localhsm.zone_privkeys + logger.debug("zone keys in local HSM: %s", hex_set(privkeys_local)) + if set(pubkeys_local) != set(privkeys_local): + raise ValueError( + "IDs of private and public keys for DNS zones in local HSM does " + "not match to key pairs: %s vs. %s" % ( + hex_set(pubkeys_local), hex_set(privkeys_local) + ) + ) + + deleted_key_ids = set(keypairs_ldap) - set(pubkeys_local) + logger.debug("zone keys deleted from local HSM but present in LDAP: %s", + hex_set(deleted_key_ids)) + for zkey_id in deleted_key_ids: + keypairs_ldap[zkey_id].schedule_deletion() + ldapkeydb.flush() + +def hex_set(s): + out = set() + for i in s: + out.add("0x%s" % str_hexlify(i)) + return out + + +def receive_systemd_command(): + fds = systemd.daemon.listen_fds() + if len(fds) != 1: + raise KeyError('Exactly one socket is expected.') + + sck = socket.fromfd(fds[0], socket.AF_UNIX, socket.SOCK_STREAM) + timeout = 1 # give the socket a bit of time + rlist, _wlist, _xlist = select.select([sck], [], [], timeout) + if not rlist: + logger.critical( + 'socket activation did not return a readable socket with a ' + 'command.' + ) + sys.exit(1) + + logger.debug('accepting new connection') + conn, _addr = sck.accept() + logger.debug('accepted new connection %s', repr(conn)) + + # this implements cmdhandler_handle_cmd() logic + cmd = conn.recv(ODS_SE_MAXLINE).strip() + # ODS uses an ASCII protocol, the rest of the code expects str + if six.PY3: + cmd = cmd.decode('ascii') + logger.debug('received command "%s" from systemd socket', cmd) + return cmd, conn + +def parse_command(cmd): + """Parse command to (exit code, message, zone_name) tuple. + + Exit code None means that execution should continue. + """ + if cmd == 'ipa-hsm-update': + return ( + 0, + 'HSM synchronization finished, skipping zone synchronization.', + None, + cmd + ) + + elif cmd == 'ipa-full-update': + return ( + None, + 'Synchronization of all zones was finished.', + None, + cmd + ) + + elif cmd.startswith('ldap-cleanup '): + zone_name = cmd2ods_zone_name(cmd) + return ( + None, + 'Zone "%s" metadata will be removed from LDAP.\n' % zone_name, + zone_name, + 'ldap-cleanup' + ) + + elif cmd.startswith('update '): + zone_name = cmd2ods_zone_name(cmd) + return ( + None, + 'Zone "%s" metadata will be updated in LDAP.\n' % zone_name, + zone_name, + 'update' + ) + + else: + return ( + 0, + "Command '%s' is not supported by IPA; HSM synchronization was " + "finished and the command will be ignored." % cmd, + None, + None + ) + + +def send_systemd_reply(conn, reply): + # Reply & close connection early. + # This is necessary to let Enforcer to unlock the ODS DB. + if six.PY3: + reply = reply.encode('ascii') + conn.send(reply + b'\n') + conn.shutdown(socket.SHUT_RDWR) + conn.close() + +def cmd2ods_zone_name(cmd): + # ODS stores zone name without trailing period + zone_name = cmd.split(' ', 1)[1].strip() + if len(zone_name) > 1 and zone_name[-1] == '.': + zone_name = zone_name[:-1] + + return zone_name + +def sync_zone(ldap, dns_dn, zone_name): + """synchronize metadata about zone keys for single DNS zone + + Key material has to be synchronized elsewhere. + Keep in mind that keys could be shared among multiple zones!""" + logger.debug('%s: synchronizing zone "%s"', zone_name, zone_name) + ods_keys = get_ods_keys(zone_name) + ods_keys_id = set(ods_keys.keys()) + + ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name) + zone_dn = ldap_zone.dn + + keys_dn = get_ldap_keys_dn(zone_dn) + try: + ldap_keys = get_ldap_keys(ldap, zone_dn) + except ipalib.errors.NotFound: + # cn=keys container does not exist, create it + ldap_keys = [] + ldap_keys_container = ldap.make_entry(keys_dn, + objectClass=['nsContainer']) + try: + ldap.add_entry(ldap_keys_container) + except ipalib.errors.DuplicateEntry: + # ldap.get_entries() does not distinguish non-existent base DN + # from empty result set so addition can fail because container + # itself exists already + pass + + ldap_keys_dict = {} + for ldap_key in ldap_keys: + cn = ldap_key['cn'][0] + ldap_keys_dict[cn] = ldap_key + + ldap_keys = ldap_keys_dict # shorthand + ldap_keys_id = set(ldap_keys.keys()) + + new_keys_id = ods_keys_id - ldap_keys_id + logger.info('%s: new key metadata from ODS: %s', zone_name, new_keys_id) + for key_id in new_keys_id: + cn = "cn=%s" % key_id + key_dn = DN(cn, keys_dn) + logger.debug('%s: adding key metadata "%s" to LDAP', zone_name, key_dn) + ldap_key = ldap.make_entry(key_dn, + objectClass=['idnsSecKey'], + **ods_keys[key_id]) + ldap.add_entry(ldap_key) + + deleted_keys_id = ldap_keys_id - ods_keys_id + logger.info('%s: deleted key metadata in LDAP: %s', + zone_name, deleted_keys_id) + for key_id in deleted_keys_id: + cn = "cn=%s" % key_id + key_dn = DN(cn, keys_dn) + logger.debug('%s: deleting key metadata "%s" from LDAP', + zone_name, key_dn) + ldap.delete_entry(key_dn) + + update_keys_id = ldap_keys_id.intersection(ods_keys_id) + logger.info('%s: key metadata in LDAP & ODS: %s', + zone_name, update_keys_id) + for key_id in update_keys_id: + ldap_key = ldap_keys[key_id] + ods_key = ods_keys[key_id] + logger.debug('%s: updating key metadata "%s" in LDAP', + zone_name, ldap_key.dn) + ldap_key.update(ods_key) + try: + ldap.update_entry(ldap_key) + except ipalib.errors.EmptyModlist: + continue + +def cleanup_ldap_zone(ldap, dns_dn, zone_name): + """delete all key metadata about zone keys for single DNS zone + + Key material has to be synchronized elsewhere. + Keep in mind that keys could be shared among multiple zones!""" + logger.debug('%s: cleaning up key metadata from zone "%s"', + zone_name, zone_name) + + try: + ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name) + ldap_keys = get_ldap_keys(ldap, ldap_zone.dn) + except ipalib.errors.NotFound as ex: + # zone or cn=keys container does not exist, we are done + logger.debug('%s: %s', zone_name, str(ex)) + return + + for ldap_key in ldap_keys: + logger.debug('%s: deleting key metadata "%s"', zone_name, ldap_key.dn) + ldap.delete_entry(ldap_key) + + +# this service is usually socket-activated +root_logger = logging.getLogger() +root_logger.addHandler(systemd.journal.JournalHandler()) +root_logger.setLevel(level=logging.DEBUG) + +if len(sys.argv) > 2: + print(__doc__) + sys.exit(1) +# program was likely invoked from console, log to it +elif len(sys.argv) == 2: + console = logging.StreamHandler() + root_logger.addHandler(console) + +# IPA framework initialization +ipalib.api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True) +ipalib.api.finalize() + +# Kerberos initialization +PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host)) +logger.debug('Kerberos principal: %s', PRINCIPAL) +ccache_name = paths.IPA_ODS_EXPORTER_CCACHE + +try: + kinit_keytab(PRINCIPAL, paths.IPA_ODS_EXPORTER_KEYTAB, ccache_name, + attempts=5) +except GSSError as e: + logger.critical('Kerberos authentication failed: %s', e) + sys.exit(1) + +os.environ['KRB5CCNAME'] = ccache_name +logger.debug('Got TGT') + +# LDAP initialization +dns_dn = DN(ipalib.api.env.container_dns, ipalib.api.env.basedn) +ldap = ipaldap.LDAPClient(ipalib.api.env.ldap_uri) +logger.debug('Connecting to LDAP') +ldap.gssapi_bind() +logger.debug('Connected') + + +### DNSSEC master: key material upload & synchronization (but not deletion) +ldapkeydb = LdapKeyDB(ldap, DN(('cn', 'keys'), + ('cn', 'sec'), + ipalib.api.env.container_dns, + ipalib.api.env.basedn)) +localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, SOFTHSM_DNSSEC_TOKEN_LABEL, + open(paths.DNSSEC_SOFTHSM_PIN).read()) + +ldap2master_replica_keys_sync(ldapkeydb, localhsm) +master2ldap_master_keys_sync(ldapkeydb, localhsm) +master2ldap_zone_keys_sync(ldapkeydb, localhsm) + + +### DNSSEC master: DNSSEC key metadata upload & synchronization & deletion +# command receive is delayed so the command will stay in socket queue until +# the problem with LDAP server or HSM is fixed +try: + cmd, conn = receive_systemd_command() + if len(sys.argv) != 1: + logger.critical('No additional parameters are accepted when ' + 'socket activation is used.') + sys.exit(1) +# Handle cases where somebody ran the program without systemd. +except KeyError as e: + if len(sys.argv) != 2: + print(__doc__) + print('ERROR: Exactly one parameter or socket activation is required.') + sys.exit(1) + conn = None + cmd = sys.argv[1] + +exitcode, msg, zone_name, cmd = parse_command(cmd) + +if exitcode is not None: + if conn: + send_systemd_reply(conn, msg) + logger.info("%s", msg) + sys.exit(exitcode) +else: + logger.debug("%s", msg) + +# Open DB directly and read key timestamps etc. +db = None +try: + # LOCK WARNING: + # ods-enforcerd is holding kasp.db.our_lock when processing all zones and + # the lock is unlocked only after all calls to ods-signer are finished, + # i.e. when ods-enforcerd receives reply from each ods-signer call. + # + # Consequently, ipa-ods-exporter (ods-signerd implementation) must not + # request kasp.db.our_lock to prevent deadlocks. + # SQLite transaction isolation should suffice. + # Beware: Reply can be sent back only after DB is unlocked and closed + # otherwise ods-enforcerd will fail. + + db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB) + db.row_factory = sqlite3.Row + db.execute('BEGIN') + + if zone_name is not None: + # only one zone should be processed + if cmd == 'update': + sync_zone(ldap, dns_dn, zone_name) + elif cmd == 'ldap-cleanup': + cleanup_ldap_zone(ldap, dns_dn, zone_name) + else: + # process all zones + for zone_row in db.execute("SELECT name FROM zones"): + sync_zone(ldap, dns_dn, zone_row['name']) + + ### DNSSEC master: DNSSEC key material purging + # references to old key material were removed above in sync_zone() + # so now we can purge old key material from LDAP + master2ldap_zone_keys_purge(ldapkeydb, localhsm) + +except Exception as ex: + msg = "ipa-ods-exporter exception: %s" % traceback.format_exc(ex) + logger.exception("%s", ex) + raise ex + +finally: + try: + if db: + db.close() + finally: + if conn: + send_systemd_reply(conn, msg) + +logger.debug('Done') diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit deleted file mode 100755 index 86810ab..0000000 --- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit +++ /dev/null @@ -1,545 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Jan Cholasta -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import print_function - -import os -# Prevent garbage from readline on standard output -# (see https://fedorahosted.org/freeipa/ticket/4064) -if not os.isatty(1): - os.environ['TERM'] = 'dumb' -import sys -import syslog -import traceback -import tempfile -import shutil -import contextlib -import json - -from cryptography import x509 as crypto_x509 -from cryptography.hazmat.backends import default_backend - -import six - -from ipalib.install.kinit import kinit_keytab -from ipapython import ipautil -from ipapython.dn import DN -from ipalib import api, errors, x509 -from ipaplatform.paths import paths -from ipaserver.plugins.ldap2 import ldap2 -from ipaserver.install import ca, cainstance, dsinstance, certs - -# This is a certmonger CA helper script for IPA CA subsystem cert renewal. See -# https://git.fedorahosted.org/cgit/certmonger.git/tree/doc/submit.txt for more -# info on certmonger CA helper scripts. - -# Return codes. Names of the constants are taken from -# https://git.fedorahosted.org/cgit/certmonger.git/tree/src/submit-e.h -ISSUED = 0 -WAIT = 1 -REJECTED = 2 -UNREACHABLE = 3 -UNCONFIGURED = 4 -WAIT_WITH_DELAY = 5 -OPERATION_NOT_SUPPORTED_BY_HELPER = 6 - -if six.PY3: - unicode = str - - -IPA_CA_NICKNAME = 'caSigningCert cert-pki-ca' - - -def get_nickname(): - # we need to get the subject from a CSR in case we are requesting - # an OpenSSL certificate for which we have to reverse the order of its DN - # components thus changing the CERTMONGER_REQ_SUBJECT - # https://pagure.io/certmonger/issue/62 - csr = os.environ.get('CERTMONGER_CSR').encode('ascii') - csr_obj = crypto_x509.load_pem_x509_csr(csr, default_backend()) - subject = csr_obj.subject - if not subject: - return None - - subject_base = dsinstance.DsInstance().find_subject_base() - if not subject_base: - return None - - ca_subject_dn = ca.lookup_ca_subject(api, subject_base) - - nickname_by_subject_dn = { - DN(ca_subject_dn): 'caSigningCert cert-pki-ca', - DN('CN=CA Audit', subject_base): 'auditSigningCert cert-pki-ca', - DN('CN=OCSP Subsystem', subject_base): 'ocspSigningCert cert-pki-ca', - DN('CN=CA Subsystem', subject_base): 'subsystemCert cert-pki-ca', - DN('CN=KRA Audit', subject_base): 'auditSigningCert cert-pki-kra', - DN('CN=KRA Transport Certificate', subject_base): - 'transportCert cert-pki-kra', - DN('CN=KRA Storage Certificate', subject_base): - 'storageCert cert-pki-kra', - DN('CN=IPA RA', subject_base): 'ipaCert', - } - - return nickname_by_subject_dn.get(DN(subject)) - - -def is_replicated(): - return bool(get_nickname()) - - -def is_renewal_master(): - ca = cainstance.CAInstance(host_name=api.env.host) - return ca.is_renewal_master() - - -@contextlib.contextmanager -def ldap_connect(): - conn = None - try: - conn = ldap2(api) - conn.connect(ccache=os.environ['KRB5CCNAME']) - yield conn - finally: - if conn is not None and conn.isconnected(): - conn.disconnect() - -def call_handler(_handler, *args, **kwargs): - """ - Request handler call wrapper - - Before calling the handler, get the original profile name and cookie from - the provided cookie, if there is one. If the profile name does not match - the requested profile name, drop the cookie and restart the request. - - After calling the handler, put the requested profile name and cookie - returned by the handler in a new cookie and return it. - """ - operation = os.environ['CERTMONGER_OPERATION'] - if operation == 'POLL': - cookie = os.environ.pop('CERTMONGER_CA_COOKIE', None) - if cookie is not None: - try: - context = json.loads(cookie) - if not isinstance(context, dict): - raise TypeError - except (TypeError, ValueError): - return (UNCONFIGURED, "Invalid cookie: %r" % cookie) - else: - return (UNCONFIGURED, "Cookie not provided") - - if 'profile' in context: - profile = context.pop('profile') - try: - if profile is not None: - if not isinstance(profile, unicode): - raise TypeError - profile = (profile.encode('raw_unicode_escape') - .decode('ascii')) - except (TypeError, UnicodeEncodeError): - return (UNCONFIGURED, - "Invalid 'profile' in cookie: %r" % profile) - else: - return (UNCONFIGURED, "No 'profile' in cookie") - - # If profile has changed between SUBMIT and POLL, restart request - if os.environ.get('CERTMONGER_CA_PROFILE') != profile: - os.environ['CERTMONGER_OPERATION'] = 'SUBMIT' - context = {} - - if 'cookie' in context: - cookie = context.pop('cookie') - try: - if not isinstance(cookie, unicode): - raise TypeError - cookie = cookie.encode('raw_unicode_escape').decode('ascii') - except (TypeError, UnicodeEncodeError): - return (UNCONFIGURED, - "Invalid 'cookie' in cookie: %r" % cookie) - os.environ['CERTMONGER_CA_COOKIE'] = cookie - else: - context = {} - - result = _handler(*args, **kwargs) - - if result[0] in (WAIT, WAIT_WITH_DELAY): - context['cookie'] = (result[-1].encode('ascii') - .decode('raw_unicode_escape')) - - profile = os.environ.get('CERTMONGER_CA_PROFILE') - if profile is not None: - profile = profile.encode('ascii').decode('raw_unicode_escape') - context['profile'] = profile - - cookie = json.dumps(context) - os.environ['CERTMONGER_CA_COOKIE'] = cookie - if result[0] in (WAIT, WAIT_WITH_DELAY): - result = result[:-1] + (cookie,) - - return result - - -def request_cert(reuse_existing, **kwargs): - """ - Request certificate from IPA CA. - """ - if reuse_existing: - cert = os.environ.get('CERTMONGER_CERTIFICATE') - if cert: - return (ISSUED, cert) - else: - return (REJECTED, "New certificate requests not supported") - - syslog.syslog(syslog.LOG_NOTICE, - "Forwarding request to dogtag-ipa-renew-agent") - - args = ([paths.DOGTAG_IPA_RENEW_AGENT_SUBMIT, - "--cafile", paths.IPA_CA_CRT, - "--certfile", paths.RA_AGENT_PEM, - "--keyfile", paths.RA_AGENT_KEY] + - sys.argv[1:] + - ['--submit-option', "requestor_name=IPA"]) - if os.environ.get('CERTMONGER_CA_PROFILE') == 'caCACert': - args += ['-N', '-O', 'bypassCAnotafter=true'] - result = ipautil.run(args, raiseonerr=False, env=os.environ, - capture_output=True) - if six.PY2: - sys.stderr.write(result.raw_error_output) - else: - # Write bytes directly - sys.stderr.buffer.write(result.raw_error_output) #pylint: disable=no-member - sys.stderr.flush() - - syslog.syslog(syslog.LOG_NOTICE, - "dogtag-ipa-renew-agent returned %d" % result.returncode) - - stdout = result.output - if stdout.endswith('\n'): - stdout = stdout[:-1] - - rc = result.returncode - if rc == WAIT_WITH_DELAY: - delay, _sep, cookie = stdout.partition('\n') - return (rc, delay, cookie) - else: - return (rc, stdout) - - -def store_cert(**kwargs): - """ - Store certificate in LDAP. - """ - operation = os.environ.get('CERTMONGER_OPERATION') - if operation == 'SUBMIT': - attempts = 0 - elif operation == 'POLL': - cookie = os.environ.get('CERTMONGER_CA_COOKIE') - if not cookie: - return (UNCONFIGURED, "Cookie not provided") - - try: - attempts = int(cookie) - except ValueError: - return (UNCONFIGURED, "Invalid cookie: %r" % cookie) - else: - return (OPERATION_NOT_SUPPORTED_BY_HELPER,) - - nickname = get_nickname() - if not nickname: - return (REJECTED, "Nickname could not be determined") - - cert = os.environ.get('CERTMONGER_CERTIFICATE') - if not cert: - return (REJECTED, "New certificate requests not supported") - cert = x509.load_pem_x509_certificate(cert.encode('ascii')) - - dn = DN(('cn', nickname), ('cn', 'ca_renewal'), - ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) - try: - with ldap_connect() as conn: - try: - entry = conn.get_entry(dn, ['usercertificate']) - entry['usercertificate'] = [cert] - conn.update_entry(entry) - except errors.NotFound: - entry = conn.make_entry( - dn, - objectclass=['top', 'pkiuser', 'nscontainer'], - cn=[nickname], - usercertificate=[cert]) - conn.add_entry(entry) - except errors.EmptyModlist: - pass - except Exception as e: - attempts += 1 - if attempts < 10: - syslog.syslog( - syslog.LOG_ERR, - "Updating renewal certificate failed: %s. Sleeping 30s" % e) - return (WAIT_WITH_DELAY, 30, str(attempts)) - else: - syslog.syslog( - syslog.LOG_ERR, - "Giving up. To retry storing the certificate, resubmit the " - "request with CA \"dogtag-ipa-ca-renew-agent-reuse\"") - - return (ISSUED, cert.public_bytes(x509.Encoding.PEM).decode('ascii')) - - -def request_and_store_cert(**kwargs): - """ - Request certificate from IPA CA and store it in LDAP. - """ - operation = os.environ.get('CERTMONGER_OPERATION') - if operation == 'SUBMIT': - state = 'request' - cookie = None - elif operation == 'POLL': - cookie = os.environ.get('CERTMONGER_CA_COOKIE') - if not cookie: - return (UNCONFIGURED, "Cookie not provided") - - state, _sep, cookie = cookie.partition(':') - if state not in ('request', 'store'): - return (UNCONFIGURED, - "Invalid cookie: %r" % os.environ['CERTMONGER_CA_COOKIE']) - else: - return (OPERATION_NOT_SUPPORTED_BY_HELPER,) - - if state == 'request': - if cookie is None: - os.environ['CERTMONGER_OPERATION'] = 'SUBMIT' - else: - os.environ['CERTMONGER_CA_COOKIE'] = cookie - - result = call_handler(request_cert, **kwargs) - if result[0] == WAIT: - return (result[0], 'request:%s' % result[1]) - elif result[0] == WAIT_WITH_DELAY: - return (result[0], result[1], 'request:%s' % result[2]) - elif result[0] != ISSUED: - return result - else: - cert = result[1] - cookie = None - else: - cert, _sep, cookie = cookie.partition(':') - - if cookie is None: - os.environ['CERTMONGER_OPERATION'] = 'SUBMIT' - else: - os.environ['CERTMONGER_CA_COOKIE'] = cookie - os.environ['CERTMONGER_CERTIFICATE'] = cert - - result = call_handler(store_cert, **kwargs) - if result[0] == WAIT: - return (result[0], 'store:%s:%s' % (cert, result[1])) - elif result[0] == WAIT_WITH_DELAY: - return (result[0], result[1], 'store:%s:%s' % (cert, result[2])) - else: - return result - - -def retrieve_or_reuse_cert(**kwargs): - """ - Retrieve certificate from LDAP. If the certificate is not available, reuse - the old certificate. - """ - nickname = get_nickname() - if not nickname: - return (REJECTED, "Nickname could not be determined") - - cert = os.environ.get('CERTMONGER_CERTIFICATE') - if not cert: - return (REJECTED, "New certificate requests not supported") - cert = x509.load_pem_x509_certificate(cert.encode('ascii')) - - with ldap_connect() as conn: - try: - entry = conn.get_entry( - DN(('cn', nickname), ('cn', 'ca_renewal'), - ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn), - ['usercertificate']) - except errors.NotFound: - pass - else: - cert = entry.single_value['usercertificate'] - - return (ISSUED, cert.public_bytes(x509.Encoding.PEM).decode('ascii')) - - -def retrieve_cert_continuous(reuse_existing, **kwargs): - """ - Retrieve new certificate from LDAP. Repeat every eight hours until the - certificate is available. - """ - old_cert = os.environ.get('CERTMONGER_CERTIFICATE') - if old_cert: - old_cert = x509.load_pem_x509_certificate(old_cert.encode('ascii')) - - result = call_handler(retrieve_or_reuse_cert, - reuse_existing=reuse_existing, - **kwargs) - if result[0] != ISSUED or reuse_existing: - return result - - new_cert = x509.load_pem_x509_certificate(result[1].encode('ascii')) - if new_cert == old_cert: - syslog.syslog(syslog.LOG_INFO, "Updated certificate not available") - # No cert available yet, tell certmonger to wait another 8 hours - return (WAIT_WITH_DELAY, 8 * 60 * 60, '') - - return result - - -def retrieve_cert(**kwargs): - """ - Retrieve new certificate from LDAP. - """ - result = call_handler(retrieve_cert_continuous, **kwargs) - if result[0] == WAIT_WITH_DELAY: - return (REJECTED, "Updated certificate not available") - - return result - - -def renew_ca_cert(reuse_existing, **kwargs): - """ - This is used for automatic CA certificate renewal. - """ - csr = os.environ.get('CERTMONGER_CSR').encode('ascii') - if not csr: - return (UNCONFIGURED, "Certificate request not provided") - - cert = os.environ.get('CERTMONGER_CERTIFICATE') - if not cert: - return (REJECTED, "New certificate requests not supported") - cert = x509.load_pem_x509_certificate(cert.encode('ascii')) - is_self_signed = cert.is_self_signed() - - operation = os.environ.get('CERTMONGER_OPERATION') - if operation == 'SUBMIT': - state = 'retrieve' - - if not reuse_existing and is_renewal_master(): - state = 'request' - - csr_file = paths.IPA_CA_CSR - try: - with open(csr_file, 'wb') as f: - f.write(csr) - except Exception as e: - return (UNREACHABLE, "Failed to write %s: %s" % (csr_file, e)) - elif operation == 'POLL': - cookie = os.environ.get('CERTMONGER_CA_COOKIE') - if not cookie: - return (UNCONFIGURED, "Cookie not provided") - - state, _sep, cookie = cookie.partition(':') - if state not in ('retrieve', 'request'): - return (UNCONFIGURED, - "Invalid cookie: %r" % os.environ['CERTMONGER_CA_COOKIE']) - - os.environ['CERTMONGER_CA_COOKIE'] = cookie - else: - return (OPERATION_NOT_SUPPORTED_BY_HELPER,) - - if state == 'retrieve': - result = call_handler(retrieve_cert, - reuse_existing=reuse_existing, - **kwargs) - if result[0] == REJECTED and not is_self_signed and not reuse_existing: - syslog.syslog(syslog.LOG_ALERT, - "Certificate with subject '%s' is about to expire, " - "use ipa-cacert-manage to renew it" - % (os.environ.get("CERTMONGER_REQ_SUBJECT"),)) - elif state == 'request': - profile = os.environ.get('CERTMONGER_CA_PROFILE') - os.environ['CERTMONGER_CA_PROFILE'] = 'caCACert' - result = call_handler(request_and_store_cert, - reuse_existing=reuse_existing, - **kwargs) - if profile is not None: - os.environ['CERTMONGER_CA_PROFILE'] = profile - else: - del os.environ['CERTMONGER_CA_PROFILE'] - - if result[0] == WAIT: - return (result[0], '%s:%s' % (state, result[1])) - elif result[0] == WAIT_WITH_DELAY: - return (result[0], result[1], '%s:%s' % (state, result[2])) - else: - return result - -def main(): - kwargs = { - 'reuse_existing': False, - } - try: - sys.argv.remove('--reuse-existing') - except ValueError: - pass - else: - kwargs['reuse_existing'] = True - - api.bootstrap(in_server=True, context='renew', confdir=paths.ETC_IPA) - api.finalize() - - operation = os.environ.get('CERTMONGER_OPERATION') - if operation not in ('SUBMIT', 'POLL'): - return OPERATION_NOT_SUPPORTED_BY_HELPER - - tmpdir = tempfile.mkdtemp(prefix="tmp-") - certs.renewal_lock.acquire() - try: - principal = str('host/%s@%s' % (api.env.host, api.env.realm)) - ccache_filename = os.path.join(tmpdir, 'ccache') - os.environ['KRB5CCNAME'] = ccache_filename - kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_filename) - - api.Backend.ldap2.connect() - - if get_nickname() == IPA_CA_NICKNAME: - handler = renew_ca_cert - elif is_replicated(): - if is_renewal_master(): - handler = request_and_store_cert - else: - handler = retrieve_cert_continuous - else: - handler = request_cert - - res = call_handler(handler, **kwargs) - for item in res[1:]: - print(item) - return res[0] - finally: - if api.Backend.ldap2.isconnected(): - api.Backend.ldap2.disconnect() - certs.renewal_lock.release() - shutil.rmtree(tmpdir) - - -try: - sys.exit(main()) -except Exception as e: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) - print("Internal error") - sys.exit(UNREACHABLE) diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit.in b/install/certmonger/dogtag-ipa-ca-renew-agent-submit.in new file mode 100644 index 0000000..6961740 --- /dev/null +++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit.in @@ -0,0 +1,545 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Jan Cholasta +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +import os +# Prevent garbage from readline on standard output +# (see https://fedorahosted.org/freeipa/ticket/4064) +if not os.isatty(1): + os.environ['TERM'] = 'dumb' +import sys +import syslog +import traceback +import tempfile +import shutil +import contextlib +import json + +from cryptography import x509 as crypto_x509 +from cryptography.hazmat.backends import default_backend + +import six + +from ipalib.install.kinit import kinit_keytab +from ipapython import ipautil +from ipapython.dn import DN +from ipalib import api, errors, x509 +from ipaplatform.paths import paths +from ipaserver.plugins.ldap2 import ldap2 +from ipaserver.install import ca, cainstance, dsinstance, certs + +# This is a certmonger CA helper script for IPA CA subsystem cert renewal. See +# https://git.fedorahosted.org/cgit/certmonger.git/tree/doc/submit.txt for more +# info on certmonger CA helper scripts. + +# Return codes. Names of the constants are taken from +# https://git.fedorahosted.org/cgit/certmonger.git/tree/src/submit-e.h +ISSUED = 0 +WAIT = 1 +REJECTED = 2 +UNREACHABLE = 3 +UNCONFIGURED = 4 +WAIT_WITH_DELAY = 5 +OPERATION_NOT_SUPPORTED_BY_HELPER = 6 + +if six.PY3: + unicode = str + + +IPA_CA_NICKNAME = 'caSigningCert cert-pki-ca' + + +def get_nickname(): + # we need to get the subject from a CSR in case we are requesting + # an OpenSSL certificate for which we have to reverse the order of its DN + # components thus changing the CERTMONGER_REQ_SUBJECT + # https://pagure.io/certmonger/issue/62 + csr = os.environ.get('CERTMONGER_CSR').encode('ascii') + csr_obj = crypto_x509.load_pem_x509_csr(csr, default_backend()) + subject = csr_obj.subject + if not subject: + return None + + subject_base = dsinstance.DsInstance().find_subject_base() + if not subject_base: + return None + + ca_subject_dn = ca.lookup_ca_subject(api, subject_base) + + nickname_by_subject_dn = { + DN(ca_subject_dn): 'caSigningCert cert-pki-ca', + DN('CN=CA Audit', subject_base): 'auditSigningCert cert-pki-ca', + DN('CN=OCSP Subsystem', subject_base): 'ocspSigningCert cert-pki-ca', + DN('CN=CA Subsystem', subject_base): 'subsystemCert cert-pki-ca', + DN('CN=KRA Audit', subject_base): 'auditSigningCert cert-pki-kra', + DN('CN=KRA Transport Certificate', subject_base): + 'transportCert cert-pki-kra', + DN('CN=KRA Storage Certificate', subject_base): + 'storageCert cert-pki-kra', + DN('CN=IPA RA', subject_base): 'ipaCert', + } + + return nickname_by_subject_dn.get(DN(subject)) + + +def is_replicated(): + return bool(get_nickname()) + + +def is_renewal_master(): + ca = cainstance.CAInstance(host_name=api.env.host) + return ca.is_renewal_master() + + +@contextlib.contextmanager +def ldap_connect(): + conn = None + try: + conn = ldap2(api) + conn.connect(ccache=os.environ['KRB5CCNAME']) + yield conn + finally: + if conn is not None and conn.isconnected(): + conn.disconnect() + +def call_handler(_handler, *args, **kwargs): + """ + Request handler call wrapper + + Before calling the handler, get the original profile name and cookie from + the provided cookie, if there is one. If the profile name does not match + the requested profile name, drop the cookie and restart the request. + + After calling the handler, put the requested profile name and cookie + returned by the handler in a new cookie and return it. + """ + operation = os.environ['CERTMONGER_OPERATION'] + if operation == 'POLL': + cookie = os.environ.pop('CERTMONGER_CA_COOKIE', None) + if cookie is not None: + try: + context = json.loads(cookie) + if not isinstance(context, dict): + raise TypeError + except (TypeError, ValueError): + return (UNCONFIGURED, "Invalid cookie: %r" % cookie) + else: + return (UNCONFIGURED, "Cookie not provided") + + if 'profile' in context: + profile = context.pop('profile') + try: + if profile is not None: + if not isinstance(profile, unicode): + raise TypeError + profile = (profile.encode('raw_unicode_escape') + .decode('ascii')) + except (TypeError, UnicodeEncodeError): + return (UNCONFIGURED, + "Invalid 'profile' in cookie: %r" % profile) + else: + return (UNCONFIGURED, "No 'profile' in cookie") + + # If profile has changed between SUBMIT and POLL, restart request + if os.environ.get('CERTMONGER_CA_PROFILE') != profile: + os.environ['CERTMONGER_OPERATION'] = 'SUBMIT' + context = {} + + if 'cookie' in context: + cookie = context.pop('cookie') + try: + if not isinstance(cookie, unicode): + raise TypeError + cookie = cookie.encode('raw_unicode_escape').decode('ascii') + except (TypeError, UnicodeEncodeError): + return (UNCONFIGURED, + "Invalid 'cookie' in cookie: %r" % cookie) + os.environ['CERTMONGER_CA_COOKIE'] = cookie + else: + context = {} + + result = _handler(*args, **kwargs) + + if result[0] in (WAIT, WAIT_WITH_DELAY): + context['cookie'] = (result[-1].encode('ascii') + .decode('raw_unicode_escape')) + + profile = os.environ.get('CERTMONGER_CA_PROFILE') + if profile is not None: + profile = profile.encode('ascii').decode('raw_unicode_escape') + context['profile'] = profile + + cookie = json.dumps(context) + os.environ['CERTMONGER_CA_COOKIE'] = cookie + if result[0] in (WAIT, WAIT_WITH_DELAY): + result = result[:-1] + (cookie,) + + return result + + +def request_cert(reuse_existing, **kwargs): + """ + Request certificate from IPA CA. + """ + if reuse_existing: + cert = os.environ.get('CERTMONGER_CERTIFICATE') + if cert: + return (ISSUED, cert) + else: + return (REJECTED, "New certificate requests not supported") + + syslog.syslog(syslog.LOG_NOTICE, + "Forwarding request to dogtag-ipa-renew-agent") + + args = ([paths.DOGTAG_IPA_RENEW_AGENT_SUBMIT, + "--cafile", paths.IPA_CA_CRT, + "--certfile", paths.RA_AGENT_PEM, + "--keyfile", paths.RA_AGENT_KEY] + + sys.argv[1:] + + ['--submit-option', "requestor_name=IPA"]) + if os.environ.get('CERTMONGER_CA_PROFILE') == 'caCACert': + args += ['-N', '-O', 'bypassCAnotafter=true'] + result = ipautil.run(args, raiseonerr=False, env=os.environ, + capture_output=True) + if six.PY2: + sys.stderr.write(result.raw_error_output) + else: + # Write bytes directly + sys.stderr.buffer.write(result.raw_error_output) #pylint: disable=no-member + sys.stderr.flush() + + syslog.syslog(syslog.LOG_NOTICE, + "dogtag-ipa-renew-agent returned %d" % result.returncode) + + stdout = result.output + if stdout.endswith('\n'): + stdout = stdout[:-1] + + rc = result.returncode + if rc == WAIT_WITH_DELAY: + delay, _sep, cookie = stdout.partition('\n') + return (rc, delay, cookie) + else: + return (rc, stdout) + + +def store_cert(**kwargs): + """ + Store certificate in LDAP. + """ + operation = os.environ.get('CERTMONGER_OPERATION') + if operation == 'SUBMIT': + attempts = 0 + elif operation == 'POLL': + cookie = os.environ.get('CERTMONGER_CA_COOKIE') + if not cookie: + return (UNCONFIGURED, "Cookie not provided") + + try: + attempts = int(cookie) + except ValueError: + return (UNCONFIGURED, "Invalid cookie: %r" % cookie) + else: + return (OPERATION_NOT_SUPPORTED_BY_HELPER,) + + nickname = get_nickname() + if not nickname: + return (REJECTED, "Nickname could not be determined") + + cert = os.environ.get('CERTMONGER_CERTIFICATE') + if not cert: + return (REJECTED, "New certificate requests not supported") + cert = x509.load_pem_x509_certificate(cert.encode('ascii')) + + dn = DN(('cn', nickname), ('cn', 'ca_renewal'), + ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) + try: + with ldap_connect() as conn: + try: + entry = conn.get_entry(dn, ['usercertificate']) + entry['usercertificate'] = [cert] + conn.update_entry(entry) + except errors.NotFound: + entry = conn.make_entry( + dn, + objectclass=['top', 'pkiuser', 'nscontainer'], + cn=[nickname], + usercertificate=[cert]) + conn.add_entry(entry) + except errors.EmptyModlist: + pass + except Exception as e: + attempts += 1 + if attempts < 10: + syslog.syslog( + syslog.LOG_ERR, + "Updating renewal certificate failed: %s. Sleeping 30s" % e) + return (WAIT_WITH_DELAY, 30, str(attempts)) + else: + syslog.syslog( + syslog.LOG_ERR, + "Giving up. To retry storing the certificate, resubmit the " + "request with CA \"dogtag-ipa-ca-renew-agent-reuse\"") + + return (ISSUED, cert.public_bytes(x509.Encoding.PEM).decode('ascii')) + + +def request_and_store_cert(**kwargs): + """ + Request certificate from IPA CA and store it in LDAP. + """ + operation = os.environ.get('CERTMONGER_OPERATION') + if operation == 'SUBMIT': + state = 'request' + cookie = None + elif operation == 'POLL': + cookie = os.environ.get('CERTMONGER_CA_COOKIE') + if not cookie: + return (UNCONFIGURED, "Cookie not provided") + + state, _sep, cookie = cookie.partition(':') + if state not in ('request', 'store'): + return (UNCONFIGURED, + "Invalid cookie: %r" % os.environ['CERTMONGER_CA_COOKIE']) + else: + return (OPERATION_NOT_SUPPORTED_BY_HELPER,) + + if state == 'request': + if cookie is None: + os.environ['CERTMONGER_OPERATION'] = 'SUBMIT' + else: + os.environ['CERTMONGER_CA_COOKIE'] = cookie + + result = call_handler(request_cert, **kwargs) + if result[0] == WAIT: + return (result[0], 'request:%s' % result[1]) + elif result[0] == WAIT_WITH_DELAY: + return (result[0], result[1], 'request:%s' % result[2]) + elif result[0] != ISSUED: + return result + else: + cert = result[1] + cookie = None + else: + cert, _sep, cookie = cookie.partition(':') + + if cookie is None: + os.environ['CERTMONGER_OPERATION'] = 'SUBMIT' + else: + os.environ['CERTMONGER_CA_COOKIE'] = cookie + os.environ['CERTMONGER_CERTIFICATE'] = cert + + result = call_handler(store_cert, **kwargs) + if result[0] == WAIT: + return (result[0], 'store:%s:%s' % (cert, result[1])) + elif result[0] == WAIT_WITH_DELAY: + return (result[0], result[1], 'store:%s:%s' % (cert, result[2])) + else: + return result + + +def retrieve_or_reuse_cert(**kwargs): + """ + Retrieve certificate from LDAP. If the certificate is not available, reuse + the old certificate. + """ + nickname = get_nickname() + if not nickname: + return (REJECTED, "Nickname could not be determined") + + cert = os.environ.get('CERTMONGER_CERTIFICATE') + if not cert: + return (REJECTED, "New certificate requests not supported") + cert = x509.load_pem_x509_certificate(cert.encode('ascii')) + + with ldap_connect() as conn: + try: + entry = conn.get_entry( + DN(('cn', nickname), ('cn', 'ca_renewal'), + ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn), + ['usercertificate']) + except errors.NotFound: + pass + else: + cert = entry.single_value['usercertificate'] + + return (ISSUED, cert.public_bytes(x509.Encoding.PEM).decode('ascii')) + + +def retrieve_cert_continuous(reuse_existing, **kwargs): + """ + Retrieve new certificate from LDAP. Repeat every eight hours until the + certificate is available. + """ + old_cert = os.environ.get('CERTMONGER_CERTIFICATE') + if old_cert: + old_cert = x509.load_pem_x509_certificate(old_cert.encode('ascii')) + + result = call_handler(retrieve_or_reuse_cert, + reuse_existing=reuse_existing, + **kwargs) + if result[0] != ISSUED or reuse_existing: + return result + + new_cert = x509.load_pem_x509_certificate(result[1].encode('ascii')) + if new_cert == old_cert: + syslog.syslog(syslog.LOG_INFO, "Updated certificate not available") + # No cert available yet, tell certmonger to wait another 8 hours + return (WAIT_WITH_DELAY, 8 * 60 * 60, '') + + return result + + +def retrieve_cert(**kwargs): + """ + Retrieve new certificate from LDAP. + """ + result = call_handler(retrieve_cert_continuous, **kwargs) + if result[0] == WAIT_WITH_DELAY: + return (REJECTED, "Updated certificate not available") + + return result + + +def renew_ca_cert(reuse_existing, **kwargs): + """ + This is used for automatic CA certificate renewal. + """ + csr = os.environ.get('CERTMONGER_CSR').encode('ascii') + if not csr: + return (UNCONFIGURED, "Certificate request not provided") + + cert = os.environ.get('CERTMONGER_CERTIFICATE') + if not cert: + return (REJECTED, "New certificate requests not supported") + cert = x509.load_pem_x509_certificate(cert.encode('ascii')) + is_self_signed = cert.is_self_signed() + + operation = os.environ.get('CERTMONGER_OPERATION') + if operation == 'SUBMIT': + state = 'retrieve' + + if not reuse_existing and is_renewal_master(): + state = 'request' + + csr_file = paths.IPA_CA_CSR + try: + with open(csr_file, 'wb') as f: + f.write(csr) + except Exception as e: + return (UNREACHABLE, "Failed to write %s: %s" % (csr_file, e)) + elif operation == 'POLL': + cookie = os.environ.get('CERTMONGER_CA_COOKIE') + if not cookie: + return (UNCONFIGURED, "Cookie not provided") + + state, _sep, cookie = cookie.partition(':') + if state not in ('retrieve', 'request'): + return (UNCONFIGURED, + "Invalid cookie: %r" % os.environ['CERTMONGER_CA_COOKIE']) + + os.environ['CERTMONGER_CA_COOKIE'] = cookie + else: + return (OPERATION_NOT_SUPPORTED_BY_HELPER,) + + if state == 'retrieve': + result = call_handler(retrieve_cert, + reuse_existing=reuse_existing, + **kwargs) + if result[0] == REJECTED and not is_self_signed and not reuse_existing: + syslog.syslog(syslog.LOG_ALERT, + "Certificate with subject '%s' is about to expire, " + "use ipa-cacert-manage to renew it" + % (os.environ.get("CERTMONGER_REQ_SUBJECT"),)) + elif state == 'request': + profile = os.environ.get('CERTMONGER_CA_PROFILE') + os.environ['CERTMONGER_CA_PROFILE'] = 'caCACert' + result = call_handler(request_and_store_cert, + reuse_existing=reuse_existing, + **kwargs) + if profile is not None: + os.environ['CERTMONGER_CA_PROFILE'] = profile + else: + del os.environ['CERTMONGER_CA_PROFILE'] + + if result[0] == WAIT: + return (result[0], '%s:%s' % (state, result[1])) + elif result[0] == WAIT_WITH_DELAY: + return (result[0], result[1], '%s:%s' % (state, result[2])) + else: + return result + +def main(): + kwargs = { + 'reuse_existing': False, + } + try: + sys.argv.remove('--reuse-existing') + except ValueError: + pass + else: + kwargs['reuse_existing'] = True + + api.bootstrap(in_server=True, context='renew', confdir=paths.ETC_IPA) + api.finalize() + + operation = os.environ.get('CERTMONGER_OPERATION') + if operation not in ('SUBMIT', 'POLL'): + return OPERATION_NOT_SUPPORTED_BY_HELPER + + tmpdir = tempfile.mkdtemp(prefix="tmp-") + certs.renewal_lock.acquire() + try: + principal = str('host/%s@%s' % (api.env.host, api.env.realm)) + ccache_filename = os.path.join(tmpdir, 'ccache') + os.environ['KRB5CCNAME'] = ccache_filename + kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_filename) + + api.Backend.ldap2.connect() + + if get_nickname() == IPA_CA_NICKNAME: + handler = renew_ca_cert + elif is_replicated(): + if is_renewal_master(): + handler = request_and_store_cert + else: + handler = retrieve_cert_continuous + else: + handler = request_cert + + res = call_handler(handler, **kwargs) + for item in res[1:]: + print(item) + return res[0] + finally: + if api.Backend.ldap2.isconnected(): + api.Backend.ldap2.disconnect() + certs.renewal_lock.release() + shutil.rmtree(tmpdir) + + +try: + sys.exit(main()) +except Exception as e: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) + print("Internal error") + sys.exit(UNREACHABLE) diff --git a/install/certmonger/ipa-server-guard b/install/certmonger/ipa-server-guard deleted file mode 100755 index 9ee2a5f..0000000 --- a/install/certmonger/ipa-server-guard +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Jan Cholasta -# -# Copyright (C) 2015 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import print_function - -import os -# Prevent garbage from readline on standard output -# (see https://fedorahosted.org/freeipa/ticket/4064) -if not os.isatty(1): - os.environ['TERM'] = 'dumb' -import sys -import syslog -import traceback - -import six - -from ipapython import ipautil -from ipaserver.install import certs - - -def main(): - if len(sys.argv) < 2: - raise RuntimeError("Not enough arguments") - - with certs.renewal_lock: - result = ipautil.run(sys.argv[1:], raiseonerr=False, env=os.environ) - if six.PY2: - sys.stdout.write(result.raw_output) - sys.stderr.write(result.raw_error_output) - else: - # Write bytes directly - sys.stdout.buffer.write(result.raw_output) #pylint: disable=no-member - sys.stderr.buffer.write(result.raw_error_output) #pylint: disable=no-member - sys.stdout.flush() - sys.stderr.flush() - - return result.returncode - - -try: - sys.exit(main()) -except Exception as e: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) - print("Internal error") - sys.exit(3) diff --git a/install/certmonger/ipa-server-guard.in b/install/certmonger/ipa-server-guard.in new file mode 100644 index 0000000..33c1601 --- /dev/null +++ b/install/certmonger/ipa-server-guard.in @@ -0,0 +1,63 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Jan Cholasta +# +# Copyright (C) 2015 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +import os +# Prevent garbage from readline on standard output +# (see https://fedorahosted.org/freeipa/ticket/4064) +if not os.isatty(1): + os.environ['TERM'] = 'dumb' +import sys +import syslog +import traceback + +import six + +from ipapython import ipautil +from ipaserver.install import certs + + +def main(): + if len(sys.argv) < 2: + raise RuntimeError("Not enough arguments") + + with certs.renewal_lock: + result = ipautil.run(sys.argv[1:], raiseonerr=False, env=os.environ) + if six.PY2: + sys.stdout.write(result.raw_output) + sys.stderr.write(result.raw_error_output) + else: + # Write bytes directly + sys.stdout.buffer.write(result.raw_output) #pylint: disable=no-member + sys.stderr.buffer.write(result.raw_error_output) #pylint: disable=no-member + sys.stdout.flush() + sys.stderr.flush() + + return result.returncode + + +try: + sys.exit(main()) +except Exception as e: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) + print("Internal error") + sys.exit(3) diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains b/install/oddjob/com.redhat.idm.trust-fetch-domains deleted file mode 100755 index 51ffd8c..0000000 --- a/install/oddjob/com.redhat.idm.trust-fetch-domains +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/python3 - -from ipaserver import dcerpc -from ipaserver.install.installutils import is_ipa_configured, ScriptError -from ipapython import config, ipautil -from ipalib import api -from ipapython.dn import DN -from ipaplatform.constants import constants -from ipaplatform.paths import paths -import sys -import os -import pwd - -import six -import gssapi - -from ipalib.install.kinit import kinit_keytab - -if six.PY3: - unicode = str - -def retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal): - getkeytab_args = ["/usr/sbin/ipa-getkeytab", - "-s", api.env.host, - "-p", oneway_principal, - "-k", oneway_keytab_name, - "-r"] - if os.path.isfile(oneway_keytab_name): - os.unlink(oneway_keytab_name) - - ipautil.run(getkeytab_args, env={'KRB5CCNAME': ccache_name, 'LANG': 'C'}, - raiseonerr=False) - # Make sure SSSD is able to read the keytab - try: - sssd = pwd.getpwnam(constants.SSSD_USER) - os.chown(oneway_keytab_name, sssd[2], sssd[3]) - except KeyError: - # If user 'sssd' does not exist, we don't need to chown from root to sssd - # because it means SSSD does not run as sssd user - pass - - -def get_forest_root_domain(api_instance, trusted_domain): - """ - retrieve trusted forest root domain for given domain name - - :param api_instance: IPA API instance - :param trusted_domain: trusted domain name - - :returns: forest root domain DNS name - """ - trustconfig_show = api_instance.Command.trustconfig_show - flatname = trustconfig_show()['result']['ipantflatname'][0] - - remote_domain = dcerpc.retrieve_remote_domain( - api_instance.env.host, flatname, trusted_domain) - - return remote_domain.info['dns_forest'] - - -def parse_options(): - usage = "%prog \n" - parser = config.IPAOptionParser(usage=usage, - formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information") - - options, args = parser.parse_args() - safe_options = parser.get_safe_opts(options) - - return safe_options, options, args - - -if not is_ipa_configured(): - # LSB status code 6: program is not configured - raise ScriptError("IPA is not configured " + - "(see man pages of ipa-server-install for help)", 6) - -if not os.getegid() == 0: - # LSB status code 4: user had insufficient privilege - raise ScriptError("You must be root to run ipactl.", 4) - -safe_options, options, args = parse_options() - -if len(args) != 1: - # LSB status code 2: invalid or excess argument(s) - raise ScriptError("You must specify trusted domain name", 2) - -trusted_domain = ipautil.fsdecode(args[0]).lower() - -api.bootstrap(in_server=True, log=None, - context='server', confdir=paths.ETC_IPA) -api.finalize() - -# Only import trust plugin after api is initialized or internal imports -# within the plugin will not work -from ipaserver.plugins import trust - -# We have to dance with two different credentials caches: -# ccache_name -- for cifs/ipa.master@IPA.REALM to communicate with LDAP -# oneway_ccache_name -- for IPA$@AD.REALM to communicate with AD DCs -# -# ccache_name may not exist, we'll have to initialize it from Samba's keytab -# -# oneway_ccache_name may not exist either but to initialize it, we need -# to check if oneway_keytab_name keytab exists and fetch it first otherwise. -# -# to fetch oneway_keytab_name keytab, we need to initialize ccache_name ccache first -# and retrieve our own NetBIOS domain name and use cifs/ipa.master@IPA.REALM to -# retrieve the keys to oneway_keytab_name. - -keytab_name = '/etc/samba/samba.keytab' - -principal = str('cifs/' + api.env.host) - -oneway_ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts_fetch' -ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts' - -# Standard sequence: -# - check if ccache exists -# - if not, initialize it from Samba's keytab -# - check if ccache contains valid TGT -# - if not, initialize it from Samba's keytab -# - refer the correct ccache object for further use -# -have_ccache = False -try: - cred = kinit_keytab(principal, keytab_name, ccache_name) - if cred.lifetime > 0: - have_ccache = True -except gssapi.exceptions.ExpiredCredentialsError: - pass -if not have_ccache: - # delete stale ccache and try again - if os.path.exists(oneway_ccache_name): - os.unlink(ccache_name) - cred = kinit_keytab(principal, keytab_name, ccache_name) - -old_ccache = os.environ.get('KRB5CCNAME') -api.Backend.ldap2.connect(ccache_name) - -# Retrieve own NetBIOS name and trusted forest's name. -# We use script's input to retrieve the trusted forest's name to sanitize input -# for file-level access as we might need to wipe out keytab in /var/lib/sss/keytabs -own_trust_dn = DN(('cn', api.env.domain),('cn','ad'), ('cn', 'etc'), api.env.basedn) -own_trust_entry = api.Backend.ldap2.get_entry(own_trust_dn, ['ipantflatname']) -own_trust_flatname = own_trust_entry.single_value.get('ipantflatname').upper() -trusted_domain_dn = DN(('cn', trusted_domain.lower()), api.env.container_adtrusts, api.env.basedn) -trusted_domain_entry = api.Backend.ldap2.get_entry(trusted_domain_dn, ['cn']) -trusted_domain = trusted_domain_entry.single_value.get('cn').lower() - -# At this point if we didn't find trusted forest name, an exception will be raised -# and script will quit. This is actually intended. - -oneway_keytab_name = '/var/lib/sss/keytabs/' + trusted_domain + '.keytab' -oneway_principal = str('%s$@%s' % (own_trust_flatname, trusted_domain.upper())) - -# If keytab does not exist, retrieve it -if not os.path.isfile(oneway_keytab_name): - retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal) - -try: - have_ccache = False - try: - # The keytab may have stale key material (from older trust-add run) - cred = kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) - if cred.lifetime > 0: - have_ccache = True - except gssapi.exceptions.ExpiredCredentialsError: - pass - if not have_ccache: - if os.path.exists(oneway_ccache_name): - os.unlink(oneway_ccache_name) - kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) -except gssapi.exceptions.GSSError: - # If there was failure on using keytab, assume it is stale and retrieve again - retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal) - if os.path.exists(oneway_ccache_name): - os.unlink(oneway_ccache_name) - kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) - -# We are done: we have ccache with TDO credentials and can fetch domains -ipa_domain = api.env.domain -os.environ['KRB5CCNAME'] = oneway_ccache_name - -# retrieve the forest root domain name and contact it to retrieve trust -# topology info -forest_root = get_forest_root_domain(api, trusted_domain) - -domains = dcerpc.fetch_domains(api, ipa_domain, forest_root, creds=True) -trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)['result'] -trust.add_new_domains_from_trust(api, None, trust_domain_object, domains) - -if old_ccache: - os.environ['KRB5CCNAME'] = old_ccache - -sys.exit(0) diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains.in b/install/oddjob/com.redhat.idm.trust-fetch-domains.in new file mode 100644 index 0000000..50830bd --- /dev/null +++ b/install/oddjob/com.redhat.idm.trust-fetch-domains.in @@ -0,0 +1,198 @@ +@PYTHONSHEBANG@ + +from ipaserver import dcerpc +from ipaserver.install.installutils import is_ipa_configured, ScriptError +from ipapython import config, ipautil +from ipalib import api +from ipapython.dn import DN +from ipaplatform.constants import constants +from ipaplatform.paths import paths +import sys +import os +import pwd + +import six +import gssapi + +from ipalib.install.kinit import kinit_keytab + +if six.PY3: + unicode = str + +def retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal): + getkeytab_args = ["/usr/sbin/ipa-getkeytab", + "-s", api.env.host, + "-p", oneway_principal, + "-k", oneway_keytab_name, + "-r"] + if os.path.isfile(oneway_keytab_name): + os.unlink(oneway_keytab_name) + + ipautil.run(getkeytab_args, env={'KRB5CCNAME': ccache_name, 'LANG': 'C'}, + raiseonerr=False) + # Make sure SSSD is able to read the keytab + try: + sssd = pwd.getpwnam(constants.SSSD_USER) + os.chown(oneway_keytab_name, sssd[2], sssd[3]) + except KeyError: + # If user 'sssd' does not exist, we don't need to chown from root to sssd + # because it means SSSD does not run as sssd user + pass + + +def get_forest_root_domain(api_instance, trusted_domain): + """ + retrieve trusted forest root domain for given domain name + + :param api_instance: IPA API instance + :param trusted_domain: trusted domain name + + :returns: forest root domain DNS name + """ + trustconfig_show = api_instance.Command.trustconfig_show + flatname = trustconfig_show()['result']['ipantflatname'][0] + + remote_domain = dcerpc.retrieve_remote_domain( + api_instance.env.host, flatname, trusted_domain) + + return remote_domain.info['dns_forest'] + + +def parse_options(): + usage = "%prog \n" + parser = config.IPAOptionParser(usage=usage, + formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information") + + options, args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + return safe_options, options, args + + +if not is_ipa_configured(): + # LSB status code 6: program is not configured + raise ScriptError("IPA is not configured " + + "(see man pages of ipa-server-install for help)", 6) + +if not os.getegid() == 0: + # LSB status code 4: user had insufficient privilege + raise ScriptError("You must be root to run ipactl.", 4) + +safe_options, options, args = parse_options() + +if len(args) != 1: + # LSB status code 2: invalid or excess argument(s) + raise ScriptError("You must specify trusted domain name", 2) + +trusted_domain = ipautil.fsdecode(args[0]).lower() + +api.bootstrap(in_server=True, log=None, + context='server', confdir=paths.ETC_IPA) +api.finalize() + +# Only import trust plugin after api is initialized or internal imports +# within the plugin will not work +from ipaserver.plugins import trust + +# We have to dance with two different credentials caches: +# ccache_name -- for cifs/ipa.master@IPA.REALM to communicate with LDAP +# oneway_ccache_name -- for IPA$@AD.REALM to communicate with AD DCs +# +# ccache_name may not exist, we'll have to initialize it from Samba's keytab +# +# oneway_ccache_name may not exist either but to initialize it, we need +# to check if oneway_keytab_name keytab exists and fetch it first otherwise. +# +# to fetch oneway_keytab_name keytab, we need to initialize ccache_name ccache first +# and retrieve our own NetBIOS domain name and use cifs/ipa.master@IPA.REALM to +# retrieve the keys to oneway_keytab_name. + +keytab_name = '/etc/samba/samba.keytab' + +principal = str('cifs/' + api.env.host) + +oneway_ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts_fetch' +ccache_name = '/var/run/ipa/krb5cc_oddjob_trusts' + +# Standard sequence: +# - check if ccache exists +# - if not, initialize it from Samba's keytab +# - check if ccache contains valid TGT +# - if not, initialize it from Samba's keytab +# - refer the correct ccache object for further use +# +have_ccache = False +try: + cred = kinit_keytab(principal, keytab_name, ccache_name) + if cred.lifetime > 0: + have_ccache = True +except gssapi.exceptions.ExpiredCredentialsError: + pass +if not have_ccache: + # delete stale ccache and try again + if os.path.exists(oneway_ccache_name): + os.unlink(ccache_name) + cred = kinit_keytab(principal, keytab_name, ccache_name) + +old_ccache = os.environ.get('KRB5CCNAME') +api.Backend.ldap2.connect(ccache_name) + +# Retrieve own NetBIOS name and trusted forest's name. +# We use script's input to retrieve the trusted forest's name to sanitize input +# for file-level access as we might need to wipe out keytab in /var/lib/sss/keytabs +own_trust_dn = DN(('cn', api.env.domain),('cn','ad'), ('cn', 'etc'), api.env.basedn) +own_trust_entry = api.Backend.ldap2.get_entry(own_trust_dn, ['ipantflatname']) +own_trust_flatname = own_trust_entry.single_value.get('ipantflatname').upper() +trusted_domain_dn = DN(('cn', trusted_domain.lower()), api.env.container_adtrusts, api.env.basedn) +trusted_domain_entry = api.Backend.ldap2.get_entry(trusted_domain_dn, ['cn']) +trusted_domain = trusted_domain_entry.single_value.get('cn').lower() + +# At this point if we didn't find trusted forest name, an exception will be raised +# and script will quit. This is actually intended. + +oneway_keytab_name = '/var/lib/sss/keytabs/' + trusted_domain + '.keytab' +oneway_principal = str('%s$@%s' % (own_trust_flatname, trusted_domain.upper())) + +# If keytab does not exist, retrieve it +if not os.path.isfile(oneway_keytab_name): + retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal) + +try: + have_ccache = False + try: + # The keytab may have stale key material (from older trust-add run) + cred = kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) + if cred.lifetime > 0: + have_ccache = True + except gssapi.exceptions.ExpiredCredentialsError: + pass + if not have_ccache: + if os.path.exists(oneway_ccache_name): + os.unlink(oneway_ccache_name) + kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) +except gssapi.exceptions.GSSError: + # If there was failure on using keytab, assume it is stale and retrieve again + retrieve_keytab(api, ccache_name, oneway_keytab_name, oneway_principal) + if os.path.exists(oneway_ccache_name): + os.unlink(oneway_ccache_name) + kinit_keytab(oneway_principal, oneway_keytab_name, oneway_ccache_name) + +# We are done: we have ccache with TDO credentials and can fetch domains +ipa_domain = api.env.domain +os.environ['KRB5CCNAME'] = oneway_ccache_name + +# retrieve the forest root domain name and contact it to retrieve trust +# topology info +forest_root = get_forest_root_domain(api, trusted_domain) + +domains = dcerpc.fetch_domains(api, ipa_domain, forest_root, creds=True) +trust_domain_object = api.Command.trust_show(trusted_domain, raw=True)['result'] +trust.add_new_domains_from_trust(api, None, trust_domain_object, domains) + +if old_ccache: + os.environ['KRB5CCNAME'] = old_ccache + +sys.exit(0) diff --git a/install/restart_scripts/renew_ca_cert b/install/restart_scripts/renew_ca_cert deleted file mode 100755 index 83e79a2..0000000 --- a/install/restart_scripts/renew_ca_cert +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Rob Crittenden -# Jan Cholasta -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import os -import syslog -import tempfile -import shutil -import traceback - -from ipalib.install import certstore -from ipapython import directivesetter -from ipapython import ipautil -from ipalib import api, errors -from ipalib import x509 -from ipalib.install.kinit import kinit_keytab -from ipaserver.install import certs, cainstance -from ipaserver.plugins.ldap2 import ldap2 -from ipaplatform import services -from ipaplatform.paths import paths -from ipapython.certdb import TrustFlags - - -def _main(): - nickname = sys.argv[1] - - api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) - api.finalize() - - dogtag_service = services.knownservices['pki_tomcatd'] - - # dogtag opens its NSS database in read/write mode so we need it - # shut down so certmonger can open it read/write mode. This avoids - # database corruption. It should already be stopped by the pre-command - # but lets be sure. - if dogtag_service.is_running('pki-tomcat'): - syslog.syslog( - syslog.LOG_NOTICE, "Stopping %s" % dogtag_service.service_name) - try: - dogtag_service.stop('pki-tomcat') - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, - "Cannot stop %s: %s" % (dogtag_service.service_name, e)) - else: - syslog.syslog( - syslog.LOG_NOTICE, "Stopped %s" % dogtag_service.service_name) - - # Fetch the new certificate - db = certs.CertDB(api.env.realm, nssdir=paths.PKI_TOMCAT_ALIAS_DIR) - cert = db.get_cert_from_db(nickname) - if not cert: - syslog.syslog(syslog.LOG_ERR, 'No certificate %s found.' % nickname) - sys.exit(1) - - tmpdir = tempfile.mkdtemp(prefix="tmp-") - try: - principal = str('host/%s@%s' % (api.env.host, api.env.realm)) - ccache_filename = os.path.join(tmpdir, 'ccache') - kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_filename) - os.environ['KRB5CCNAME'] = ccache_filename - - api.Backend.ldap2.connect() - - ca = cainstance.CAInstance(host_name=api.env.host) - ca.update_cert_config(nickname, cert) - if ca.is_renewal_master(): - cainstance.update_people_entry(cert) - cainstance.update_authority_entry(cert) - - if nickname == 'auditSigningCert cert-pki-ca': - # Fix trust on the audit cert - try: - db.run_certutil(['-M', - '-n', nickname, - '-t', 'u,u,Pu']) - syslog.syslog( - syslog.LOG_NOTICE, - "Updated trust on certificate %s in %s" % - (nickname, db.secdir)) - except ipautil.CalledProcessError: - syslog.syslog( - syslog.LOG_ERR, - "Updating trust on certificate %s failed in %s" % - (nickname, db.secdir)) - elif nickname == 'caSigningCert cert-pki-ca': - # Update CS.cfg - cfg_path = paths.CA_CS_CFG_PATH - config = directivesetter.get_directive( - cfg_path, 'subsystem.select', '=') - if config == 'New': - syslog.syslog(syslog.LOG_NOTICE, "Updating CS.cfg") - if cert.is_self_signed(): - directivesetter.set_directive( - cfg_path, 'hierarchy.select', 'Root', - quotes=False, separator='=') - directivesetter.set_directive( - cfg_path, 'subsystem.count', '1', - quotes=False, separator='=') - else: - directivesetter.set_directive( - cfg_path, 'hierarchy.select', 'Subordinate', - quotes=False, separator='=') - directivesetter.set_directive( - cfg_path, 'subsystem.count', '0', - quotes=False, separator='=') - else: - syslog.syslog(syslog.LOG_NOTICE, "Not updating CS.cfg") - - # Remove old external CA certificates - for ca_nick, ca_flags in db.list_certs(): - if ca_flags.has_key: - continue - # Delete *all* certificates that use the nickname - while True: - try: - db.delete_cert(ca_nick) - except ipautil.CalledProcessError: - syslog.syslog( - syslog.LOG_ERR, - "Failed to remove certificate %s" % ca_nick) - break - if not db.has_nickname(ca_nick): - break - - conn = None - try: - conn = ldap2(api) - conn.connect(ccache=ccache_filename) - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, "Failed to connect to LDAP: %s" % e) - else: - # Update CA certificate in LDAP - if ca.is_renewal_master(): - try: - certstore.update_ca_cert(conn, api.env.basedn, cert) - except errors.EmptyModlist: - pass - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, - "Updating CA certificate failed: %s" % e) - - # Add external CA certificates - try: - ca_certs = certstore.get_ca_certs_nss( - conn, api.env.basedn, api.env.realm, False) - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, - "Failed to get external CA certificates from LDAP: " - "%s" % e) - ca_certs = [] - - for ca_cert, ca_nick, ca_flags in ca_certs: - try: - db.add_cert(ca_cert, ca_nick, ca_flags) - except ipautil.CalledProcessError as e: - syslog.syslog( - syslog.LOG_ERR, - "Failed to add certificate %s" % ca_nick) - - # Pass Dogtag's self-tests - for ca_nick in db.find_root_cert(nickname)[-2:-1]: - ca_flags = dict(cc[1:] for cc in ca_certs)[ca_nick] - usages = ca_flags.usages or set() - ca_flags_modified = TrustFlags(ca_flags.has_key, - True, True, - usages | {x509.EKU_SERVER_AUTH}) - db.trust_root_cert(ca_nick, ca_flags_modified) - finally: - if conn is not None and conn.isconnected(): - conn.disconnect() - finally: - if api.Backend.ldap2.isconnected(): - api.Backend.ldap2.disconnect() - shutil.rmtree(tmpdir) - - # Now we can start the CA. Using the services start should fire - # off the servlet to verify that the CA is actually up and responding so - # when this returns it should be good-to-go. The CA was stopped in the - # pre-save state. - syslog.syslog( - syslog.LOG_NOTICE, - 'Starting %s' % dogtag_service.service_name) - try: - dogtag_service.start('pki-tomcat') - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, - "Cannot start %s: %s" % (dogtag_service.service_name, e)) - else: - syslog.syslog( - syslog.LOG_NOTICE, "Started %s" % dogtag_service.service_name) - - -def main(): - try: - _main() - finally: - certs.renewal_lock.release('renew_ca_cert') - - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_ca_cert.in b/install/restart_scripts/renew_ca_cert.in new file mode 100644 index 0000000..0bb0482 --- /dev/null +++ b/install/restart_scripts/renew_ca_cert.in @@ -0,0 +1,227 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Rob Crittenden +# Jan Cholasta +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import syslog +import tempfile +import shutil +import traceback + +from ipalib.install import certstore +from ipapython import directivesetter +from ipapython import ipautil +from ipalib import api, errors +from ipalib import x509 +from ipalib.install.kinit import kinit_keytab +from ipaserver.install import certs, cainstance +from ipaserver.plugins.ldap2 import ldap2 +from ipaplatform import services +from ipaplatform.paths import paths +from ipapython.certdb import TrustFlags + + +def _main(): + nickname = sys.argv[1] + + api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) + api.finalize() + + dogtag_service = services.knownservices['pki_tomcatd'] + + # dogtag opens its NSS database in read/write mode so we need it + # shut down so certmonger can open it read/write mode. This avoids + # database corruption. It should already be stopped by the pre-command + # but lets be sure. + if dogtag_service.is_running('pki-tomcat'): + syslog.syslog( + syslog.LOG_NOTICE, "Stopping %s" % dogtag_service.service_name) + try: + dogtag_service.stop('pki-tomcat') + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, + "Cannot stop %s: %s" % (dogtag_service.service_name, e)) + else: + syslog.syslog( + syslog.LOG_NOTICE, "Stopped %s" % dogtag_service.service_name) + + # Fetch the new certificate + db = certs.CertDB(api.env.realm, nssdir=paths.PKI_TOMCAT_ALIAS_DIR) + cert = db.get_cert_from_db(nickname) + if not cert: + syslog.syslog(syslog.LOG_ERR, 'No certificate %s found.' % nickname) + sys.exit(1) + + tmpdir = tempfile.mkdtemp(prefix="tmp-") + try: + principal = str('host/%s@%s' % (api.env.host, api.env.realm)) + ccache_filename = os.path.join(tmpdir, 'ccache') + kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_filename) + os.environ['KRB5CCNAME'] = ccache_filename + + api.Backend.ldap2.connect() + + ca = cainstance.CAInstance(host_name=api.env.host) + ca.update_cert_config(nickname, cert) + if ca.is_renewal_master(): + cainstance.update_people_entry(cert) + cainstance.update_authority_entry(cert) + + if nickname == 'auditSigningCert cert-pki-ca': + # Fix trust on the audit cert + try: + db.run_certutil(['-M', + '-n', nickname, + '-t', 'u,u,Pu']) + syslog.syslog( + syslog.LOG_NOTICE, + "Updated trust on certificate %s in %s" % + (nickname, db.secdir)) + except ipautil.CalledProcessError: + syslog.syslog( + syslog.LOG_ERR, + "Updating trust on certificate %s failed in %s" % + (nickname, db.secdir)) + elif nickname == 'caSigningCert cert-pki-ca': + # Update CS.cfg + cfg_path = paths.CA_CS_CFG_PATH + config = directivesetter.get_directive( + cfg_path, 'subsystem.select', '=') + if config == 'New': + syslog.syslog(syslog.LOG_NOTICE, "Updating CS.cfg") + if cert.is_self_signed(): + directivesetter.set_directive( + cfg_path, 'hierarchy.select', 'Root', + quotes=False, separator='=') + directivesetter.set_directive( + cfg_path, 'subsystem.count', '1', + quotes=False, separator='=') + else: + directivesetter.set_directive( + cfg_path, 'hierarchy.select', 'Subordinate', + quotes=False, separator='=') + directivesetter.set_directive( + cfg_path, 'subsystem.count', '0', + quotes=False, separator='=') + else: + syslog.syslog(syslog.LOG_NOTICE, "Not updating CS.cfg") + + # Remove old external CA certificates + for ca_nick, ca_flags in db.list_certs(): + if ca_flags.has_key: + continue + # Delete *all* certificates that use the nickname + while True: + try: + db.delete_cert(ca_nick) + except ipautil.CalledProcessError: + syslog.syslog( + syslog.LOG_ERR, + "Failed to remove certificate %s" % ca_nick) + break + if not db.has_nickname(ca_nick): + break + + conn = None + try: + conn = ldap2(api) + conn.connect(ccache=ccache_filename) + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, "Failed to connect to LDAP: %s" % e) + else: + # Update CA certificate in LDAP + if ca.is_renewal_master(): + try: + certstore.update_ca_cert(conn, api.env.basedn, cert) + except errors.EmptyModlist: + pass + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, + "Updating CA certificate failed: %s" % e) + + # Add external CA certificates + try: + ca_certs = certstore.get_ca_certs_nss( + conn, api.env.basedn, api.env.realm, False) + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, + "Failed to get external CA certificates from LDAP: " + "%s" % e) + ca_certs = [] + + for ca_cert, ca_nick, ca_flags in ca_certs: + try: + db.add_cert(ca_cert, ca_nick, ca_flags) + except ipautil.CalledProcessError as e: + syslog.syslog( + syslog.LOG_ERR, + "Failed to add certificate %s" % ca_nick) + + # Pass Dogtag's self-tests + for ca_nick in db.find_root_cert(nickname)[-2:-1]: + ca_flags = dict(cc[1:] for cc in ca_certs)[ca_nick] + usages = ca_flags.usages or set() + ca_flags_modified = TrustFlags(ca_flags.has_key, + True, True, + usages | {x509.EKU_SERVER_AUTH}) + db.trust_root_cert(ca_nick, ca_flags_modified) + finally: + if conn is not None and conn.isconnected(): + conn.disconnect() + finally: + if api.Backend.ldap2.isconnected(): + api.Backend.ldap2.disconnect() + shutil.rmtree(tmpdir) + + # Now we can start the CA. Using the services start should fire + # off the servlet to verify that the CA is actually up and responding so + # when this returns it should be good-to-go. The CA was stopped in the + # pre-save state. + syslog.syslog( + syslog.LOG_NOTICE, + 'Starting %s' % dogtag_service.service_name) + try: + dogtag_service.start('pki-tomcat') + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, + "Cannot start %s: %s" % (dogtag_service.service_name, e)) + else: + syslog.syslog( + syslog.LOG_NOTICE, "Started %s" % dogtag_service.service_name) + + +def main(): + try: + _main() + finally: + certs.renewal_lock.release('renew_ca_cert') + + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_kdc_cert b/install/restart_scripts/renew_kdc_cert deleted file mode 100755 index 48935b2..0000000 --- a/install/restart_scripts/renew_kdc_cert +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python3 -E -# -# Copyright (C) 2017 FreeIPA Contributors see COPYING for license -# - -import syslog -import traceback - -from ipaplatform import services -from ipaserver.install import certs - - -def main(): - with certs.renewal_lock: - try: - if services.knownservices.krb5kdc.is_running(): - syslog.syslog(syslog.LOG_NOTICE, 'restarting krb5kdc') - services.knownservices.krb5kdc.restart() - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, "cannot restart krb5kdc: {}".format(e)) - - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_kdc_cert.in b/install/restart_scripts/renew_kdc_cert.in new file mode 100644 index 0000000..eeeaf8a --- /dev/null +++ b/install/restart_scripts/renew_kdc_cert.in @@ -0,0 +1,27 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# + +import syslog +import traceback + +from ipaplatform import services +from ipaserver.install import certs + + +def main(): + with certs.renewal_lock: + try: + if services.knownservices.krb5kdc.is_running(): + syslog.syslog(syslog.LOG_NOTICE, 'restarting krb5kdc') + services.knownservices.krb5kdc.restart() + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, "cannot restart krb5kdc: {}".format(e)) + + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_ra_cert b/install/restart_scripts/renew_ra_cert deleted file mode 100755 index 2ea3be3..0000000 --- a/install/restart_scripts/renew_ra_cert +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Rob Crittenden -# Jan Cholasta -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import os -import syslog -import tempfile -import shutil -import traceback - -from ipalib.install.kinit import kinit_keytab -from ipalib import api, x509 -from ipaserver.install import certs, cainstance -from ipaplatform.paths import paths - - -def _main(): - api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) - api.finalize() - - tmpdir = tempfile.mkdtemp(prefix="tmp-") - try: - principal = str('host/%s@%s' % (api.env.host, api.env.realm)) - ccache_filename = os.path.join(tmpdir, 'ccache') - kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_filename) - os.environ['KRB5CCNAME'] = ccache_filename - - api.Backend.ldap2.connect() - - ca = cainstance.CAInstance(host_name=api.env.host) - ra_certpath = paths.RA_AGENT_PEM - if ca.is_renewal_master(): - # Fetch the new certificate - try: - cert = x509.load_certificate_from_file(ra_certpath) - except IOError as e: - syslog.syslog( - syslog.LOG_ERR, "Can't open '{certpath}': {err}" - .format(certpath=ra_certpath, err=e) - ) - sys.exit(1) - except (TypeError, ValueError): - syslog.syslog( - syslog.LOG_ERR, "'{certpath}' is not a valid certificate " - "file".format(certpath=ra_certpath) - ) - sys.exit(1) - - # Load it into dogtag - cainstance.update_people_entry(cert) - finally: - if api.Backend.ldap2.isconnected(): - api.Backend.ldap2.disconnect() - shutil.rmtree(tmpdir) - - -def main(): - try: - _main() - finally: - # lock acquired in renew_ra_cert_pre - certs.renewal_lock.release('renew_ra_cert') - - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_ra_cert.in b/install/restart_scripts/renew_ra_cert.in new file mode 100644 index 0000000..ed06f62 --- /dev/null +++ b/install/restart_scripts/renew_ra_cert.in @@ -0,0 +1,87 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Rob Crittenden +# Jan Cholasta +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import syslog +import tempfile +import shutil +import traceback + +from ipalib.install.kinit import kinit_keytab +from ipalib import api, x509 +from ipaserver.install import certs, cainstance +from ipaplatform.paths import paths + + +def _main(): + api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) + api.finalize() + + tmpdir = tempfile.mkdtemp(prefix="tmp-") + try: + principal = str('host/%s@%s' % (api.env.host, api.env.realm)) + ccache_filename = os.path.join(tmpdir, 'ccache') + kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_filename) + os.environ['KRB5CCNAME'] = ccache_filename + + api.Backend.ldap2.connect() + + ca = cainstance.CAInstance(host_name=api.env.host) + ra_certpath = paths.RA_AGENT_PEM + if ca.is_renewal_master(): + # Fetch the new certificate + try: + cert = x509.load_certificate_from_file(ra_certpath) + except IOError as e: + syslog.syslog( + syslog.LOG_ERR, "Can't open '{certpath}': {err}" + .format(certpath=ra_certpath, err=e) + ) + sys.exit(1) + except (TypeError, ValueError): + syslog.syslog( + syslog.LOG_ERR, "'{certpath}' is not a valid certificate " + "file".format(certpath=ra_certpath) + ) + sys.exit(1) + + # Load it into dogtag + cainstance.update_people_entry(cert) + finally: + if api.Backend.ldap2.isconnected(): + api.Backend.ldap2.disconnect() + shutil.rmtree(tmpdir) + + +def main(): + try: + _main() + finally: + # lock acquired in renew_ra_cert_pre + certs.renewal_lock.release('renew_ra_cert') + + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_ra_cert_pre b/install/restart_scripts/renew_ra_cert_pre deleted file mode 100755 index 151d98a..0000000 --- a/install/restart_scripts/renew_ra_cert_pre +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python3 -E -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -import syslog -import traceback - -from ipaserver.install import certs - - -def main(): - certs.renewal_lock.acquire('renew_ra_cert') - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/renew_ra_cert_pre.in b/install/restart_scripts/renew_ra_cert_pre.in new file mode 100644 index 0000000..6cfe144 --- /dev/null +++ b/install/restart_scripts/renew_ra_cert_pre.in @@ -0,0 +1,18 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +import syslog +import traceback + +from ipaserver.install import certs + + +def main(): + certs.renewal_lock.acquire('renew_ra_cert') + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/restart_dirsrv b/install/restart_scripts/restart_dirsrv deleted file mode 100755 index 01fca48..0000000 --- a/install/restart_scripts/restart_dirsrv +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Rob Crittenden -# -# Copyright (C) 2012 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -import syslog -import traceback -from ipalib import api -from ipaplatform import services -from ipaplatform.paths import paths -from ipaserver.install import certs - - -def _main(): - try: - instance = sys.argv[1] - except IndexError: - instance = "" - - api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) - api.finalize() - - syslog.syslog(syslog.LOG_NOTICE, "certmonger restarted dirsrv instance '%s'" % instance) - - try: - if services.knownservices.dirsrv.is_running(): - services.knownservices.dirsrv.restart(instance, ldapi=True) - except Exception as e: - syslog.syslog(syslog.LOG_ERR, "Cannot restart dirsrv (instance: '%s'): %s" % (instance, str(e))) - - -def main(): - with certs.renewal_lock: - _main() - - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/restart_dirsrv.in b/install/restart_scripts/restart_dirsrv.in new file mode 100644 index 0000000..55a056a --- /dev/null +++ b/install/restart_scripts/restart_dirsrv.in @@ -0,0 +1,57 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Rob Crittenden +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import syslog +import traceback +from ipalib import api +from ipaplatform import services +from ipaplatform.paths import paths +from ipaserver.install import certs + + +def _main(): + try: + instance = sys.argv[1] + except IndexError: + instance = "" + + api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) + api.finalize() + + syslog.syslog(syslog.LOG_NOTICE, "certmonger restarted dirsrv instance '%s'" % instance) + + try: + if services.knownservices.dirsrv.is_running(): + services.knownservices.dirsrv.restart(instance, ldapi=True) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, "Cannot restart dirsrv (instance: '%s'): %s" % (instance, str(e))) + + +def main(): + with certs.renewal_lock: + _main() + + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/restart_httpd b/install/restart_scripts/restart_httpd deleted file mode 100755 index 9b342bf..0000000 --- a/install/restart_scripts/restart_httpd +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Rob Crittenden -# -# Copyright (C) 2012 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import syslog -import traceback -from ipaplatform import services -from ipaserver.install import certs - - -def _main(): - syslog.syslog(syslog.LOG_NOTICE, 'certmonger restarted httpd') - - try: - if services.knownservices.httpd.is_running(): - services.knownservices.httpd.restart() - except Exception as e: - syslog.syslog(syslog.LOG_ERR, "Cannot restart httpd: %s" % str(e)) - - -def main(): - with certs.renewal_lock: - _main() - - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/restart_httpd.in b/install/restart_scripts/restart_httpd.in new file mode 100644 index 0000000..47cc276 --- /dev/null +++ b/install/restart_scripts/restart_httpd.in @@ -0,0 +1,46 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Rob Crittenden +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import syslog +import traceback +from ipaplatform import services +from ipaserver.install import certs + + +def _main(): + syslog.syslog(syslog.LOG_NOTICE, 'certmonger restarted httpd') + + try: + if services.knownservices.httpd.is_running(): + services.knownservices.httpd.restart() + except Exception as e: + syslog.syslog(syslog.LOG_ERR, "Cannot restart httpd: %s" % str(e)) + + +def main(): + with certs.renewal_lock: + _main() + + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/stop_pkicad b/install/restart_scripts/stop_pkicad deleted file mode 100755 index f245a27..0000000 --- a/install/restart_scripts/stop_pkicad +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python3 -E -# -# Authors: -# Rob Crittenden -# -# Copyright (C) 2012 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import syslog -import traceback -from ipalib import api -from ipaplatform import services -from ipaplatform.paths import paths -from ipaserver.install import certs - - -def main(): - api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) - api.finalize() - - dogtag_service = services.knownservices['pki_tomcatd'] - - certs.renewal_lock.acquire('renew_ca_cert') - - syslog.syslog(syslog.LOG_NOTICE, "Stopping %s" % dogtag_service.service_name) - try: - dogtag_service.stop('pki-tomcat') - except Exception as e: - syslog.syslog( - syslog.LOG_ERR, "Cannot stop %s: %s" % (dogtag_service.service_name, e)) - else: - syslog.syslog( - syslog.LOG_NOTICE, "Stopped %s" % dogtag_service.service_name) - -try: - main() -except Exception: - syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/restart_scripts/stop_pkicad.in b/install/restart_scripts/stop_pkicad.in new file mode 100644 index 0000000..d495afc --- /dev/null +++ b/install/restart_scripts/stop_pkicad.in @@ -0,0 +1,51 @@ +@PYTHONSHEBANG@ +# +# Authors: +# Rob Crittenden +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import syslog +import traceback +from ipalib import api +from ipaplatform import services +from ipaplatform.paths import paths +from ipaserver.install import certs + + +def main(): + api.bootstrap(in_server=True, context='restart', confdir=paths.ETC_IPA) + api.finalize() + + dogtag_service = services.knownservices['pki_tomcatd'] + + certs.renewal_lock.acquire('renew_ca_cert') + + syslog.syslog(syslog.LOG_NOTICE, "Stopping %s" % dogtag_service.service_name) + try: + dogtag_service.stop('pki-tomcat') + except Exception as e: + syslog.syslog( + syslog.LOG_ERR, "Cannot stop %s: %s" % (dogtag_service.service_name, e)) + else: + syslog.syslog( + syslog.LOG_NOTICE, "Stopped %s" % dogtag_service.service_name) + +try: + main() +except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc()) diff --git a/install/tools/ipa-adtrust-install b/install/tools/ipa-adtrust-install deleted file mode 100755 index 077eda4..0000000 --- a/install/tools/ipa-adtrust-install +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/python3 -# -# Authors: Sumit Bose -# Based on ipa-server-install by Karl MacMillan -# and ipa-dns-install by Martin Nagy -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging -import os -import sys - -import six - -from optparse import SUPPRESS_HELP # pylint: disable=deprecated-module - -from ipalib.install import sysrestore -from ipaserver.install import adtrust, service -from ipaserver.install.installutils import ( - read_password, - check_server_configuration, - run_script) -from ipapython.admintool import ScriptError -from ipapython import version -from ipapython import ipautil -from ipalib import api, errors, krb_utils -from ipapython.config import IPAOptionParser -from ipaplatform.paths import paths -from ipapython.ipa_log_manager import standard_logging_setup - -if six.PY3: - unicode = str - -logger = logging.getLogger(os.path.basename(__file__)) - -log_file_name = paths.IPASERVER_INSTALL_LOG - - -def parse_options(): - parser = IPAOptionParser(version=version.VERSION) - parser.add_option("-d", "--debug", dest="debug", action="store_true", - default=False, help="print debugging information") - parser.add_option("--netbios-name", dest="netbios_name", - help="NetBIOS name of the IPA domain") - - # no-msdcs has not effect, option is here just for backward compatibility - parser.add_option("--no-msdcs", dest="no_msdcs", action="store_true", - default=False, help=SUPPRESS_HELP) - - parser.add_option("--rid-base", dest="rid_base", type=int, default=1000, - help="Start value for mapping UIDs and GIDs to RIDs") - parser.add_option("--secondary-rid-base", dest="secondary_rid_base", - type=int, default=100000000, - help="Start value of the secondary range for mapping " - "UIDs and GIDs to RIDs") - parser.add_option("-U", "--unattended", dest="unattended", - action="store_true", - default=False, - help="unattended installation never prompts the user") - parser.add_option("-a", "--admin-password", - sensitive=True, dest="admin_password", - help="admin user kerberos password") - parser.add_option("-A", "--admin-name", - sensitive=True, dest="admin_name", default='admin', - help="admin user principal") - parser.add_option("--add-sids", dest="add_sids", action="store_true", - default=False, help="Add SIDs for existing users and" - " groups as the final step") - parser.add_option("--add-agents", dest="add_agents", action="store_true", - default=False, - help="Add IPA masters to a list of hosts allowed to " - "serve information about users from trusted forests") - parser.add_option("--enable-compat", - dest="enable_compat", default=False, action="store_true", - help="Enable support for trusted domains for old " - "clients") - - options, _args = parser.parse_args() - safe_options = parser.get_safe_opts(options) - - return safe_options, options - - -def read_admin_password(admin_name): - print("Configuring cross-realm trusts for IPA server requires password " - "for user '%s'." % (admin_name)) - print("This user is a regular system account used for IPA server " - "administration.") - print("") - admin_password = read_password(admin_name, confirm=False, validate=None) - return admin_password - - -def ensure_admin_kinit(admin_name, admin_password): - try: - ipautil.run([paths.KINIT, admin_name], stdin=admin_password+'\n') - except ipautil.CalledProcessError: - print("There was error to automatically re-kinit your admin user " - "ticket.") - return False - return True - - -def main(): - safe_options, options = parse_options() - - if os.getegid() != 0: - raise ScriptError("Must be root to setup AD trusts on server") - - standard_logging_setup(log_file_name, debug=options.debug, filemode='a') - print("\nThe log file for this installation can be found in %s" - % log_file_name) - - logger.debug('%s was invoked with options: %s', sys.argv[0], safe_options) - logger.debug( - "missing options might be asked for interactively later\n") - logger.debug('IPA version %s', version.VENDOR_VERSION) - - check_server_configuration() - - fstore = sysrestore.FileStore(paths.SYSRESTORE) - - print("================================================================" - "==============") - print("This program will setup components needed to establish trust to " - "AD domains for") - print("the FreeIPA Server.") - print("") - print("This includes:") - print(" * Configure Samba") - print(" * Add trust related objects to FreeIPA LDAP server") - # TODO: - # print " * Add a SID to all users and Posix groups" - print("") - print("To accept the default shown in brackets, press the Enter key.") - print("") - - # Check if samba packages are installed - # the same check is in the adtrust module but we must fail first if the - # package is missing - adtrust.check_for_installed_deps() - - # Initialize the ipalib api - api.bootstrap( - in_server=True, - debug=options.debug, - context='install', - confdir=paths.ETC_IPA - ) - api.finalize() - - admin_password = options.admin_password - if not (options.unattended or admin_password): - admin_password = read_admin_password(options.admin_name) - - admin_kinited = None - if admin_password: - admin_kinited = ensure_admin_kinit(options.admin_name, admin_password) - if not admin_kinited: - print("Proceeding with credentials that existed before") - - try: - principal = krb_utils.get_principal() - except errors.CCacheError as e: - raise ScriptError( - "Must have Kerberos credentials to setup AD trusts on server: " - "{err}".format(err=e)) - - try: - api.Backend.ldap2.connect() - except errors.ACIError: - raise ScriptError( - "Outdated Kerberos credentials. " - "Use kdestroy and kinit to update your ticket") - except errors.DatabaseError: - raise ScriptError( - "Cannot connect to the LDAP database. Please check if IPA " - "is running") - - try: - user = api.Command.user_show( - principal.partition('@')[0].partition('/')[0])['result'] - group = api.Command.group_show(u'admins')['result'] - if not (user['uid'][0] in group['member_user'] and - group['cn'][0] in user['memberof_group']): - raise errors.RequirementError(name='admins group membership') - except errors.RequirementError as e: - raise ScriptError( - "Must have administrative privileges to setup AD trusts on server" - ) - except Exception as e: - raise ScriptError( - "Unrecognized error during check of admin rights: %s" % e) - - adtrust.install_check(True, options, api) - adtrust.install(True, options, fstore, api) - - # Enable configured services and update DNS SRV records - service.enable_services(api.env.host) - api.Command.dns_update_system_records() - - print(""" -============================================================================= -Setup complete - -You must make sure these network ports are open: -\tTCP Ports: -\t * 135: epmap -\t * 138: netbios-dgm -\t * 139: netbios-ssn -\t * 445: microsoft-ds -\t * 1024..1300: epmap listener range -\t * 3268: msft-gc -\tUDP Ports: -\t * 138: netbios-dgm -\t * 139: netbios-ssn -\t * 389: (C)LDAP -\t * 445: microsoft-ds - -See the ipa-adtrust-install(1) man page for more details - -============================================================================= -""") - if admin_password: - admin_kinited = ensure_admin_kinit(options.admin_name, admin_password) - - if not admin_kinited: - print(""" -WARNING: you MUST re-kinit admin user before using 'ipa trust-*' commands -family in order to re-generate Kerberos tickets to include AD-specific -information""") - - api.Backend.ldap2.disconnect() - - return 0 - -if __name__ == '__main__': - run_script( - main, - log_file_name=log_file_name, - operation_name='ipa-adtrust-install') diff --git a/install/tools/ipa-adtrust-install.in b/install/tools/ipa-adtrust-install.in new file mode 100644 index 0000000..cb0b1a1 --- /dev/null +++ b/install/tools/ipa-adtrust-install.in @@ -0,0 +1,258 @@ +@PYTHONSHEBANG@ +# +# Authors: Sumit Bose +# Based on ipa-server-install by Karl MacMillan +# and ipa-dns-install by Martin Nagy +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging +import os +import sys + +import six + +from optparse import SUPPRESS_HELP # pylint: disable=deprecated-module + +from ipalib.install import sysrestore +from ipaserver.install import adtrust, service +from ipaserver.install.installutils import ( + read_password, + check_server_configuration, + run_script) +from ipapython.admintool import ScriptError +from ipapython import version +from ipapython import ipautil +from ipalib import api, errors, krb_utils +from ipapython.config import IPAOptionParser +from ipaplatform.paths import paths +from ipapython.ipa_log_manager import standard_logging_setup + +if six.PY3: + unicode = str + +logger = logging.getLogger(os.path.basename(__file__)) + +log_file_name = paths.IPASERVER_INSTALL_LOG + + +def parse_options(): + parser = IPAOptionParser(version=version.VERSION) + parser.add_option("-d", "--debug", dest="debug", action="store_true", + default=False, help="print debugging information") + parser.add_option("--netbios-name", dest="netbios_name", + help="NetBIOS name of the IPA domain") + + # no-msdcs has not effect, option is here just for backward compatibility + parser.add_option("--no-msdcs", dest="no_msdcs", action="store_true", + default=False, help=SUPPRESS_HELP) + + parser.add_option("--rid-base", dest="rid_base", type=int, default=1000, + help="Start value for mapping UIDs and GIDs to RIDs") + parser.add_option("--secondary-rid-base", dest="secondary_rid_base", + type=int, default=100000000, + help="Start value of the secondary range for mapping " + "UIDs and GIDs to RIDs") + parser.add_option("-U", "--unattended", dest="unattended", + action="store_true", + default=False, + help="unattended installation never prompts the user") + parser.add_option("-a", "--admin-password", + sensitive=True, dest="admin_password", + help="admin user kerberos password") + parser.add_option("-A", "--admin-name", + sensitive=True, dest="admin_name", default='admin', + help="admin user principal") + parser.add_option("--add-sids", dest="add_sids", action="store_true", + default=False, help="Add SIDs for existing users and" + " groups as the final step") + parser.add_option("--add-agents", dest="add_agents", action="store_true", + default=False, + help="Add IPA masters to a list of hosts allowed to " + "serve information about users from trusted forests") + parser.add_option("--enable-compat", + dest="enable_compat", default=False, action="store_true", + help="Enable support for trusted domains for old " + "clients") + + options, _args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + return safe_options, options + + +def read_admin_password(admin_name): + print("Configuring cross-realm trusts for IPA server requires password " + "for user '%s'." % (admin_name)) + print("This user is a regular system account used for IPA server " + "administration.") + print("") + admin_password = read_password(admin_name, confirm=False, validate=None) + return admin_password + + +def ensure_admin_kinit(admin_name, admin_password): + try: + ipautil.run([paths.KINIT, admin_name], stdin=admin_password+'\n') + except ipautil.CalledProcessError: + print("There was error to automatically re-kinit your admin user " + "ticket.") + return False + return True + + +def main(): + safe_options, options = parse_options() + + if os.getegid() != 0: + raise ScriptError("Must be root to setup AD trusts on server") + + standard_logging_setup(log_file_name, debug=options.debug, filemode='a') + print("\nThe log file for this installation can be found in %s" + % log_file_name) + + logger.debug('%s was invoked with options: %s', sys.argv[0], safe_options) + logger.debug( + "missing options might be asked for interactively later\n") + logger.debug('IPA version %s', version.VENDOR_VERSION) + + check_server_configuration() + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + print("================================================================" + "==============") + print("This program will setup components needed to establish trust to " + "AD domains for") + print("the FreeIPA Server.") + print("") + print("This includes:") + print(" * Configure Samba") + print(" * Add trust related objects to FreeIPA LDAP server") + # TODO: + # print " * Add a SID to all users and Posix groups" + print("") + print("To accept the default shown in brackets, press the Enter key.") + print("") + + # Check if samba packages are installed + # the same check is in the adtrust module but we must fail first if the + # package is missing + adtrust.check_for_installed_deps() + + # Initialize the ipalib api + api.bootstrap( + in_server=True, + debug=options.debug, + context='install', + confdir=paths.ETC_IPA + ) + api.finalize() + + admin_password = options.admin_password + if not (options.unattended or admin_password): + admin_password = read_admin_password(options.admin_name) + + admin_kinited = None + if admin_password: + admin_kinited = ensure_admin_kinit(options.admin_name, admin_password) + if not admin_kinited: + print("Proceeding with credentials that existed before") + + try: + principal = krb_utils.get_principal() + except errors.CCacheError as e: + raise ScriptError( + "Must have Kerberos credentials to setup AD trusts on server: " + "{err}".format(err=e)) + + try: + api.Backend.ldap2.connect() + except errors.ACIError: + raise ScriptError( + "Outdated Kerberos credentials. " + "Use kdestroy and kinit to update your ticket") + except errors.DatabaseError: + raise ScriptError( + "Cannot connect to the LDAP database. Please check if IPA " + "is running") + + try: + user = api.Command.user_show( + principal.partition('@')[0].partition('/')[0])['result'] + group = api.Command.group_show(u'admins')['result'] + if not (user['uid'][0] in group['member_user'] and + group['cn'][0] in user['memberof_group']): + raise errors.RequirementError(name='admins group membership') + except errors.RequirementError as e: + raise ScriptError( + "Must have administrative privileges to setup AD trusts on server" + ) + except Exception as e: + raise ScriptError( + "Unrecognized error during check of admin rights: %s" % e) + + adtrust.install_check(True, options, api) + adtrust.install(True, options, fstore, api) + + # Enable configured services and update DNS SRV records + service.enable_services(api.env.host) + api.Command.dns_update_system_records() + + print(""" +============================================================================= +Setup complete + +You must make sure these network ports are open: +\tTCP Ports: +\t * 135: epmap +\t * 138: netbios-dgm +\t * 139: netbios-ssn +\t * 445: microsoft-ds +\t * 1024..1300: epmap listener range +\t * 3268: msft-gc +\tUDP Ports: +\t * 138: netbios-dgm +\t * 139: netbios-ssn +\t * 389: (C)LDAP +\t * 445: microsoft-ds + +See the ipa-adtrust-install(1) man page for more details + +============================================================================= +""") + if admin_password: + admin_kinited = ensure_admin_kinit(options.admin_name, admin_password) + + if not admin_kinited: + print(""" +WARNING: you MUST re-kinit admin user before using 'ipa trust-*' commands +family in order to re-generate Kerberos tickets to include AD-specific +information""") + + api.Backend.ldap2.disconnect() + + return 0 + +if __name__ == '__main__': + run_script( + main, + log_file_name=log_file_name, + operation_name='ipa-adtrust-install') diff --git a/install/tools/ipa-advise b/install/tools/ipa-advise deleted file mode 100755 index 527a045..0000000 --- a/install/tools/ipa-advise +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Tomas Babej -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.advise.base import IpaAdvise - -IpaAdvise.run_cli() diff --git a/install/tools/ipa-advise.in b/install/tools/ipa-advise.in new file mode 100644 index 0000000..7018041 --- /dev/null +++ b/install/tools/ipa-advise.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Tomas Babej +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.advise.base import IpaAdvise + +IpaAdvise.run_cli() diff --git a/install/tools/ipa-backup b/install/tools/ipa-backup deleted file mode 100755 index 61bcb10..0000000 --- a/install/tools/ipa-backup +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Rob Crittenden -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_backup import Backup - -Backup.run_cli() diff --git a/install/tools/ipa-backup.in b/install/tools/ipa-backup.in new file mode 100644 index 0000000..2511d74 --- /dev/null +++ b/install/tools/ipa-backup.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_backup import Backup + +Backup.run_cli() diff --git a/install/tools/ipa-ca-install b/install/tools/ipa-ca-install deleted file mode 100755 index c583494..0000000 --- a/install/tools/ipa-ca-install +++ /dev/null @@ -1,382 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Rob Crittenden -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging -import sys -import os -import shutil -import tempfile - -from ipalib.install.kinit import kinit_keytab -from ipapython import ipautil - -from ipaclient.install import ipa_certupdate -from ipaserver.install import installutils -from ipaserver.install.installutils import create_replica_config -from ipaserver.install.installutils import check_creds, ReplicaConfig -from ipaserver.install import dsinstance, ca -from ipaserver.install import cainstance, service -from ipaserver.install import custodiainstance -from ipapython import version -from ipalib import api -from ipalib.constants import DOMAIN_LEVEL_0 -from ipapython.config import IPAOptionParser -from ipapython.ipa_log_manager import standard_logging_setup -from ipaplatform.paths import paths - -logger = logging.getLogger(os.path.basename(__file__)) - -log_file_name = paths.IPAREPLICA_CA_INSTALL_LOG -REPLICA_INFO_TOP_DIR = None - -def parse_options(): - usage = "%prog [options] [REPLICA_FILE]" - parser = IPAOptionParser(usage=usage, version=version.VERSION) - parser.add_option("-d", "--debug", dest="debug", action="store_true", - default=False, help="gather extra debugging information") - parser.add_option("-p", "--password", dest="password", sensitive=True, - help="Directory Manager (existing master) password") - parser.add_option("-w", "--admin-password", dest="admin_password", sensitive=True, - help="Admin user Kerberos password used for connection check") - parser.add_option("--no-host-dns", dest="no_host_dns", action="store_true", - default=False, - help="Do not use DNS for hostname lookup during installation") - parser.add_option("--skip-conncheck", dest="skip_conncheck", action="store_true", - default=False, help="skip connection check to remote master") - parser.add_option("--skip-schema-check", dest="skip_schema_check", action="store_true", - default=False, help="skip check for updated CA DS schema on the remote master") - parser.add_option("-U", "--unattended", dest="unattended", action="store_true", - default=False, help="unattended installation never prompts the user") - parser.add_option("--external-ca", dest="external_ca", action="store_true", - default=False, help="Generate a CSR to be signed by an external CA") - ext_cas = tuple(x.value for x in cainstance.ExternalCAType) - parser.add_option("--external-ca-type", dest="external_ca_type", - type="choice", choices=ext_cas, - metavar="{{{0}}}".format(",".join(ext_cas)), - help="Type of the external CA. Default: generic") - parser.add_option("--external-ca-profile", dest="external_ca_profile", - type='constructor', constructor=cainstance.ExternalCAProfile, - default=None, metavar="PROFILE-SPEC", - help="Specify the certificate profile/template to use " - "at the external CA") - parser.add_option("--external-cert-file", dest="external_cert_files", - action="append", metavar="FILE", - help="File containing the IPA CA certificate and the external CA certificate chain") - ca_algos = ('SHA1withRSA', 'SHA256withRSA', 'SHA512withRSA') - parser.add_option("--ca-signing-algorithm", dest="ca_signing_algorithm", - type="choice", choices=ca_algos, - metavar="{{{0}}}".format(",".join(ca_algos)), - help="Signing algorithm of the IPA CA certificate") - parser.add_option("-P", "--principal", dest="principal", sensitive=True, - default=None, help="User allowed to manage replicas") - parser.add_option("--subject-base", dest="subject_base", - default=None, - help=( - "The certificate subject base " - "(default O=). " - "RDNs are in LDAP order (most specific RDN first).")) - parser.add_option("--ca-subject", dest="ca_subject", - default=None, - help=( - "The CA certificate subject DN " - "(default CN=Certificate Authority,O=). " - "RDNs are in LDAP order (most specific RDN first).")) - - options, args = parser.parse_args() - safe_options = parser.get_safe_opts(options) - - if args: - filename = args[0] - - if len(args) != 1: - parser.error("you must provide a file generated by " - "ipa-replica-prepare") - - options.external_ca = None - options.external_cert_files = None - else: - filename = None - - if options.external_ca: - if options.external_cert_files: - parser.error("You cannot specify --external-cert-file " - "together with --external-ca") - - if options.external_ca_type and not options.external_ca: - parser.error( - "You cannot specify --external-ca-type without --external-ca") - - if options.external_ca_profile and not options.external_ca: - parser.error( - "You cannot specify --external-ca-profile " - "without --external-ca") - - return safe_options, options, filename - - -def _get_dirman_password(password=None, unattended=False): - # sys.exit() is used on purpose, because otherwise user is advised to - # uninstall the component, even though it is not needed - if not password: - if unattended: - sys.exit('Directory Manager password required') - password = installutils.read_password( - "Directory Manager (existing master)", confirm=False, - validate=False) - try: - installutils.validate_dm_password_ldap(password) - except ValueError: - sys.exit("Directory Manager password is invalid") - - return password - - -def install_replica(safe_options, options, filename): - if options.ca_subject: - sys.exit("--ca-subject cannot be used when installing a CA replica") - if options.subject_base: - sys.exit("--subject-base cannot be used when installing a CA replica") - - if options.promote: - if filename is not None: - sys.exit("Too many parameters provided. " - "No replica file is required") - else: - if filename is None: - sys.exit("A replica file is required") - if not os.path.isfile(filename): - sys.exit("Replica file %s does not exist" % filename) - - if not options.promote: - # Check if we have admin creds already, otherwise acquire them - check_creds(options, api.env.realm) - - # get the directory manager password - dirman_password = _get_dirman_password( - options.password, options.unattended) - - if (not options.promote and not options.admin_password and - not options.skip_conncheck and options.unattended): - sys.exit('admin password required') - - # Run ipa-certupdate to ensure we have the CA cert. This is - # necessary if the admin has just promoted the topology from - # CA-less to CA-ful, and ipa-certupdate has not been run yet. - ipa_certupdate.run_with_args(api) - - # CertUpdate restarts DS causing broken pipe on the original - # connection, so reconnect the backend. - api.Backend.ldap2.disconnect() - api.Backend.ldap2.connect() - - if options.promote: - config = ReplicaConfig() - config.ca_host_name = None - config.realm_name = api.env.realm - config.host_name = api.env.host - config.domain_name = api.env.domain - config.dirman_password = dirman_password - config.ca_ds_port = 389 - config.top_dir = tempfile.mkdtemp("ipa") - config.dir = config.top_dir - cafile = paths.IPA_CA_CRT - else: - config = create_replica_config(dirman_password, filename, options) - config.ca_host_name = config.master_host_name - cafile = config.dir + '/ca.crt' - - global REPLICA_INFO_TOP_DIR - REPLICA_INFO_TOP_DIR = config.top_dir - config.setup_ca = True - - if config.subject_base is None: - attrs = api.Backend.ldap2.get_ipa_config() - config.subject_base = attrs.get('ipacertificatesubjectbase')[0] - - if config.ca_host_name is None: - config.ca_host_name = \ - service.find_providing_server('CA', api.Backend.ldap2, api.env.ca_host) - - options.realm_name = config.realm_name - options.domain_name = config.domain_name - options.dm_password = config.dirman_password - options.host_name = config.host_name - options.ca_host_name = config.ca_host_name - if os.path.exists(cafile): - options.ca_cert_file = cafile - else: - options.ca_cert_file = None - - ca.install_check(True, config, options) - - custodia = custodiainstance.get_custodia_instance( - options, custodiainstance.CustodiaModes.CA_PEER) - ca.install(True, config, options, custodia=custodia) - - -def install_master(safe_options, options): - dm_password = _get_dirman_password( - options.password, options.unattended) - - options.realm_name = api.env.realm - options.domain_name = api.env.domain - options.dm_password = dm_password - options.host_name = api.env.host - - if not options.subject_base: - options.subject_base = str( - installutils.default_subject_base(api.env.realm)) - if not options.ca_subject: - options.ca_subject = str( - installutils.default_ca_subject_dn(options.subject_base)) - - try: - ca.subject_validator(ca.VALID_SUBJECT_BASE_ATTRS, options.subject_base) - except ValueError as e: - sys.exit("Subject base: {}".format(e)) - try: - ca.subject_validator(ca.VALID_SUBJECT_ATTRS, options.ca_subject) - except ValueError as e: - sys.exit("CA subject: {}".format(e)) - - ca.install_check(True, None, options) - - ca.print_ca_configuration(options) - print() - - if not options.unattended: - if not ipautil.user_input( - "Continue to configure the CA with these values?", False): - sys.exit("Installation aborted") - - # No CA peer available yet. - custodia = custodiainstance.get_custodia_instance( - options, custodiainstance.CustodiaModes.STANDALONE) - ca.install(True, None, options, custodia=custodia) - - # Run ipa-certupdate to add the new CA certificate to - # certificate databases on this server. - logger.info("Updating certificate databases.") - ipa_certupdate.run_with_args(api) - - -def install(safe_options, options, filename): - options.promote = False - - try: - if filename is None: - install_master(safe_options, options) - else: - install_replica(safe_options, options, filename) - - finally: - # Clean up if we created custom credentials - created_ccache_file = getattr(options, 'created_ccache_file', None) - if created_ccache_file is not None: - try: - os.unlink(created_ccache_file) - except OSError: - pass - - -def promote(safe_options, options, filename): - options.promote = True - - with ipautil.private_ccache(): - ccache = os.environ['KRB5CCNAME'] - - kinit_keytab( - 'host/{env.host}@{env.realm}'.format(env=api.env), - paths.KRB5_KEYTAB, - ccache) - - ca_host = service.find_providing_server('CA', api.Backend.ldap2) - if ca_host is None: - install_master(safe_options, options) - else: - install_replica(safe_options, options, filename) - - -def main(): - safe_options, options, filename = parse_options() - - if os.geteuid() != 0: - sys.exit("\nYou must be root to run this script.\n") - - if not dsinstance.DsInstance().is_configured(): - sys.exit("IPA server is not configured on this system.\n") - - if (not options.external_cert_files and - cainstance.is_ca_installed_locally()): - sys.exit("CA is already installed on this host.") - - standard_logging_setup(log_file_name, debug=options.debug) - logger.debug("%s was invoked with options: %s,%s", - sys.argv[0], safe_options, filename) - logger.debug("IPA version %s", version.VENDOR_VERSION) - - # override ra_plugin setting read from default.conf so that we have - # functional dogtag backend plugins during CA install - api.bootstrap( - context='install', confdir=paths.ETC_IPA, - in_server=True, ra_plugin='dogtag' - ) - api.finalize() - api.Backend.ldap2.connect() - domain_level = dsinstance.get_domain_level(api) - - if domain_level > DOMAIN_LEVEL_0: - promote(safe_options, options, filename) - else: - install(safe_options, options, filename) - - # pki-spawn restarts 389-DS, reconnect - api.Backend.ldap2.close() - api.Backend.ldap2.connect() - - # Enable configured services and update DNS SRV records - service.enable_services(api.env.host) - api.Command.dns_update_system_records() - api.Backend.ldap2.disconnect() - - # execute ipactl to refresh services status - ipautil.run([paths.IPACTL, 'start', '--ignore-service-failures'], - raiseonerr=False) - - -fail_message = ''' -Your system may be partly configured. -Run /usr/sbin/ipa-server-install --uninstall to clean up. -''' - -if __name__ == '__main__': - try: - installutils.run_script(main, log_file_name=log_file_name, - operation_name='ipa-ca-install', - fail_message=fail_message) - finally: - # always try to remove decrypted replica file - try: - if REPLICA_INFO_TOP_DIR: - shutil.rmtree(REPLICA_INFO_TOP_DIR) - except OSError: - pass diff --git a/install/tools/ipa-ca-install.in b/install/tools/ipa-ca-install.in new file mode 100644 index 0000000..e955d0a --- /dev/null +++ b/install/tools/ipa-ca-install.in @@ -0,0 +1,382 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging +import sys +import os +import shutil +import tempfile + +from ipalib.install.kinit import kinit_keytab +from ipapython import ipautil + +from ipaclient.install import ipa_certupdate +from ipaserver.install import installutils +from ipaserver.install.installutils import create_replica_config +from ipaserver.install.installutils import check_creds, ReplicaConfig +from ipaserver.install import dsinstance, ca +from ipaserver.install import cainstance, service +from ipaserver.install import custodiainstance +from ipapython import version +from ipalib import api +from ipalib.constants import DOMAIN_LEVEL_0 +from ipapython.config import IPAOptionParser +from ipapython.ipa_log_manager import standard_logging_setup +from ipaplatform.paths import paths + +logger = logging.getLogger(os.path.basename(__file__)) + +log_file_name = paths.IPAREPLICA_CA_INSTALL_LOG +REPLICA_INFO_TOP_DIR = None + +def parse_options(): + usage = "%prog [options] [REPLICA_FILE]" + parser = IPAOptionParser(usage=usage, version=version.VERSION) + parser.add_option("-d", "--debug", dest="debug", action="store_true", + default=False, help="gather extra debugging information") + parser.add_option("-p", "--password", dest="password", sensitive=True, + help="Directory Manager (existing master) password") + parser.add_option("-w", "--admin-password", dest="admin_password", sensitive=True, + help="Admin user Kerberos password used for connection check") + parser.add_option("--no-host-dns", dest="no_host_dns", action="store_true", + default=False, + help="Do not use DNS for hostname lookup during installation") + parser.add_option("--skip-conncheck", dest="skip_conncheck", action="store_true", + default=False, help="skip connection check to remote master") + parser.add_option("--skip-schema-check", dest="skip_schema_check", action="store_true", + default=False, help="skip check for updated CA DS schema on the remote master") + parser.add_option("-U", "--unattended", dest="unattended", action="store_true", + default=False, help="unattended installation never prompts the user") + parser.add_option("--external-ca", dest="external_ca", action="store_true", + default=False, help="Generate a CSR to be signed by an external CA") + ext_cas = tuple(x.value for x in cainstance.ExternalCAType) + parser.add_option("--external-ca-type", dest="external_ca_type", + type="choice", choices=ext_cas, + metavar="{{{0}}}".format(",".join(ext_cas)), + help="Type of the external CA. Default: generic") + parser.add_option("--external-ca-profile", dest="external_ca_profile", + type='constructor', constructor=cainstance.ExternalCAProfile, + default=None, metavar="PROFILE-SPEC", + help="Specify the certificate profile/template to use " + "at the external CA") + parser.add_option("--external-cert-file", dest="external_cert_files", + action="append", metavar="FILE", + help="File containing the IPA CA certificate and the external CA certificate chain") + ca_algos = ('SHA1withRSA', 'SHA256withRSA', 'SHA512withRSA') + parser.add_option("--ca-signing-algorithm", dest="ca_signing_algorithm", + type="choice", choices=ca_algos, + metavar="{{{0}}}".format(",".join(ca_algos)), + help="Signing algorithm of the IPA CA certificate") + parser.add_option("-P", "--principal", dest="principal", sensitive=True, + default=None, help="User allowed to manage replicas") + parser.add_option("--subject-base", dest="subject_base", + default=None, + help=( + "The certificate subject base " + "(default O=). " + "RDNs are in LDAP order (most specific RDN first).")) + parser.add_option("--ca-subject", dest="ca_subject", + default=None, + help=( + "The CA certificate subject DN " + "(default CN=Certificate Authority,O=). " + "RDNs are in LDAP order (most specific RDN first).")) + + options, args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + if args: + filename = args[0] + + if len(args) != 1: + parser.error("you must provide a file generated by " + "ipa-replica-prepare") + + options.external_ca = None + options.external_cert_files = None + else: + filename = None + + if options.external_ca: + if options.external_cert_files: + parser.error("You cannot specify --external-cert-file " + "together with --external-ca") + + if options.external_ca_type and not options.external_ca: + parser.error( + "You cannot specify --external-ca-type without --external-ca") + + if options.external_ca_profile and not options.external_ca: + parser.error( + "You cannot specify --external-ca-profile " + "without --external-ca") + + return safe_options, options, filename + + +def _get_dirman_password(password=None, unattended=False): + # sys.exit() is used on purpose, because otherwise user is advised to + # uninstall the component, even though it is not needed + if not password: + if unattended: + sys.exit('Directory Manager password required') + password = installutils.read_password( + "Directory Manager (existing master)", confirm=False, + validate=False) + try: + installutils.validate_dm_password_ldap(password) + except ValueError: + sys.exit("Directory Manager password is invalid") + + return password + + +def install_replica(safe_options, options, filename): + if options.ca_subject: + sys.exit("--ca-subject cannot be used when installing a CA replica") + if options.subject_base: + sys.exit("--subject-base cannot be used when installing a CA replica") + + if options.promote: + if filename is not None: + sys.exit("Too many parameters provided. " + "No replica file is required") + else: + if filename is None: + sys.exit("A replica file is required") + if not os.path.isfile(filename): + sys.exit("Replica file %s does not exist" % filename) + + if not options.promote: + # Check if we have admin creds already, otherwise acquire them + check_creds(options, api.env.realm) + + # get the directory manager password + dirman_password = _get_dirman_password( + options.password, options.unattended) + + if (not options.promote and not options.admin_password and + not options.skip_conncheck and options.unattended): + sys.exit('admin password required') + + # Run ipa-certupdate to ensure we have the CA cert. This is + # necessary if the admin has just promoted the topology from + # CA-less to CA-ful, and ipa-certupdate has not been run yet. + ipa_certupdate.run_with_args(api) + + # CertUpdate restarts DS causing broken pipe on the original + # connection, so reconnect the backend. + api.Backend.ldap2.disconnect() + api.Backend.ldap2.connect() + + if options.promote: + config = ReplicaConfig() + config.ca_host_name = None + config.realm_name = api.env.realm + config.host_name = api.env.host + config.domain_name = api.env.domain + config.dirman_password = dirman_password + config.ca_ds_port = 389 + config.top_dir = tempfile.mkdtemp("ipa") + config.dir = config.top_dir + cafile = paths.IPA_CA_CRT + else: + config = create_replica_config(dirman_password, filename, options) + config.ca_host_name = config.master_host_name + cafile = config.dir + '/ca.crt' + + global REPLICA_INFO_TOP_DIR + REPLICA_INFO_TOP_DIR = config.top_dir + config.setup_ca = True + + if config.subject_base is None: + attrs = api.Backend.ldap2.get_ipa_config() + config.subject_base = attrs.get('ipacertificatesubjectbase')[0] + + if config.ca_host_name is None: + config.ca_host_name = \ + service.find_providing_server('CA', api.Backend.ldap2, api.env.ca_host) + + options.realm_name = config.realm_name + options.domain_name = config.domain_name + options.dm_password = config.dirman_password + options.host_name = config.host_name + options.ca_host_name = config.ca_host_name + if os.path.exists(cafile): + options.ca_cert_file = cafile + else: + options.ca_cert_file = None + + ca.install_check(True, config, options) + + custodia = custodiainstance.get_custodia_instance( + options, custodiainstance.CustodiaModes.CA_PEER) + ca.install(True, config, options, custodia=custodia) + + +def install_master(safe_options, options): + dm_password = _get_dirman_password( + options.password, options.unattended) + + options.realm_name = api.env.realm + options.domain_name = api.env.domain + options.dm_password = dm_password + options.host_name = api.env.host + + if not options.subject_base: + options.subject_base = str( + installutils.default_subject_base(api.env.realm)) + if not options.ca_subject: + options.ca_subject = str( + installutils.default_ca_subject_dn(options.subject_base)) + + try: + ca.subject_validator(ca.VALID_SUBJECT_BASE_ATTRS, options.subject_base) + except ValueError as e: + sys.exit("Subject base: {}".format(e)) + try: + ca.subject_validator(ca.VALID_SUBJECT_ATTRS, options.ca_subject) + except ValueError as e: + sys.exit("CA subject: {}".format(e)) + + ca.install_check(True, None, options) + + ca.print_ca_configuration(options) + print() + + if not options.unattended: + if not ipautil.user_input( + "Continue to configure the CA with these values?", False): + sys.exit("Installation aborted") + + # No CA peer available yet. + custodia = custodiainstance.get_custodia_instance( + options, custodiainstance.CustodiaModes.STANDALONE) + ca.install(True, None, options, custodia=custodia) + + # Run ipa-certupdate to add the new CA certificate to + # certificate databases on this server. + logger.info("Updating certificate databases.") + ipa_certupdate.run_with_args(api) + + +def install(safe_options, options, filename): + options.promote = False + + try: + if filename is None: + install_master(safe_options, options) + else: + install_replica(safe_options, options, filename) + + finally: + # Clean up if we created custom credentials + created_ccache_file = getattr(options, 'created_ccache_file', None) + if created_ccache_file is not None: + try: + os.unlink(created_ccache_file) + except OSError: + pass + + +def promote(safe_options, options, filename): + options.promote = True + + with ipautil.private_ccache(): + ccache = os.environ['KRB5CCNAME'] + + kinit_keytab( + 'host/{env.host}@{env.realm}'.format(env=api.env), + paths.KRB5_KEYTAB, + ccache) + + ca_host = service.find_providing_server('CA', api.Backend.ldap2) + if ca_host is None: + install_master(safe_options, options) + else: + install_replica(safe_options, options, filename) + + +def main(): + safe_options, options, filename = parse_options() + + if os.geteuid() != 0: + sys.exit("\nYou must be root to run this script.\n") + + if not dsinstance.DsInstance().is_configured(): + sys.exit("IPA server is not configured on this system.\n") + + if (not options.external_cert_files and + cainstance.is_ca_installed_locally()): + sys.exit("CA is already installed on this host.") + + standard_logging_setup(log_file_name, debug=options.debug) + logger.debug("%s was invoked with options: %s,%s", + sys.argv[0], safe_options, filename) + logger.debug("IPA version %s", version.VENDOR_VERSION) + + # override ra_plugin setting read from default.conf so that we have + # functional dogtag backend plugins during CA install + api.bootstrap( + context='install', confdir=paths.ETC_IPA, + in_server=True, ra_plugin='dogtag' + ) + api.finalize() + api.Backend.ldap2.connect() + domain_level = dsinstance.get_domain_level(api) + + if domain_level > DOMAIN_LEVEL_0: + promote(safe_options, options, filename) + else: + install(safe_options, options, filename) + + # pki-spawn restarts 389-DS, reconnect + api.Backend.ldap2.close() + api.Backend.ldap2.connect() + + # Enable configured services and update DNS SRV records + service.enable_services(api.env.host) + api.Command.dns_update_system_records() + api.Backend.ldap2.disconnect() + + # execute ipactl to refresh services status + ipautil.run([paths.IPACTL, 'start', '--ignore-service-failures'], + raiseonerr=False) + + +fail_message = ''' +Your system may be partly configured. +Run /usr/sbin/ipa-server-install --uninstall to clean up. +''' + +if __name__ == '__main__': + try: + installutils.run_script(main, log_file_name=log_file_name, + operation_name='ipa-ca-install', + fail_message=fail_message) + finally: + # always try to remove decrypted replica file + try: + if REPLICA_INFO_TOP_DIR: + shutil.rmtree(REPLICA_INFO_TOP_DIR) + except OSError: + pass diff --git a/install/tools/ipa-cacert-manage b/install/tools/ipa-cacert-manage deleted file mode 100755 index 81f3e2f..0000000 --- a/install/tools/ipa-cacert-manage +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Jan Cholasta -# -# Copyright (C) 2014 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_cacert_manage import CACertManage - -CACertManage.run_cli() diff --git a/install/tools/ipa-cacert-manage.in b/install/tools/ipa-cacert-manage.in new file mode 100644 index 0000000..791fefd --- /dev/null +++ b/install/tools/ipa-cacert-manage.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Jan Cholasta +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_cacert_manage import CACertManage + +CACertManage.run_cli() diff --git a/install/tools/ipa-compat-manage b/install/tools/ipa-compat-manage deleted file mode 100755 index f338a69..0000000 --- a/install/tools/ipa-compat-manage +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/python3 -# Authors: Rob Crittenden -# Authors: Simo Sorce -# -# Copyright (C) 2008-2016 Red Hat, Inc. -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import sys -from ipaplatform.paths import paths -try: - from optparse import OptionParser # pylint: disable=deprecated-module - from ipapython import ipautil, config - from ipaserver.install import installutils - from ipaserver.install.ldapupdate import LDAPUpdate - from ipalib import api, errors - from ipapython.ipa_log_manager import standard_logging_setup - from ipapython.dn import DN -except ImportError as e: - print("""\ -There was a problem importing one of the required Python modules. The -error was: - - %s -""" % e, file=sys.stderr) - sys.exit(1) - -compat_dn = DN(('cn', 'Schema Compatibility'), ('cn', 'plugins'), ('cn', 'config')) -nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config')) - -def parse_options(): - usage = "%prog [options] \n" - usage += "%prog [options]\n" - parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information about the update(s)") - parser.add_option("-y", dest="password", - help="File containing the Directory Manager password") - - config.add_standard_options(parser) - options, args = parser.parse_args() - - return options, args - -def get_dirman_password(): - """Prompt the user for the Directory Manager password and verify its - correctness. - """ - password = installutils.read_password("Directory Manager", confirm=False, validate=False) - - return password - -def get_entry(dn): - """ - Return the entry for the given DN. If the entry is not found return - None. - """ - entry = None - try: - entry = api.Backend.ldap2.get_entry(dn) - except errors.NotFound: - pass - return entry - -def main(): - retval = 0 - files = [paths.SCHEMA_COMPAT_ULDIF] - - installutils.check_server_configuration() - - options, args = parse_options() - - if len(args) != 1: - sys.exit("You must specify one action: enable | disable | status") - elif args[0] != "enable" and args[0] != "disable" and args[0] != "status": - sys.exit("Unrecognized action [" + args[0] + "]") - - standard_logging_setup(None, debug=options.debug) - - dirman_password = "" - if options.password: - pw = ipautil.template_file(options.password, []) - dirman_password = pw.strip() - else: - dirman_password = get_dirman_password() - if dirman_password is None: - sys.exit("Directory Manager password required") - - api.bootstrap(context='cli', - in_server=True, - debug=options.debug, - confdir=paths.ETC_IPA) - api.finalize() - api.Backend.ldap2.connect(bind_pw=dirman_password) - - if args[0] == "status": - entry = None - try: - entry = get_entry(compat_dn) - if entry is not None and entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'on': - print("Plugin Enabled") - else: - print("Plugin Disabled") - except errors.LDAPError as lde: - print("An error occurred while talking to the server.") - print(lde) - - if args[0] == "enable": - entry = None - try: - entry = get_entry(compat_dn) - if entry is not None and entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'on': - print("Plugin already Enabled") - retval = 2 - else: - print("Enabling plugin") - - if entry is None: - ld = LDAPUpdate(dm_password=dirman_password, sub_dict={}) - if not ld.update(files): - print("Updating Directory Server failed.") - retval = 1 - else: - entry['nsslapd-pluginenabled'] = ['on'] - api.Backend.ldap2.update_entry(entry) - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - elif args[0] == "disable": - entry = None - try: - entry = get_entry(nis_config_dn) - # We can't disable schema compat if the NIS plugin is enabled - if entry is not None and entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'on': - print("The NIS plugin is configured, cannot disable compatibility.", file=sys.stderr) - print("Run 'ipa-nis-manage disable' first.", file=sys.stderr) - retval = 2 - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - if retval == 0: - entry = None - try: - entry = get_entry(compat_dn) - if entry is None or entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'off': - print("Plugin is already disabled") - retval = 2 - else: - print("Disabling plugin") - - entry['nsslapd-pluginenabled'] = ['off'] - api.Backend.ldap2.update_entry(entry) - except errors.DatabaseError as dbe: - print("An error occurred while talking to the server.") - print(dbe) - retval = 1 - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - else: - retval = 1 - - if retval == 0: - print("This setting will not take effect until you restart Directory Server.") - - api.Backend.ldap2.disconnect() - - return retval - -if __name__ == '__main__': - installutils.run_script(main, operation_name='ipa-compat-manage') diff --git a/install/tools/ipa-compat-manage.in b/install/tools/ipa-compat-manage.in new file mode 100644 index 0000000..5eee9e8 --- /dev/null +++ b/install/tools/ipa-compat-manage.in @@ -0,0 +1,193 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# Authors: Simo Sorce +# +# Copyright (C) 2008-2016 Red Hat, Inc. +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import sys +from ipaplatform.paths import paths +try: + from optparse import OptionParser # pylint: disable=deprecated-module + from ipapython import ipautil, config + from ipaserver.install import installutils + from ipaserver.install.ldapupdate import LDAPUpdate + from ipalib import api, errors + from ipapython.ipa_log_manager import standard_logging_setup + from ipapython.dn import DN +except ImportError as e: + print("""\ +There was a problem importing one of the required Python modules. The +error was: + + %s +""" % e, file=sys.stderr) + sys.exit(1) + +compat_dn = DN(('cn', 'Schema Compatibility'), ('cn', 'plugins'), ('cn', 'config')) +nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config')) + +def parse_options(): + usage = "%prog [options] \n" + usage += "%prog [options]\n" + parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information about the update(s)") + parser.add_option("-y", dest="password", + help="File containing the Directory Manager password") + + config.add_standard_options(parser) + options, args = parser.parse_args() + + return options, args + +def get_dirman_password(): + """Prompt the user for the Directory Manager password and verify its + correctness. + """ + password = installutils.read_password("Directory Manager", confirm=False, validate=False) + + return password + +def get_entry(dn): + """ + Return the entry for the given DN. If the entry is not found return + None. + """ + entry = None + try: + entry = api.Backend.ldap2.get_entry(dn) + except errors.NotFound: + pass + return entry + +def main(): + retval = 0 + files = [paths.SCHEMA_COMPAT_ULDIF] + + installutils.check_server_configuration() + + options, args = parse_options() + + if len(args) != 1: + sys.exit("You must specify one action: enable | disable | status") + elif args[0] != "enable" and args[0] != "disable" and args[0] != "status": + sys.exit("Unrecognized action [" + args[0] + "]") + + standard_logging_setup(None, debug=options.debug) + + dirman_password = "" + if options.password: + pw = ipautil.template_file(options.password, []) + dirman_password = pw.strip() + else: + dirman_password = get_dirman_password() + if dirman_password is None: + sys.exit("Directory Manager password required") + + api.bootstrap(context='cli', + in_server=True, + debug=options.debug, + confdir=paths.ETC_IPA) + api.finalize() + api.Backend.ldap2.connect(bind_pw=dirman_password) + + if args[0] == "status": + entry = None + try: + entry = get_entry(compat_dn) + if entry is not None and entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'on': + print("Plugin Enabled") + else: + print("Plugin Disabled") + except errors.LDAPError as lde: + print("An error occurred while talking to the server.") + print(lde) + + if args[0] == "enable": + entry = None + try: + entry = get_entry(compat_dn) + if entry is not None and entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'on': + print("Plugin already Enabled") + retval = 2 + else: + print("Enabling plugin") + + if entry is None: + ld = LDAPUpdate(dm_password=dirman_password, sub_dict={}) + if not ld.update(files): + print("Updating Directory Server failed.") + retval = 1 + else: + entry['nsslapd-pluginenabled'] = ['on'] + api.Backend.ldap2.update_entry(entry) + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + elif args[0] == "disable": + entry = None + try: + entry = get_entry(nis_config_dn) + # We can't disable schema compat if the NIS plugin is enabled + if entry is not None and entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'on': + print("The NIS plugin is configured, cannot disable compatibility.", file=sys.stderr) + print("Run 'ipa-nis-manage disable' first.", file=sys.stderr) + retval = 2 + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + if retval == 0: + entry = None + try: + entry = get_entry(compat_dn) + if entry is None or entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'off': + print("Plugin is already disabled") + retval = 2 + else: + print("Disabling plugin") + + entry['nsslapd-pluginenabled'] = ['off'] + api.Backend.ldap2.update_entry(entry) + except errors.DatabaseError as dbe: + print("An error occurred while talking to the server.") + print(dbe) + retval = 1 + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + else: + retval = 1 + + if retval == 0: + print("This setting will not take effect until you restart Directory Server.") + + api.Backend.ldap2.disconnect() + + return retval + +if __name__ == '__main__': + installutils.run_script(main, operation_name='ipa-compat-manage') diff --git a/install/tools/ipa-csreplica-manage b/install/tools/ipa-csreplica-manage deleted file mode 100755 index 16b3914..0000000 --- a/install/tools/ipa-csreplica-manage +++ /dev/null @@ -1,504 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Rob Crittenden -# -# Based on ipa-replica-manage by Karl MacMillan -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging -import sys -import os - -from ipaplatform.paths import paths -from ipaserver.install import (replication, installutils, bindinstance, - cainstance) -from ipalib import api, errors -from ipalib.util import has_managed_topology -from ipapython import ipautil, ipaldap, version -from ipapython.admintool import ScriptError -from ipapython.dn import DN - -logger = logging.getLogger(os.path.basename(__file__)) - -# dict of command name and tuples of min/max num of args needed -commands = { - "list": (0, 1, "[master fqdn]", ""), - "connect": (1, 2, " [other master fqdn]", - "must provide the name of the servers to connect"), - "disconnect": (1, 2, " [other master fqdn]", - "must provide the name of the server to disconnect"), - "del": (1, 1, "", - "must provide hostname of master to delete"), - "re-initialize": (0, 0, "", ""), - "force-sync": (0, 0, "", ""), - "set-renewal-master": (0, 1, "[master fqdn]", "") -} - - -def parse_options(): - from optparse import OptionParser # pylint: disable=deprecated-module - - parser = OptionParser(version=version.VERSION) - parser.add_option("-H", "--host", dest="host", help="starting host") - parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") - parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, - help="provide additional information") - parser.add_option("-f", "--force", dest="force", action="store_true", default=False, - help="ignore some types of errors") - parser.add_option("--from", dest="fromhost", help="Host to get data from") - - options, args = parser.parse_args() - - valid_syntax = False - - if len(args): - n = len(args) - 1 - k = commands.keys() - for cmd in k: - if cmd == args[0]: - v = commands[cmd] - err = None - if n < v[0]: - err = v[3] - elif n > v[1]: - err = "too many arguments" - else: - valid_syntax = True - if err: - parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2])) - - if not valid_syntax: - cmdstr = " | ".join(commands.keys()) - parser.error("must provide a command [%s]" % cmdstr) - - return options, args - -def list_replicas(realm, host, replica, dirman_passwd, verbose): - - peers = {} - - try: - # connect to main IPA LDAP server - ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) - conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) - conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, - bind_password=dirman_passwd) - - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) - entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) - - for ent in entries: - try: - cadn = DN(('cn', 'CA'), DN(ent.dn)) - entry = conn.get_entry(cadn) - peers[ent.single_value['cn']] = ['master', ''] - except errors.NotFound: - peers[ent.single_value['cn']] = ['CA not configured', ''] - - except Exception as e: - sys.exit( - "Failed to get data from '%s' while trying to list replicas: %s" % - (host, e)) - finally: - conn.unbind() - - if not replica: - for k, p in peers.items(): - print('%s: %s' % (k, p[0])) - return - - try: - repl = replication.get_cs_replication_manager(realm, replica, dirman_passwd) - except Exception as e: - sys.exit(str(e)) - - entries = repl.find_replication_agreements() - - for entry in entries: - print('%s' % entry.single_value.get('nsds5replicahost')) - - if verbose: - print(" last init status: %s" % entry.single_value.get( - 'nsds5replicalastinitstatus')) - print(" last init ended: %s" % str( - ipautil.parse_generalized_time( - entry.single_value['nsds5replicalastinitend']))) - print(" last update status: %s" % entry.single_value.get( - 'nsds5replicalastupdatestatus')) - print(" last update ended: %s" % str( - ipautil.parse_generalized_time( - entry.single_value['nsds5replicalastupdateend']))) - -def del_link(realm, replica1, replica2, dirman_passwd, force=False): - - repl2 = None - - try: - repl1 = replication.get_cs_replication_manager(realm, replica1, dirman_passwd) - - repl1.hostnames = [replica1, replica2] - - repl_list1 = repl1.find_replication_agreements() - - # Find the DN of the replication agreement to remove - replica1_dn = None - for e in repl_list1: - if e.single_value.get('nsDS5ReplicaHost') == replica2: - replica1_dn = e.dn - break - - if replica1_dn is None: - sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) - - repl1.hostnames = [replica1, replica2] - - except errors.NetworkError as e: - sys.exit("Unable to connect to %s: %s" % (replica1, e)) - except Exception as e: - sys.exit("Failed to get data from '%s': %s" % (replica1, e)) - - try: - repl2 = replication.get_cs_replication_manager(realm, replica2, dirman_passwd) - - repl2.hostnames = [replica1, replica2] - - repl_list = repl2.find_replication_agreements() - - # Now that we've confirmed that both hostnames are vaild, make sure - # that we aren't removing the last link from either side. - if not force and len(repl_list) <= 1: - print("Cannot remove the last replication link of '%s'" % replica2) - print("Please use the 'del' command to remove it from the domain") - sys.exit(1) - - if not force and len(repl_list1) <= 1: - print("Cannot remove the last replication link of '%s'" % replica1) - print("Please use the 'del' command to remove it from the domain") - sys.exit(1) - - # Find the DN of the replication agreement to remove - replica2_dn = None - for entry in repl_list: - if entry.single_value.get('nsDS5ReplicaHost') == replica1: - replica2_dn = entry.dn - break - - # This should never happen - if replica2_dn is None: - sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) - - except errors.NotFound: - print("'%s' has no replication agreement for '%s'" % (replica2, replica1)) - if not force: - return - except Exception as exc: - print("Failed to get data from '%s': %s" % (replica2, exc)) - if not force: - sys.exit(1) - - if repl2: - failed = False - try: - repl2.delete_agreement(replica1, replica2_dn) - repl2.delete_referral(replica1, repl1.port) - except Exception as exc: - print("Unable to remove agreement on %s: %s" % (replica2, exc)) - failed = True - - if failed: - if force: - print("Forcing removal on '%s'" % replica1) - else: - sys.exit(1) - - if not repl2 and force: - print("Forcing removal on '%s'" % replica1) - - repl1.delete_agreement(replica2, replica1_dn) - repl1.delete_referral(replica2, repl2.port) - - print("Deleted replication agreement from '%s' to '%s'" % (replica1, replica2)) - -def del_master(realm, hostname, options): - delrepl = None - - # 1. Connect to the local dogtag DS server - try: - thisrepl = replication.get_cs_replication_manager(realm, options.host, - options.dirman_passwd) - except Exception as e: - sys.exit("Failed to connect to server %s: %s" % (options.host, e)) - - # 2. Ensure we have an agreement with the master - if thisrepl.get_replication_agreement(hostname) is None: - sys.exit("'%s' has no replication agreement for '%s'" % (options.host, hostname)) - - # 3. Connect to the dogtag DS to be removed. - try: - delrepl = replication.get_cs_replication_manager(realm, hostname, - options.dirman_passwd) - except Exception as e: - if not options.force: - print("Unable to delete replica %s: %s" % (hostname, e)) - sys.exit(1) - else: - print("Unable to connect to replica %s, forcing removal" % hostname) - - # 4. Get list of agreements. - if delrepl is None: - # server not up, just remove it from this server - replica_names = [options.host] - else: - replica_entries = delrepl.find_ipa_replication_agreements() - replica_names = [rep.single_value.get('nsds5replicahost') - for rep in replica_entries] - - # 5. Remove each agreement - for r in replica_names: - try: - del_link(realm, r, hostname, options.dirman_passwd, force=True) - except Exception as e: - sys.exit("There were issues removing a connection: %s" % e) - - # 6. Pick CA renewal master - ca = cainstance.CAInstance(api.env.realm) - if ca.is_renewal_master(hostname): - ca.set_renewal_master(options.host) - - # 7. And clean up the removed replica DNS entries if any. - try: - if bindinstance.dns_container_exists(api.env.basedn): - bind = bindinstance.BindInstance() - bind.update_system_records() - except Exception as e: - print("Failed to cleanup %s DNS entries: %s" % (hostname, e)) - print("You may need to manually remove them from the tree") - -def add_link(realm, replica1, replica2, dirman_passwd, options): - try: - repl2 = replication.get_cs_replication_manager(realm, replica2, - dirman_passwd) - except Exception as e: - sys.exit(str(e)) - try: - ldap_uri = ipaldap.get_ldap_uri(replica2, 636, cacert=paths.IPA_CA_CRT) - conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) - conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, - bind_password=dirman_passwd) - - dn = DN(('cn', 'CA'), ('cn', replica2), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), - ipautil.realm_to_suffix(realm)) - conn.get_entries(dn, conn.SCOPE_BASE) - conn.unbind() - except errors.NotFound: - sys.exit('%s does not have a CA configured.' % replica2) - except errors.NetworkError as e: - sys.exit("Unable to connect to %s: %s" % (ipautil.format_netloc(replica2, 636), str(e))) - except Exception as e: - sys.exit("Failed to get data while trying to bind to '%s': %s" % (replica1, str(e))) - - try: - repl1 = replication.get_cs_replication_manager(realm, replica1, - dirman_passwd) - entries = repl1.find_replication_agreements() - for e in entries: - if e.single_value.get('nsDS5ReplicaHost') == replica2: - sys.exit('This replication agreement already exists.') - repl1.hostnames = [replica1, replica2] - - except errors.NotFound: - sys.exit("Cannot find replica '%s'" % replica1) - except errors.NetworkError as e: - sys.exit("Unable to connect to %s: %s" % (replica1, e)) - except Exception as e: - sys.exit( - "Failed to get data from '%s' while trying to get current " - "agreements: %s" % (replica1, e)) - - repl1.setup_replication( - replica2, repl2.port, 0, DN(('cn', 'Directory Manager')), - dirman_passwd, is_cs_replica=True, local_port=repl1.port) - print("Connected '%s' to '%s'" % (replica1, replica2)) - -def re_initialize(realm, options): - - if not options.fromhost: - sys.exit("re-initialize requires the option --from ") - - thishost = installutils.get_fqdn() - - try: - repl = replication.get_cs_replication_manager(realm, options.fromhost, - options.dirman_passwd) - thisrepl = replication.get_cs_replication_manager(realm, thishost, - options.dirman_passwd) - except Exception as e: - sys.exit(str(e)) - - filter = repl.get_agreement_filter(host=thishost) - try: - entry = repl.conn.get_entries( - DN(('cn', 'config')), repl.conn.SCOPE_SUBTREE, filter) - except errors.NotFound: - logger.error("Unable to find %s -> %s replication agreement", - options.fromhost, thishost) - sys.exit(1) - if len(entry) > 1: - logger.error("Found multiple agreements for %s. Only initializing the " - "first one returned: %s", thishost, entry[0].dn) - - repl.hostnames = thisrepl.hostnames = [thishost, options.fromhost] - thisrepl.enable_agreement(options.fromhost) - repl.enable_agreement(thishost) - - repl.initialize_replication(entry[0].dn, repl.conn) - repl.wait_for_repl_init(repl.conn, entry[0].dn) - -def force_sync(realm, thishost, fromhost, dirman_passwd): - - try: - repl = replication.get_cs_replication_manager(realm, fromhost, - dirman_passwd) - repl.force_sync(repl.conn, thishost) - except Exception as e: - sys.exit(str(e)) - -def set_renewal_master(realm, replica): - if not replica: - replica = installutils.get_fqdn() - - ca = cainstance.CAInstance(realm) - if ca.is_renewal_master(replica): - sys.exit("%s is already the renewal master" % replica) - - try: - ca.set_renewal_master(replica) - except Exception as e: - sys.exit("Failed to set renewal master to %s: %s" % (replica, e)) - - print("%s is now the renewal master" % replica) - - -def exit_on_managed_topology(what, hint="topologysegment"): - if hint == "topologysegment": - hinttext = ("Please use `ipa topologysegment-*` commands to manage " - "the topology.") - elif hint == "ipa-replica-manage-del": - hinttext = ("Please use the `ipa-replica-manage del` command.") - else: - assert False, "Unexpected value" - sys.exit("{0} is deprecated with managed IPA replication topology. {1}" - .format(what, hinttext)) - - -def main(): - installutils.check_server_configuration() - options, args = parse_options() - - # Just initialize the environment. This is so the installer can have - # access to the plugin environment - api_env = {} - if os.getegid() != 0: - api_env['log'] = None # turn off logging for non-root - - api.bootstrap( - context='cli', - in_server=True, - verbose=options.verbose, - confdir=paths.ETC_IPA, - **api_env - ) - api.finalize() - - dirman_passwd = None - realm = api.env.realm - - if options.host: - host = options.host - else: - host = installutils.get_fqdn() - - options.host = host - - if options.dirman_passwd: - dirman_passwd = options.dirman_passwd - else: - dirman_passwd = installutils.read_password("Directory Manager", confirm=False, - validate=False, retry=False) - if dirman_passwd is None: - sys.exit("Directory Manager password required") - - options.dirman_passwd = dirman_passwd - - api.Backend.ldap2.connect(bind_pw=options.dirman_passwd) - - if args[0] == "list": - replica = None - if len(args) == 2: - replica = args[1] - list_replicas(realm, host, replica, dirman_passwd, options.verbose) - elif args[0] == "del": - if has_managed_topology(api): - exit_on_managed_topology( - "Removal of IPA CS replication agreement and replication data", - hint="ipa-replica-manage-del") - del_master(realm, args[1], options) - elif args[0] == "re-initialize": - re_initialize(realm, options) - elif args[0] == "force-sync": - if not options.fromhost: - sys.exit("force-sync requires the option --from ") - force_sync(realm, host, options.fromhost, options.dirman_passwd) - elif args[0] == "connect": - if has_managed_topology(api): - exit_on_managed_topology("Creation of IPA CS replication agreement") - if len(args) == 3: - replica1 = args[1] - replica2 = args[2] - elif len(args) == 2: - replica1 = host - replica2 = args[1] - add_link(realm, replica1, replica2, dirman_passwd, options) - elif args[0] == "disconnect": - if has_managed_topology(api): - exit_on_managed_topology("Removal of IPA CS replication agreement") - if len(args) == 3: - replica1 = args[1] - replica2 = args[2] - elif len(args) == 2: - replica1 = host - replica2 = args[1] - del_link(realm, replica1, replica2, dirman_passwd, options.force) - elif args[0] == 'set-renewal-master': - replica = None - if len(args) > 1: - replica = args[1] - set_renewal_master(realm, replica) - - api.Backend.ldap2.disconnect() - -try: - main() -except KeyboardInterrupt: - sys.exit(1) -except (SystemExit, ScriptError) as e: - sys.exit(e) -except Exception as e: - sys.exit("unexpected error: %s" % e) diff --git a/install/tools/ipa-csreplica-manage.in b/install/tools/ipa-csreplica-manage.in new file mode 100644 index 0000000..b9ad377 --- /dev/null +++ b/install/tools/ipa-csreplica-manage.in @@ -0,0 +1,504 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# +# Based on ipa-replica-manage by Karl MacMillan +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging +import sys +import os + +from ipaplatform.paths import paths +from ipaserver.install import (replication, installutils, bindinstance, + cainstance) +from ipalib import api, errors +from ipalib.util import has_managed_topology +from ipapython import ipautil, ipaldap, version +from ipapython.admintool import ScriptError +from ipapython.dn import DN + +logger = logging.getLogger(os.path.basename(__file__)) + +# dict of command name and tuples of min/max num of args needed +commands = { + "list": (0, 1, "[master fqdn]", ""), + "connect": (1, 2, " [other master fqdn]", + "must provide the name of the servers to connect"), + "disconnect": (1, 2, " [other master fqdn]", + "must provide the name of the server to disconnect"), + "del": (1, 1, "", + "must provide hostname of master to delete"), + "re-initialize": (0, 0, "", ""), + "force-sync": (0, 0, "", ""), + "set-renewal-master": (0, 1, "[master fqdn]", "") +} + + +def parse_options(): + from optparse import OptionParser # pylint: disable=deprecated-module + + parser = OptionParser(version=version.VERSION) + parser.add_option("-H", "--host", dest="host", help="starting host") + parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") + parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="provide additional information") + parser.add_option("-f", "--force", dest="force", action="store_true", default=False, + help="ignore some types of errors") + parser.add_option("--from", dest="fromhost", help="Host to get data from") + + options, args = parser.parse_args() + + valid_syntax = False + + if len(args): + n = len(args) - 1 + k = commands.keys() + for cmd in k: + if cmd == args[0]: + v = commands[cmd] + err = None + if n < v[0]: + err = v[3] + elif n > v[1]: + err = "too many arguments" + else: + valid_syntax = True + if err: + parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2])) + + if not valid_syntax: + cmdstr = " | ".join(commands.keys()) + parser.error("must provide a command [%s]" % cmdstr) + + return options, args + +def list_replicas(realm, host, replica, dirman_passwd, verbose): + + peers = {} + + try: + # connect to main IPA LDAP server + ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) + conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) + conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, + bind_password=dirman_passwd) + + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) + entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) + + for ent in entries: + try: + cadn = DN(('cn', 'CA'), DN(ent.dn)) + entry = conn.get_entry(cadn) + peers[ent.single_value['cn']] = ['master', ''] + except errors.NotFound: + peers[ent.single_value['cn']] = ['CA not configured', ''] + + except Exception as e: + sys.exit( + "Failed to get data from '%s' while trying to list replicas: %s" % + (host, e)) + finally: + conn.unbind() + + if not replica: + for k, p in peers.items(): + print('%s: %s' % (k, p[0])) + return + + try: + repl = replication.get_cs_replication_manager(realm, replica, dirman_passwd) + except Exception as e: + sys.exit(str(e)) + + entries = repl.find_replication_agreements() + + for entry in entries: + print('%s' % entry.single_value.get('nsds5replicahost')) + + if verbose: + print(" last init status: %s" % entry.single_value.get( + 'nsds5replicalastinitstatus')) + print(" last init ended: %s" % str( + ipautil.parse_generalized_time( + entry.single_value['nsds5replicalastinitend']))) + print(" last update status: %s" % entry.single_value.get( + 'nsds5replicalastupdatestatus')) + print(" last update ended: %s" % str( + ipautil.parse_generalized_time( + entry.single_value['nsds5replicalastupdateend']))) + +def del_link(realm, replica1, replica2, dirman_passwd, force=False): + + repl2 = None + + try: + repl1 = replication.get_cs_replication_manager(realm, replica1, dirman_passwd) + + repl1.hostnames = [replica1, replica2] + + repl_list1 = repl1.find_replication_agreements() + + # Find the DN of the replication agreement to remove + replica1_dn = None + for e in repl_list1: + if e.single_value.get('nsDS5ReplicaHost') == replica2: + replica1_dn = e.dn + break + + if replica1_dn is None: + sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) + + repl1.hostnames = [replica1, replica2] + + except errors.NetworkError as e: + sys.exit("Unable to connect to %s: %s" % (replica1, e)) + except Exception as e: + sys.exit("Failed to get data from '%s': %s" % (replica1, e)) + + try: + repl2 = replication.get_cs_replication_manager(realm, replica2, dirman_passwd) + + repl2.hostnames = [replica1, replica2] + + repl_list = repl2.find_replication_agreements() + + # Now that we've confirmed that both hostnames are vaild, make sure + # that we aren't removing the last link from either side. + if not force and len(repl_list) <= 1: + print("Cannot remove the last replication link of '%s'" % replica2) + print("Please use the 'del' command to remove it from the domain") + sys.exit(1) + + if not force and len(repl_list1) <= 1: + print("Cannot remove the last replication link of '%s'" % replica1) + print("Please use the 'del' command to remove it from the domain") + sys.exit(1) + + # Find the DN of the replication agreement to remove + replica2_dn = None + for entry in repl_list: + if entry.single_value.get('nsDS5ReplicaHost') == replica1: + replica2_dn = entry.dn + break + + # This should never happen + if replica2_dn is None: + sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) + + except errors.NotFound: + print("'%s' has no replication agreement for '%s'" % (replica2, replica1)) + if not force: + return + except Exception as exc: + print("Failed to get data from '%s': %s" % (replica2, exc)) + if not force: + sys.exit(1) + + if repl2: + failed = False + try: + repl2.delete_agreement(replica1, replica2_dn) + repl2.delete_referral(replica1, repl1.port) + except Exception as exc: + print("Unable to remove agreement on %s: %s" % (replica2, exc)) + failed = True + + if failed: + if force: + print("Forcing removal on '%s'" % replica1) + else: + sys.exit(1) + + if not repl2 and force: + print("Forcing removal on '%s'" % replica1) + + repl1.delete_agreement(replica2, replica1_dn) + repl1.delete_referral(replica2, repl2.port) + + print("Deleted replication agreement from '%s' to '%s'" % (replica1, replica2)) + +def del_master(realm, hostname, options): + delrepl = None + + # 1. Connect to the local dogtag DS server + try: + thisrepl = replication.get_cs_replication_manager(realm, options.host, + options.dirman_passwd) + except Exception as e: + sys.exit("Failed to connect to server %s: %s" % (options.host, e)) + + # 2. Ensure we have an agreement with the master + if thisrepl.get_replication_agreement(hostname) is None: + sys.exit("'%s' has no replication agreement for '%s'" % (options.host, hostname)) + + # 3. Connect to the dogtag DS to be removed. + try: + delrepl = replication.get_cs_replication_manager(realm, hostname, + options.dirman_passwd) + except Exception as e: + if not options.force: + print("Unable to delete replica %s: %s" % (hostname, e)) + sys.exit(1) + else: + print("Unable to connect to replica %s, forcing removal" % hostname) + + # 4. Get list of agreements. + if delrepl is None: + # server not up, just remove it from this server + replica_names = [options.host] + else: + replica_entries = delrepl.find_ipa_replication_agreements() + replica_names = [rep.single_value.get('nsds5replicahost') + for rep in replica_entries] + + # 5. Remove each agreement + for r in replica_names: + try: + del_link(realm, r, hostname, options.dirman_passwd, force=True) + except Exception as e: + sys.exit("There were issues removing a connection: %s" % e) + + # 6. Pick CA renewal master + ca = cainstance.CAInstance(api.env.realm) + if ca.is_renewal_master(hostname): + ca.set_renewal_master(options.host) + + # 7. And clean up the removed replica DNS entries if any. + try: + if bindinstance.dns_container_exists(api.env.basedn): + bind = bindinstance.BindInstance() + bind.update_system_records() + except Exception as e: + print("Failed to cleanup %s DNS entries: %s" % (hostname, e)) + print("You may need to manually remove them from the tree") + +def add_link(realm, replica1, replica2, dirman_passwd, options): + try: + repl2 = replication.get_cs_replication_manager(realm, replica2, + dirman_passwd) + except Exception as e: + sys.exit(str(e)) + try: + ldap_uri = ipaldap.get_ldap_uri(replica2, 636, cacert=paths.IPA_CA_CRT) + conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) + conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, + bind_password=dirman_passwd) + + dn = DN(('cn', 'CA'), ('cn', replica2), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), + ipautil.realm_to_suffix(realm)) + conn.get_entries(dn, conn.SCOPE_BASE) + conn.unbind() + except errors.NotFound: + sys.exit('%s does not have a CA configured.' % replica2) + except errors.NetworkError as e: + sys.exit("Unable to connect to %s: %s" % (ipautil.format_netloc(replica2, 636), str(e))) + except Exception as e: + sys.exit("Failed to get data while trying to bind to '%s': %s" % (replica1, str(e))) + + try: + repl1 = replication.get_cs_replication_manager(realm, replica1, + dirman_passwd) + entries = repl1.find_replication_agreements() + for e in entries: + if e.single_value.get('nsDS5ReplicaHost') == replica2: + sys.exit('This replication agreement already exists.') + repl1.hostnames = [replica1, replica2] + + except errors.NotFound: + sys.exit("Cannot find replica '%s'" % replica1) + except errors.NetworkError as e: + sys.exit("Unable to connect to %s: %s" % (replica1, e)) + except Exception as e: + sys.exit( + "Failed to get data from '%s' while trying to get current " + "agreements: %s" % (replica1, e)) + + repl1.setup_replication( + replica2, repl2.port, 0, DN(('cn', 'Directory Manager')), + dirman_passwd, is_cs_replica=True, local_port=repl1.port) + print("Connected '%s' to '%s'" % (replica1, replica2)) + +def re_initialize(realm, options): + + if not options.fromhost: + sys.exit("re-initialize requires the option --from ") + + thishost = installutils.get_fqdn() + + try: + repl = replication.get_cs_replication_manager(realm, options.fromhost, + options.dirman_passwd) + thisrepl = replication.get_cs_replication_manager(realm, thishost, + options.dirman_passwd) + except Exception as e: + sys.exit(str(e)) + + filter = repl.get_agreement_filter(host=thishost) + try: + entry = repl.conn.get_entries( + DN(('cn', 'config')), repl.conn.SCOPE_SUBTREE, filter) + except errors.NotFound: + logger.error("Unable to find %s -> %s replication agreement", + options.fromhost, thishost) + sys.exit(1) + if len(entry) > 1: + logger.error("Found multiple agreements for %s. Only initializing the " + "first one returned: %s", thishost, entry[0].dn) + + repl.hostnames = thisrepl.hostnames = [thishost, options.fromhost] + thisrepl.enable_agreement(options.fromhost) + repl.enable_agreement(thishost) + + repl.initialize_replication(entry[0].dn, repl.conn) + repl.wait_for_repl_init(repl.conn, entry[0].dn) + +def force_sync(realm, thishost, fromhost, dirman_passwd): + + try: + repl = replication.get_cs_replication_manager(realm, fromhost, + dirman_passwd) + repl.force_sync(repl.conn, thishost) + except Exception as e: + sys.exit(str(e)) + +def set_renewal_master(realm, replica): + if not replica: + replica = installutils.get_fqdn() + + ca = cainstance.CAInstance(realm) + if ca.is_renewal_master(replica): + sys.exit("%s is already the renewal master" % replica) + + try: + ca.set_renewal_master(replica) + except Exception as e: + sys.exit("Failed to set renewal master to %s: %s" % (replica, e)) + + print("%s is now the renewal master" % replica) + + +def exit_on_managed_topology(what, hint="topologysegment"): + if hint == "topologysegment": + hinttext = ("Please use `ipa topologysegment-*` commands to manage " + "the topology.") + elif hint == "ipa-replica-manage-del": + hinttext = ("Please use the `ipa-replica-manage del` command.") + else: + assert False, "Unexpected value" + sys.exit("{0} is deprecated with managed IPA replication topology. {1}" + .format(what, hinttext)) + + +def main(): + installutils.check_server_configuration() + options, args = parse_options() + + # Just initialize the environment. This is so the installer can have + # access to the plugin environment + api_env = {} + if os.getegid() != 0: + api_env['log'] = None # turn off logging for non-root + + api.bootstrap( + context='cli', + in_server=True, + verbose=options.verbose, + confdir=paths.ETC_IPA, + **api_env + ) + api.finalize() + + dirman_passwd = None + realm = api.env.realm + + if options.host: + host = options.host + else: + host = installutils.get_fqdn() + + options.host = host + + if options.dirman_passwd: + dirman_passwd = options.dirman_passwd + else: + dirman_passwd = installutils.read_password("Directory Manager", confirm=False, + validate=False, retry=False) + if dirman_passwd is None: + sys.exit("Directory Manager password required") + + options.dirman_passwd = dirman_passwd + + api.Backend.ldap2.connect(bind_pw=options.dirman_passwd) + + if args[0] == "list": + replica = None + if len(args) == 2: + replica = args[1] + list_replicas(realm, host, replica, dirman_passwd, options.verbose) + elif args[0] == "del": + if has_managed_topology(api): + exit_on_managed_topology( + "Removal of IPA CS replication agreement and replication data", + hint="ipa-replica-manage-del") + del_master(realm, args[1], options) + elif args[0] == "re-initialize": + re_initialize(realm, options) + elif args[0] == "force-sync": + if not options.fromhost: + sys.exit("force-sync requires the option --from ") + force_sync(realm, host, options.fromhost, options.dirman_passwd) + elif args[0] == "connect": + if has_managed_topology(api): + exit_on_managed_topology("Creation of IPA CS replication agreement") + if len(args) == 3: + replica1 = args[1] + replica2 = args[2] + elif len(args) == 2: + replica1 = host + replica2 = args[1] + add_link(realm, replica1, replica2, dirman_passwd, options) + elif args[0] == "disconnect": + if has_managed_topology(api): + exit_on_managed_topology("Removal of IPA CS replication agreement") + if len(args) == 3: + replica1 = args[1] + replica2 = args[2] + elif len(args) == 2: + replica1 = host + replica2 = args[1] + del_link(realm, replica1, replica2, dirman_passwd, options.force) + elif args[0] == 'set-renewal-master': + replica = None + if len(args) > 1: + replica = args[1] + set_renewal_master(realm, replica) + + api.Backend.ldap2.disconnect() + +try: + main() +except KeyboardInterrupt: + sys.exit(1) +except (SystemExit, ScriptError) as e: + sys.exit(e) +except Exception as e: + sys.exit("unexpected error: %s" % e) diff --git a/install/tools/ipa-custodia b/install/tools/ipa-custodia deleted file mode 100755 index 8a112e2..0000000 --- a/install/tools/ipa-custodia +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/python3 -# Copyright (C) 2017 IPA Project Contributors, see COPYING for license -from ipaserver.secrets.service import main - -if __name__ == '__main__': - main() diff --git a/install/tools/ipa-custodia-check b/install/tools/ipa-custodia-check deleted file mode 100755 index 7c91255..0000000 --- a/install/tools/ipa-custodia-check +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/python3 -"""Test client for ipa-custodia - -The test script is expected to be executed on an IPA server with existing -Custodia server keys. -""" -from __future__ import print_function -import argparse -import logging -import os -import platform -import socket -import warnings - -from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP - -from jwcrypto.common import json_decode -from jwcrypto.jwk import JWK - -from ipalib import api -from ipaplatform.paths import paths -import ipapython.version -from ipaserver.install.installutils import is_ipa_configured - -try: - # FreeIPA >= 4.5 - from ipaserver.secrets.client import CustodiaClient -except ImportError: - # FreeIPA <= 4.4 - from ipapython.secrets.client import CustodiaClient - -# Ignore security warning from vendored and non-vendored urllib3 -try: - from urllib3.exceptions import SecurityWarning -except ImportError: - SecurityWarning = None -else: - warnings.simplefilter("ignore", SecurityWarning) - -try: - from requests.packages.urllib3.exceptions import SecurityWarning -except ImportError: - SecurityWarning = None -else: - warnings.simplefilter("ignore", SecurityWarning) - - -KEYS = [ - 'dm/DMHash', - 'ra/ipaCert', - 'ca/auditSigningCert cert-pki-ca', - 'ca/caSigningCert cert-pki-ca', - 'ca/ocspSigningCert cert-pki-ca', - 'ca/subsystemCert cert-pki-ca', -] - -IPA_CUSTODIA_KEYFILE = os.path.join(paths.IPA_CUSTODIA_CONF_DIR, - 'server.keys') - - -logger = logging.getLogger('ipa-custodia-tester') - - -parser = argparse.ArgumentParser( - "IPA Custodia check", -) -# --store is dangerous and therefore hidden! Don't use it unless you really -# know what you are doing! Keep in mind that it might destroy your NSSDB -# unless it uses sqlite format. -parser.add_argument( - "--store", action='store_true', dest='store', - help=argparse.SUPPRESS -) -parser.add_argument( - "--debug", action='store_true', - help="Debug mode" -) -parser.add_argument( - "--verbose", action='store_true', - help='Verbose mode' -) -parser.add_argument( - "server", - help="FQDN of a IPA server (can be own FQDN for self-test)" -) -parser.add_argument( - 'keys', nargs='*', default=KEYS, - help="Remote key ({})".format(', '.join(KEYS)) -) - - -class IPACustodiaTester(object): - files = [ - paths.IPA_DEFAULT_CONF, - paths.KRB5_KEYTAB, - paths.IPA_CUSTODIA_CONF, - IPA_CUSTODIA_KEYFILE - ] - - def __init__(self, parser, args): - self.parser = parser - self.args = args - if not api.isdone('bootstrap'): - # bootstrap to initialize api.env - api.bootstrap() - self.debug("IPA API bootstrapped") - self.realm = api.env.realm - self.host = api.env.host - self.host_spn = 'host/{}@{}'.format(self.host, self.realm) - self.server_spn = 'host/{}@{}'.format(self.args.server, self.realm) - self.client = None - self._errors = [] - - def error(self, msg, fatal=False): - self._errors.append(msg) - logger.error(msg, exc_info=self.args.verbose) - if fatal: - self.exit() - - def exit(self): - if self._errors: - self.parser.exit(1, "[ERROR] One or more tests have failed.\n") - else: - self.parser.exit(0, "All tests have passed successfully.\n") - - def warning(self, msg): - logger.warning(msg) - - def info(self, msg): - logger.info(msg) - - def debug(self, msg): - logger.debug(msg) - - def check(self): - self.status() - self.check_fqdn() - self.check_files() - self.check_client() - self.check_jwk() - self.check_keys() - - def status(self): - self.info("Platform: {}".format(platform.platform())) - self.info("IPA version: {}".format( - ipapython.version.VERSION - )) - self.info("IPA vendor version: {}".format( - ipapython.version.VENDOR_VERSION - )) - self.info("Realm: {}".format(self.realm)) - self.info("Host: {}".format(self.host)) - self.info("Remote server: {}".format(self.args.server)) - if self.host == self.args.server: - self.warning("Performing self-test only.") - - def check_fqdn(self): - fqdn = socket.getfqdn() - if self.host != fqdn: - self.warning( - "socket.getfqdn() reports hostname '{}'".format(fqdn) - ) - - def check_files(self): - for filename in self.files: - if not os.path.isfile(filename): - self.error("File '{0}' is missing.".format(filename)) - else: - self.info("File '{0}' exists.".format(filename)) - - def check_client(self): - try: - self.client = CustodiaClient( - server=self.args.server, - client_service='host@{}'.format(self.host), - keyfile=IPA_CUSTODIA_KEYFILE, - keytab=paths.KRB5_KEYTAB, - realm=self.realm, - ) - except Exception as e: - self.error("Failed to create client: {}".format(e), fatal=True) - else: - self.info("Custodia client created.") - - def _check_jwk_single(self, usage_id): - usage = KEY_USAGE_MAP[usage_id] - with open(IPA_CUSTODIA_KEYFILE) as f: - dictkeys = json_decode(f.read()) - - try: - pkey = JWK(**dictkeys[usage_id]) - local_pubkey = json_decode(pkey.export_public()) - except Exception: - raise self.error( - "Failed to load and parse local JWK.", fatal=True - ) - else: - self.info("Loaded key for usage '{}' from '{}'.".format( - usage, IPA_CUSTODIA_KEYFILE - )) - - if pkey.key_id != self.host_spn: - raise self.error( - "KID '{}' != host service principal name '{}' " - "(usage: {})".format(pkey.key_id, self.host_spn, usage), - fatal=True - ) - else: - self.info( - "JWK KID matches host's service principal name '{}'.".format( - self.host_spn - )) - - # LDAP doesn't contain KID - local_pubkey.pop("kid", None) - find_key = self.client.ikk.find_key - try: - host_pubkey = json_decode(find_key(self.host_spn, usage_id)) - except Exception: - raise self.error( - "Fetching host keys {} (usage: {}) failed.".format( - self.host_spn, usage), - fatal=True - ) - else: - self.info("Checked host LDAP keys '{}' for usage {}.".format( - self.host_spn, usage - )) - - if host_pubkey != local_pubkey: - self.debug("LDAP: '{}'".format(host_pubkey)) - self.debug("Local: '{}'".format(local_pubkey)) - raise self.error( - "Host key in LDAP does not match local key.", - fatal=True - ) - else: - self.info( - "Local key for usage '{}' matches key in LDAP.".format(usage) - ) - - try: - server_pubkey = json_decode(find_key(self.server_spn, usage_id)) - except Exception: - raise self.error( - "Fetching server keys {} (usage: {}) failed.".format( - self.server_spn, usage), - fatal=True - ) - else: - self.info("Checked server LDAP keys '{}' for usage {}.".format( - self.server_spn, usage - )) - - return local_pubkey, host_pubkey, server_pubkey - - def check_jwk(self): - self._check_jwk_single(KEY_USAGE_SIG) - self._check_jwk_single(KEY_USAGE_ENC) - - def check_keys(self): - for key in self.args.keys: - try: - result = self.client.fetch_key(key, store=self.args.store) - except Exception as e: - self.error("Failed to retrieve key '{}': {}.".format( - key, e - )) - else: - self.info("Successfully retrieved '{}'.".format(key)) - if not self.args.store: - self.debug(result) - - -def main(): - args = parser.parse_args() - if args.debug: - args.verbose = True - - logging.basicConfig( - level=logging.DEBUG if args.debug else logging.INFO, - format='[%(asctime)s %(name)s] <%(levelname)s>: %(message)s', - datefmt='%Y-%m-%dT%H:%M:%S', - ) - if not is_ipa_configured(): - parser.error("IPA is not configured on this system.\n") - if os.geteuid() != 0: - parser.error("Script must be executed as root.\n") - - tester = IPACustodiaTester(parser, args) - tester.check() - tester.exit() - - -if __name__ == '__main__': - main() diff --git a/install/tools/ipa-custodia-check.in b/install/tools/ipa-custodia-check.in new file mode 100644 index 0000000..ffa9ef3 --- /dev/null +++ b/install/tools/ipa-custodia-check.in @@ -0,0 +1,296 @@ +@PYTHONSHEBANG@ +"""Test client for ipa-custodia + +The test script is expected to be executed on an IPA server with existing +Custodia server keys. +""" +from __future__ import print_function +import argparse +import logging +import os +import platform +import socket +import warnings + +from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP + +from jwcrypto.common import json_decode +from jwcrypto.jwk import JWK + +from ipalib import api +from ipaplatform.paths import paths +import ipapython.version +from ipaserver.install.installutils import is_ipa_configured + +try: + # FreeIPA >= 4.5 + from ipaserver.secrets.client import CustodiaClient +except ImportError: + # FreeIPA <= 4.4 + from ipapython.secrets.client import CustodiaClient + +# Ignore security warning from vendored and non-vendored urllib3 +try: + from urllib3.exceptions import SecurityWarning +except ImportError: + SecurityWarning = None +else: + warnings.simplefilter("ignore", SecurityWarning) + +try: + from requests.packages.urllib3.exceptions import SecurityWarning +except ImportError: + SecurityWarning = None +else: + warnings.simplefilter("ignore", SecurityWarning) + + +KEYS = [ + 'dm/DMHash', + 'ra/ipaCert', + 'ca/auditSigningCert cert-pki-ca', + 'ca/caSigningCert cert-pki-ca', + 'ca/ocspSigningCert cert-pki-ca', + 'ca/subsystemCert cert-pki-ca', +] + +IPA_CUSTODIA_KEYFILE = os.path.join(paths.IPA_CUSTODIA_CONF_DIR, + 'server.keys') + + +logger = logging.getLogger('ipa-custodia-tester') + + +parser = argparse.ArgumentParser( + "IPA Custodia check", +) +# --store is dangerous and therefore hidden! Don't use it unless you really +# know what you are doing! Keep in mind that it might destroy your NSSDB +# unless it uses sqlite format. +parser.add_argument( + "--store", action='store_true', dest='store', + help=argparse.SUPPRESS +) +parser.add_argument( + "--debug", action='store_true', + help="Debug mode" +) +parser.add_argument( + "--verbose", action='store_true', + help='Verbose mode' +) +parser.add_argument( + "server", + help="FQDN of a IPA server (can be own FQDN for self-test)" +) +parser.add_argument( + 'keys', nargs='*', default=KEYS, + help="Remote key ({})".format(', '.join(KEYS)) +) + + +class IPACustodiaTester(object): + files = [ + paths.IPA_DEFAULT_CONF, + paths.KRB5_KEYTAB, + paths.IPA_CUSTODIA_CONF, + IPA_CUSTODIA_KEYFILE + ] + + def __init__(self, parser, args): + self.parser = parser + self.args = args + if not api.isdone('bootstrap'): + # bootstrap to initialize api.env + api.bootstrap() + self.debug("IPA API bootstrapped") + self.realm = api.env.realm + self.host = api.env.host + self.host_spn = 'host/{}@{}'.format(self.host, self.realm) + self.server_spn = 'host/{}@{}'.format(self.args.server, self.realm) + self.client = None + self._errors = [] + + def error(self, msg, fatal=False): + self._errors.append(msg) + logger.error(msg, exc_info=self.args.verbose) + if fatal: + self.exit() + + def exit(self): + if self._errors: + self.parser.exit(1, "[ERROR] One or more tests have failed.\n") + else: + self.parser.exit(0, "All tests have passed successfully.\n") + + def warning(self, msg): + logger.warning(msg) + + def info(self, msg): + logger.info(msg) + + def debug(self, msg): + logger.debug(msg) + + def check(self): + self.status() + self.check_fqdn() + self.check_files() + self.check_client() + self.check_jwk() + self.check_keys() + + def status(self): + self.info("Platform: {}".format(platform.platform())) + self.info("IPA version: {}".format( + ipapython.version.VERSION + )) + self.info("IPA vendor version: {}".format( + ipapython.version.VENDOR_VERSION + )) + self.info("Realm: {}".format(self.realm)) + self.info("Host: {}".format(self.host)) + self.info("Remote server: {}".format(self.args.server)) + if self.host == self.args.server: + self.warning("Performing self-test only.") + + def check_fqdn(self): + fqdn = socket.getfqdn() + if self.host != fqdn: + self.warning( + "socket.getfqdn() reports hostname '{}'".format(fqdn) + ) + + def check_files(self): + for filename in self.files: + if not os.path.isfile(filename): + self.error("File '{0}' is missing.".format(filename)) + else: + self.info("File '{0}' exists.".format(filename)) + + def check_client(self): + try: + self.client = CustodiaClient( + server=self.args.server, + client_service='host@{}'.format(self.host), + keyfile=IPA_CUSTODIA_KEYFILE, + keytab=paths.KRB5_KEYTAB, + realm=self.realm, + ) + except Exception as e: + self.error("Failed to create client: {}".format(e), fatal=True) + else: + self.info("Custodia client created.") + + def _check_jwk_single(self, usage_id): + usage = KEY_USAGE_MAP[usage_id] + with open(IPA_CUSTODIA_KEYFILE) as f: + dictkeys = json_decode(f.read()) + + try: + pkey = JWK(**dictkeys[usage_id]) + local_pubkey = json_decode(pkey.export_public()) + except Exception: + raise self.error( + "Failed to load and parse local JWK.", fatal=True + ) + else: + self.info("Loaded key for usage '{}' from '{}'.".format( + usage, IPA_CUSTODIA_KEYFILE + )) + + if pkey.key_id != self.host_spn: + raise self.error( + "KID '{}' != host service principal name '{}' " + "(usage: {})".format(pkey.key_id, self.host_spn, usage), + fatal=True + ) + else: + self.info( + "JWK KID matches host's service principal name '{}'.".format( + self.host_spn + )) + + # LDAP doesn't contain KID + local_pubkey.pop("kid", None) + find_key = self.client.ikk.find_key + try: + host_pubkey = json_decode(find_key(self.host_spn, usage_id)) + except Exception: + raise self.error( + "Fetching host keys {} (usage: {}) failed.".format( + self.host_spn, usage), + fatal=True + ) + else: + self.info("Checked host LDAP keys '{}' for usage {}.".format( + self.host_spn, usage + )) + + if host_pubkey != local_pubkey: + self.debug("LDAP: '{}'".format(host_pubkey)) + self.debug("Local: '{}'".format(local_pubkey)) + raise self.error( + "Host key in LDAP does not match local key.", + fatal=True + ) + else: + self.info( + "Local key for usage '{}' matches key in LDAP.".format(usage) + ) + + try: + server_pubkey = json_decode(find_key(self.server_spn, usage_id)) + except Exception: + raise self.error( + "Fetching server keys {} (usage: {}) failed.".format( + self.server_spn, usage), + fatal=True + ) + else: + self.info("Checked server LDAP keys '{}' for usage {}.".format( + self.server_spn, usage + )) + + return local_pubkey, host_pubkey, server_pubkey + + def check_jwk(self): + self._check_jwk_single(KEY_USAGE_SIG) + self._check_jwk_single(KEY_USAGE_ENC) + + def check_keys(self): + for key in self.args.keys: + try: + result = self.client.fetch_key(key, store=self.args.store) + except Exception as e: + self.error("Failed to retrieve key '{}': {}.".format( + key, e + )) + else: + self.info("Successfully retrieved '{}'.".format(key)) + if not self.args.store: + self.debug(result) + + +def main(): + args = parser.parse_args() + if args.debug: + args.verbose = True + + logging.basicConfig( + level=logging.DEBUG if args.debug else logging.INFO, + format='[%(asctime)s %(name)s] <%(levelname)s>: %(message)s', + datefmt='%Y-%m-%dT%H:%M:%S', + ) + if not is_ipa_configured(): + parser.error("IPA is not configured on this system.\n") + if os.geteuid() != 0: + parser.error("Script must be executed as root.\n") + + tester = IPACustodiaTester(parser, args) + tester.check() + tester.exit() + + +if __name__ == '__main__': + main() diff --git a/install/tools/ipa-custodia.in b/install/tools/ipa-custodia.in new file mode 100644 index 0000000..aaee730 --- /dev/null +++ b/install/tools/ipa-custodia.in @@ -0,0 +1,6 @@ +@PYTHONSHEBANG@ +# Copyright (C) 2017 IPA Project Contributors, see COPYING for license +from ipaserver.secrets.service import main + +if __name__ == '__main__': + main() diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install deleted file mode 100755 index f7b9d93..0000000 --- a/install/tools/ipa-dns-install +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Martin Nagy -# Based on ipa-server-install by Karl MacMillan -# -# Copyright (C) 2007 - 2009 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging -import os -import sys - -from ipaserver.install import bindinstance -from ipaserver.install import installutils -from ipapython import version -from ipalib import api -from ipaplatform.paths import paths -from ipapython import ipautil -from ipapython.config import IPAOptionParser -from ipapython.ipa_log_manager import standard_logging_setup - -from ipaserver.install import dns as dns_installer - -logger = logging.getLogger(os.path.basename(__file__)) - -log_file_name = paths.IPASERVER_INSTALL_LOG - -def parse_options(): - parser = IPAOptionParser(version=version.VERSION) - parser.add_option("-d", "--debug", dest="debug", action="store_true", - default=False, help="print debugging information") - parser.add_option("--ip-address", dest="ip_addresses", metavar="IP_ADDRESS", - default=[], action="append", - type="ip", - help="Master Server IP Address. This option can be used " - "multiple times") - parser.add_option("--forwarder", dest="forwarders", action="append", - type="ip_with_loopback", help="Add a DNS forwarder. This option can be used multiple times") - parser.add_option("--no-forwarders", dest="no_forwarders", action="store_true", - default=False, help="Do not add any DNS forwarders, use root servers instead") - parser.add_option("--auto-forwarders", dest="auto_forwarders", - action="store_true", default=False, - help="Use DNS forwarders configured in /etc/resolv.conf") - parser.add_option("--forward-policy", dest="forward_policy", - choices=("first", "only"), default=None, - help="DNS forwarding policy for global forwarders") - parser.add_option("--reverse-zone", dest="reverse_zones", - default=[], action="append", metavar="REVERSE_ZONE", - help="The reverse DNS zone to use. This option can be used multiple times") - parser.add_option("--no-reverse", dest="no_reverse", action="store_true", - default=False, help="Do not create new reverse DNS zone") - parser.add_option("--auto-reverse", dest="auto_reverse", action="store_true", - default=False, help="Create necessary DNS zones") - parser.add_option("--allow-zone-overlap", dest="allow_zone_overlap", - action="store_true", default=False, help="Create DNS " - "zone even if it already exists") - parser.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true", - default=False, help="Disable DNSSEC validation") - parser.add_option("--dnssec-master", dest="dnssec_master", action="store_true", - default=False, help="Setup server to be DNSSEC key master") - parser.add_option("--zonemgr", action="callback", callback=bindinstance.zonemgr_callback, - type="string", - help="DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN") - parser.add_option("-U", "--unattended", dest="unattended", action="store_true", - default=False, help="unattended installation never prompts the user") - parser.add_option("--disable-dnssec-master", dest="disable_dnssec_master", - action="store_true", default=False, help="Disable the " - "DNSSEC master on this server") - parser.add_option("--kasp-db", dest="kasp_db_file", type="string", - metavar="FILE", action="store", help="Copy OpenDNSSEC " - "metadata from the specified file (will not create a new " - "kasp.db file)") - parser.add_option("--force", dest="force", action="store_true", - help="Force install") - - options, _args = parser.parse_args() - safe_options = parser.get_safe_opts(options) - - if options.dnssec_master and options.disable_dnssec_master: - parser.error("Invalid combination of parameters: --dnssec-master and " - "--disable-dnssec-master") - - if options.forwarders and options.no_forwarders: - parser.error("You cannot specify a --forwarder option together with --no-forwarders") - elif options.reverse_zones and options.no_reverse: - parser.error("You cannot specify a --reverse-zone option together with --no-reverse") - elif options.auto_reverse and options.no_reverse: - parser.error("You cannot specify a --auto-reverse option together with --no-reverse") - - if options.unattended: - if (not options.forwarders - and not options.no_forwarders - and not options.auto_forwarders): - parser.error("You must specify at least one option: " - "--forwarder or --no-forwarders or --auto-forwarders") - - if options.kasp_db_file and not os.path.isfile(options.kasp_db_file): - parser.error("File %s does not exist" % options.kasp_db_file) - - return safe_options, options - -def main(): - safe_options, options = parse_options() - - if os.getegid() != 0: - sys.exit("Must be root to setup server") - - standard_logging_setup(log_file_name, debug=options.debug, filemode='a') - print("\nThe log file for this installation can be found in %s" % log_file_name) - - logger.debug('%s was invoked with options: %s', sys.argv[0], safe_options) - logger.debug("missing options might be asked for interactively later\n") - logger.debug('IPA version %s', version.VENDOR_VERSION) - - installutils.check_server_configuration() - - # Initialize the ipalib api - api.bootstrap( - context='install', confdir=paths.ETC_IPA, - in_server=True, debug=options.debug, - ) - api.finalize() - api.Backend.ldap2.connect() - - options.setup_ca = None # must be None to enable autodetection - - dns_installer.install_check(True, api, False, options, hostname=api.env.host) - dns_installer.install(True, False, options) - # Services are enabled in dns_installer.install() - - # execute ipactl to refresh services status - ipautil.run([paths.IPACTL, 'start', '--ignore-service-failures'], - raiseonerr=False) - - api.Backend.ldap2.disconnect() - - return 0 - -if __name__ == '__main__': - installutils.run_script(main, log_file_name=log_file_name, - operation_name='ipa-dns-install') diff --git a/install/tools/ipa-dns-install.in b/install/tools/ipa-dns-install.in new file mode 100644 index 0000000..99260fe --- /dev/null +++ b/install/tools/ipa-dns-install.in @@ -0,0 +1,156 @@ +@PYTHONSHEBANG@ +# Authors: Martin Nagy +# Based on ipa-server-install by Karl MacMillan +# +# Copyright (C) 2007 - 2009 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging +import os +import sys + +from ipaserver.install import bindinstance +from ipaserver.install import installutils +from ipapython import version +from ipalib import api +from ipaplatform.paths import paths +from ipapython import ipautil +from ipapython.config import IPAOptionParser +from ipapython.ipa_log_manager import standard_logging_setup + +from ipaserver.install import dns as dns_installer + +logger = logging.getLogger(os.path.basename(__file__)) + +log_file_name = paths.IPASERVER_INSTALL_LOG + +def parse_options(): + parser = IPAOptionParser(version=version.VERSION) + parser.add_option("-d", "--debug", dest="debug", action="store_true", + default=False, help="print debugging information") + parser.add_option("--ip-address", dest="ip_addresses", metavar="IP_ADDRESS", + default=[], action="append", + type="ip", + help="Master Server IP Address. This option can be used " + "multiple times") + parser.add_option("--forwarder", dest="forwarders", action="append", + type="ip_with_loopback", help="Add a DNS forwarder. This option can be used multiple times") + parser.add_option("--no-forwarders", dest="no_forwarders", action="store_true", + default=False, help="Do not add any DNS forwarders, use root servers instead") + parser.add_option("--auto-forwarders", dest="auto_forwarders", + action="store_true", default=False, + help="Use DNS forwarders configured in /etc/resolv.conf") + parser.add_option("--forward-policy", dest="forward_policy", + choices=("first", "only"), default=None, + help="DNS forwarding policy for global forwarders") + parser.add_option("--reverse-zone", dest="reverse_zones", + default=[], action="append", metavar="REVERSE_ZONE", + help="The reverse DNS zone to use. This option can be used multiple times") + parser.add_option("--no-reverse", dest="no_reverse", action="store_true", + default=False, help="Do not create new reverse DNS zone") + parser.add_option("--auto-reverse", dest="auto_reverse", action="store_true", + default=False, help="Create necessary DNS zones") + parser.add_option("--allow-zone-overlap", dest="allow_zone_overlap", + action="store_true", default=False, help="Create DNS " + "zone even if it already exists") + parser.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true", + default=False, help="Disable DNSSEC validation") + parser.add_option("--dnssec-master", dest="dnssec_master", action="store_true", + default=False, help="Setup server to be DNSSEC key master") + parser.add_option("--zonemgr", action="callback", callback=bindinstance.zonemgr_callback, + type="string", + help="DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN") + parser.add_option("-U", "--unattended", dest="unattended", action="store_true", + default=False, help="unattended installation never prompts the user") + parser.add_option("--disable-dnssec-master", dest="disable_dnssec_master", + action="store_true", default=False, help="Disable the " + "DNSSEC master on this server") + parser.add_option("--kasp-db", dest="kasp_db_file", type="string", + metavar="FILE", action="store", help="Copy OpenDNSSEC " + "metadata from the specified file (will not create a new " + "kasp.db file)") + parser.add_option("--force", dest="force", action="store_true", + help="Force install") + + options, _args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + if options.dnssec_master and options.disable_dnssec_master: + parser.error("Invalid combination of parameters: --dnssec-master and " + "--disable-dnssec-master") + + if options.forwarders and options.no_forwarders: + parser.error("You cannot specify a --forwarder option together with --no-forwarders") + elif options.reverse_zones and options.no_reverse: + parser.error("You cannot specify a --reverse-zone option together with --no-reverse") + elif options.auto_reverse and options.no_reverse: + parser.error("You cannot specify a --auto-reverse option together with --no-reverse") + + if options.unattended: + if (not options.forwarders + and not options.no_forwarders + and not options.auto_forwarders): + parser.error("You must specify at least one option: " + "--forwarder or --no-forwarders or --auto-forwarders") + + if options.kasp_db_file and not os.path.isfile(options.kasp_db_file): + parser.error("File %s does not exist" % options.kasp_db_file) + + return safe_options, options + +def main(): + safe_options, options = parse_options() + + if os.getegid() != 0: + sys.exit("Must be root to setup server") + + standard_logging_setup(log_file_name, debug=options.debug, filemode='a') + print("\nThe log file for this installation can be found in %s" % log_file_name) + + logger.debug('%s was invoked with options: %s', sys.argv[0], safe_options) + logger.debug("missing options might be asked for interactively later\n") + logger.debug('IPA version %s', version.VENDOR_VERSION) + + installutils.check_server_configuration() + + # Initialize the ipalib api + api.bootstrap( + context='install', confdir=paths.ETC_IPA, + in_server=True, debug=options.debug, + ) + api.finalize() + api.Backend.ldap2.connect() + + options.setup_ca = None # must be None to enable autodetection + + dns_installer.install_check(True, api, False, options, hostname=api.env.host) + dns_installer.install(True, False, options) + # Services are enabled in dns_installer.install() + + # execute ipactl to refresh services status + ipautil.run([paths.IPACTL, 'start', '--ignore-service-failures'], + raiseonerr=False) + + api.Backend.ldap2.disconnect() + + return 0 + +if __name__ == '__main__': + installutils.run_script(main, log_file_name=log_file_name, + operation_name='ipa-dns-install') diff --git a/install/tools/ipa-httpd-kdcproxy b/install/tools/ipa-httpd-kdcproxy deleted file mode 100755 index 5c65788..0000000 --- a/install/tools/ipa-httpd-kdcproxy +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/python3 -# Authors: -# Christian Heimes -# -# Copyright (C) 2015 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -"""ipa-httpd-kdproxy - -This script creates or removes the symlink from /etc/ipa/ipa-kdc-proxy.conf -to /etc/httpd/conf.d/. It's called from ExecStartPre hook in httpd.service. -""" -import logging -import os -import socket -import sys - -from ipalib import api, errors -from ipapython.ipa_log_manager import standard_logging_setup -from ipapython.ipaldap import LDAPClient -from ipapython.dn import DN -from ipaplatform.paths import paths - -logger = logging.getLogger(os.path.basename(__file__)) - - -DEBUG = False -TIME_LIMIT = 2 - - -class Error(Exception): - """Base error class""" - - -class ConfigFileError(Error): - """Something is wrong with the config file""" - - -class CheckError(Error): - """An unrecoverable error has occured - - The exit code is 0. - """ - - -class FatalError(Error): - """A fatal error has occured - - Fatal errors cause the command to exit with a non-null exit code. - """ - - -class KDCProxyConfig(object): - ipaconfig_flag = 'ipaKDCProxyEnabled' - - def __init__(self, time_limit=TIME_LIMIT): - self.time_limit = time_limit - self.con = None - self.ldap_uri = api.env.ldap_uri - self.kdc_dn = DN(('cn', 'KDC'), ('cn', api.env.host), - ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), - api.env.basedn) - self.conf = paths.HTTPD_IPA_KDCPROXY_CONF - self.conflink = paths.HTTPD_IPA_KDCPROXY_CONF_SYMLINK - - def _ldap_con(self): - """Establish LDAP connection""" - logger.debug('ldap_uri: %s', self.ldap_uri) - try: - self.con = LDAPClient(self.ldap_uri) - self.con.external_bind() - except (errors.NetworkError, socket.timeout) as e: - msg = 'Unable to connect to dirsrv: %s' % e - raise CheckError(msg) - except errors.AuthorizationError as e: - msg = 'Authorization error: %s' % e - raise CheckError(msg) - except Exception as e: - msg = ('Unknown error while retrieving setting from %s: %s' % - (self.ldap_uri, e)) - logger.exception('%s', msg) - raise FatalError(msg) - - def _find_entry(self, dn, attrs, filter, scope=LDAPClient.SCOPE_BASE): - """Find an LDAP entry, handles NotFound and Limit""" - try: - entries = self.con.get_entries( - dn, scope, filter, attrs, time_limit=self.time_limit) - except errors.NotFound: - logger.debug('Entry not found: %s', dn) - return None - except Exception as e: - msg = ('Unknown error while retrieving setting from %s: %s' % - (self.ldap_uri, e)) - logger.exception('%s', msg) - raise FatalError(msg) - return entries[0] - - def is_host_enabled(self): - """Check replica specific flag""" - logger.debug('Read settings from dn: %s', self.kdc_dn) - srcfilter = self.con.make_filter( - {'ipaConfigString': u'kdcProxyEnabled'} - ) - entry = self._find_entry(self.kdc_dn, ['cn'], srcfilter) - logger.debug('%s ipaConfigString: %s', self.kdc_dn, entry) - return entry is not None - - def validate_symlink(self): - """Validate symlink in Apache conf.d""" - if not os.path.exists(self.conflink): - return False - if not os.path.islink(self.conflink): - raise ConfigFileError( - "'%s' already exists, but it is not a symlink" - % self.conflink) - dest = os.readlink(self.conflink) - if dest != self.conf: - raise ConfigFileError( - "'%s' points to '%s', expected '%s'" - % (self.conflink, dest, self.conf)) - return True - - def create_symlink(self): - """Create symlink to enable KDC proxy support""" - try: - valid = self.validate_symlink() - except ConfigFileError as e: - logger.warning("Cannot enable KDC proxy: %s ", e) - return False - - if valid: - logger.debug("Symlink exists and is valid") - return True - - if not os.path.isfile(self.conf): - logger.warning("'%s' does not exist", self.conf) - return False - - # create the symbolic link - logger.debug("Creating symlink from '%s' to '%s'", - self.conf, self.conflink) - os.symlink(self.conf, self.conflink) - return True - - def remove_symlink(self): - """Delete symlink to disable KDC proxy support""" - try: - valid = self.validate_symlink() - except CheckError as e: - logger.warning("Cannot disable KDC proxy: %s ", e) - return False - - if valid: - logger.debug("Removing symlink '%s'", self.conflink) - os.unlink(self.conflink) - else: - logger.debug("Symlink '%s' has already been removed.", - self.conflink) - - return True - - def __enter__(self): - self._ldap_con() - return self - - def __exit__(self, exc_type, exc_value, traceback): - if self.con is not None: - self.con.unbind() - self.con = None - - -def main(debug=DEBUG, time_limit=TIME_LIMIT): - # initialize API without file logging - if not api.isdone('bootstrap'): - api.bootstrap(context='server', confdir=paths.ETC_IPA, - log=None, debug=debug) - standard_logging_setup(verbose=True, debug=debug) - - try: - cfg = KDCProxyConfig(time_limit) - with cfg: - if cfg.is_host_enabled(): - if cfg.create_symlink(): - logger.info('KDC proxy enabled') - return 0 - else: - if cfg.remove_symlink(): - logger.info('KDC proxy disabled') - return 0 - except CheckError as e: - logger.warning('%s', str(e)) - logger.warning('Disabling KDC proxy') - cfg.remove_symlink() - return 0 - except Exception as e: - logger.error('%s', str(e)) - return 1 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/install/tools/ipa-httpd-kdcproxy.in b/install/tools/ipa-httpd-kdcproxy.in new file mode 100644 index 0000000..6ce10c1 --- /dev/null +++ b/install/tools/ipa-httpd-kdcproxy.in @@ -0,0 +1,215 @@ +@PYTHONSHEBANG@ +# Authors: +# Christian Heimes +# +# Copyright (C) 2015 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +"""ipa-httpd-kdproxy + +This script creates or removes the symlink from /etc/ipa/ipa-kdc-proxy.conf +to /etc/httpd/conf.d/. It's called from ExecStartPre hook in httpd.service. +""" +import logging +import os +import socket +import sys + +from ipalib import api, errors +from ipapython.ipa_log_manager import standard_logging_setup +from ipapython.ipaldap import LDAPClient +from ipapython.dn import DN +from ipaplatform.paths import paths + +logger = logging.getLogger(os.path.basename(__file__)) + + +DEBUG = False +TIME_LIMIT = 2 + + +class Error(Exception): + """Base error class""" + + +class ConfigFileError(Error): + """Something is wrong with the config file""" + + +class CheckError(Error): + """An unrecoverable error has occured + + The exit code is 0. + """ + + +class FatalError(Error): + """A fatal error has occured + + Fatal errors cause the command to exit with a non-null exit code. + """ + + +class KDCProxyConfig(object): + ipaconfig_flag = 'ipaKDCProxyEnabled' + + def __init__(self, time_limit=TIME_LIMIT): + self.time_limit = time_limit + self.con = None + self.ldap_uri = api.env.ldap_uri + self.kdc_dn = DN(('cn', 'KDC'), ('cn', api.env.host), + ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), + api.env.basedn) + self.conf = paths.HTTPD_IPA_KDCPROXY_CONF + self.conflink = paths.HTTPD_IPA_KDCPROXY_CONF_SYMLINK + + def _ldap_con(self): + """Establish LDAP connection""" + logger.debug('ldap_uri: %s', self.ldap_uri) + try: + self.con = LDAPClient(self.ldap_uri) + self.con.external_bind() + except (errors.NetworkError, socket.timeout) as e: + msg = 'Unable to connect to dirsrv: %s' % e + raise CheckError(msg) + except errors.AuthorizationError as e: + msg = 'Authorization error: %s' % e + raise CheckError(msg) + except Exception as e: + msg = ('Unknown error while retrieving setting from %s: %s' % + (self.ldap_uri, e)) + logger.exception('%s', msg) + raise FatalError(msg) + + def _find_entry(self, dn, attrs, filter, scope=LDAPClient.SCOPE_BASE): + """Find an LDAP entry, handles NotFound and Limit""" + try: + entries = self.con.get_entries( + dn, scope, filter, attrs, time_limit=self.time_limit) + except errors.NotFound: + logger.debug('Entry not found: %s', dn) + return None + except Exception as e: + msg = ('Unknown error while retrieving setting from %s: %s' % + (self.ldap_uri, e)) + logger.exception('%s', msg) + raise FatalError(msg) + return entries[0] + + def is_host_enabled(self): + """Check replica specific flag""" + logger.debug('Read settings from dn: %s', self.kdc_dn) + srcfilter = self.con.make_filter( + {'ipaConfigString': u'kdcProxyEnabled'} + ) + entry = self._find_entry(self.kdc_dn, ['cn'], srcfilter) + logger.debug('%s ipaConfigString: %s', self.kdc_dn, entry) + return entry is not None + + def validate_symlink(self): + """Validate symlink in Apache conf.d""" + if not os.path.exists(self.conflink): + return False + if not os.path.islink(self.conflink): + raise ConfigFileError( + "'%s' already exists, but it is not a symlink" + % self.conflink) + dest = os.readlink(self.conflink) + if dest != self.conf: + raise ConfigFileError( + "'%s' points to '%s', expected '%s'" + % (self.conflink, dest, self.conf)) + return True + + def create_symlink(self): + """Create symlink to enable KDC proxy support""" + try: + valid = self.validate_symlink() + except ConfigFileError as e: + logger.warning("Cannot enable KDC proxy: %s ", e) + return False + + if valid: + logger.debug("Symlink exists and is valid") + return True + + if not os.path.isfile(self.conf): + logger.warning("'%s' does not exist", self.conf) + return False + + # create the symbolic link + logger.debug("Creating symlink from '%s' to '%s'", + self.conf, self.conflink) + os.symlink(self.conf, self.conflink) + return True + + def remove_symlink(self): + """Delete symlink to disable KDC proxy support""" + try: + valid = self.validate_symlink() + except CheckError as e: + logger.warning("Cannot disable KDC proxy: %s ", e) + return False + + if valid: + logger.debug("Removing symlink '%s'", self.conflink) + os.unlink(self.conflink) + else: + logger.debug("Symlink '%s' has already been removed.", + self.conflink) + + return True + + def __enter__(self): + self._ldap_con() + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.con is not None: + self.con.unbind() + self.con = None + + +def main(debug=DEBUG, time_limit=TIME_LIMIT): + # initialize API without file logging + if not api.isdone('bootstrap'): + api.bootstrap(context='server', confdir=paths.ETC_IPA, + log=None, debug=debug) + standard_logging_setup(verbose=True, debug=debug) + + try: + cfg = KDCProxyConfig(time_limit) + with cfg: + if cfg.is_host_enabled(): + if cfg.create_symlink(): + logger.info('KDC proxy enabled') + return 0 + else: + if cfg.remove_symlink(): + logger.info('KDC proxy disabled') + return 0 + except CheckError as e: + logger.warning('%s', str(e)) + logger.warning('Disabling KDC proxy') + cfg.remove_symlink() + return 0 + except Exception as e: + logger.error('%s', str(e)) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/install/tools/ipa-kra-install b/install/tools/ipa-kra-install deleted file mode 100755 index 29b7d60..0000000 --- a/install/tools/ipa-kra-install +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Ade Lee -# -# Copyright (C) 2014 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_kra_install import KRAInstall - -KRAInstall.run_cli() diff --git a/install/tools/ipa-kra-install.in b/install/tools/ipa-kra-install.in new file mode 100644 index 0000000..831bb0e --- /dev/null +++ b/install/tools/ipa-kra-install.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Ade Lee +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_kra_install import KRAInstall + +KRAInstall.run_cli() diff --git a/install/tools/ipa-ldap-updater b/install/tools/ipa-ldap-updater deleted file mode 100755 index b341781..0000000 --- a/install/tools/ipa-ldap-updater +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/python3 -# Authors: Rob Crittenden -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# Documentation can be found at http://freeipa.org/page/LdapUpdate - -from ipaserver.install.ipa_ldap_updater import LDAPUpdater - -LDAPUpdater.run_cli() diff --git a/install/tools/ipa-ldap-updater.in b/install/tools/ipa-ldap-updater.in new file mode 100644 index 0000000..ad54d56 --- /dev/null +++ b/install/tools/ipa-ldap-updater.in @@ -0,0 +1,25 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Documentation can be found at http://freeipa.org/page/LdapUpdate + +from ipaserver.install.ipa_ldap_updater import LDAPUpdater + +LDAPUpdater.run_cli() diff --git a/install/tools/ipa-managed-entries b/install/tools/ipa-managed-entries deleted file mode 100755 index e9be41b..0000000 --- a/install/tools/ipa-managed-entries +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/python3 -# Authors: Jr Aquino -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging -import os -import re -import sys -from optparse import OptionParser # pylint: disable=deprecated-module - -from ipaplatform.paths import paths -from ipapython import config -from ipaserver.install import installutils -from ipalib import api, errors -from ipapython.ipa_log_manager import standard_logging_setup -from ipapython.dn import DN - -logger = logging.getLogger(os.path.basename(__file__)) - - -def parse_options(): - usage = "%prog [options] \n" - usage += "%prog [options]\n" - parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information about the update(s)") - parser.add_option("-e", "--entry", dest="managed_entry", - default=None, type="string", - help="DN for the Managed Entry Definition") - parser.add_option("-l", "--list", dest="list_managed_entries", - action="store_true", - help="List available Managed Entries") - parser.add_option("-p", "--password", dest="dirman_password", - help="Directory Manager password") - - options, args = parser.parse_args() - - return options, args - -def get_dirman_password(): - """Prompt the user for the Directory Manager password and verify its - correctness. - """ - password = installutils.read_password("Directory Manager", confirm=False, - validate=True) - - return password - -def main(): - retval = 0 - def_dn = None - - installutils.check_server_configuration() - - options, args = parse_options() - - if options.list_managed_entries: - pass - elif len(args) != 1: - sys.exit("You must specify an action, either status, enable or disable") - elif args[0] != "enable" and args[0] != "disable" and args[0] != "status": - sys.exit("Unrecognized action [" + args[0] + "]") - standard_logging_setup(None, debug=options.debug) - - api.bootstrap( - context='cli', - in_server=True, - debug=options.debug, - confdir=paths.ETC_IPA) - api.finalize() - api.Backend.ldap2.connect(bind_pw=options.dirman_password) - - managed_entry_definitions_dn = DN( - ('cn', 'Definitions'), - ('cn', 'Managed Entries'), - ('cn', 'etc'), - api.env.basedn - ) - - filter = '(objectClass=extensibleObject)' - - if options.list_managed_entries: - # List available Managed Entry Plugins - managed_entries = None - try: - entries = api.Backend.ldap2.get_entries( - managed_entry_definitions_dn, api.Backend.ldap2.SCOPE_SUBTREE, filter) - except Exception as e: - logger.debug("Search for managed entries failed: %s", str(e)) - sys.exit("Unable to find managed entries at %s" % managed_entry_definitions_dn) - managed_entries = [entry.single_value['cn'] for entry in entries] - if managed_entries: - print("Available Managed Entry Definitions:") - for managed_entry in managed_entries: - print(managed_entry) - retval = 0 - sys.exit() - - if not options.managed_entry: - sys.exit("\nYou must specify a managed entry definition") - else: - def_dn = DN(('cn', options.managed_entry), managed_entry_definitions_dn) - - disabled = True - try: - entry = api.Backend.ldap2.get_entry(def_dn) - disable_attr = '(objectclass=disable)' - try: - org_filter = entry.single_value.get('originfilter') - disabled = re.search(r'%s' % disable_attr, org_filter) - except KeyError: - sys.exit("%s is not a valid Managed Entry" % def_dn) - except errors.NotFound: - sys.exit("%s is not a valid Managed Entry" % def_dn) - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - - if args[0] == "status": - if not disabled: - print("Plugin Enabled") - else: - print("Plugin Disabled") - return 0 - - if args[0] == "enable": - try: - if not disabled: - print("Plugin already Enabled") - retval = 2 - else: - # Remove disable_attr from filter - enable_attr = org_filter.replace(disable_attr, '') - #enable_attr = {'originfilter': enable_attr} - entry['originfilter'] = [enable_attr] - api.Backend.ldap2.update_entry(entry) - print("Enabling Plugin") - retval = 0 - except errors.NotFound: - print("Enabling Plugin") - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - elif args[0] == "disable": - # Set originFilter to objectclass=disabled - # In future we should we should dedicate an attribute for enabling/ - # disabling. - try: - if disabled: - print("Plugin already disabled") - retval = 2 - else: - if org_filter[:2] == '(&' and org_filter[-1] == ')': - disable_attr = org_filter[:2] + disable_attr + org_filter[2:] - else: - disable_attr = '(&%s(%s))' % (disable_attr, org_filter) - entry['originfilter'] = [disable_attr] - api.Backend.ldap2.update_entry(entry) - print("Disabling Plugin") - except errors.NotFound: - print("Plugin is already disabled") - retval = 2 - except errors.DatabaseError as dbe: - print("An error occurred while talking to the server.") - print(dbe) - retval = 1 - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - else: - retval = 1 - - api.Backend.ldap2.disconnect() - - return retval - -if __name__ == '__main__': - if not os.geteuid() == 0: - sys.exit("\nMust be run as root\n") - installutils.run_script(main, operation_name='ipa-managed-entries') diff --git a/install/tools/ipa-managed-entries.in b/install/tools/ipa-managed-entries.in new file mode 100644 index 0000000..d0f06bf --- /dev/null +++ b/install/tools/ipa-managed-entries.in @@ -0,0 +1,203 @@ +@PYTHONSHEBANG@ +# Authors: Jr Aquino +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging +import os +import re +import sys +from optparse import OptionParser # pylint: disable=deprecated-module + +from ipaplatform.paths import paths +from ipapython import config +from ipaserver.install import installutils +from ipalib import api, errors +from ipapython.ipa_log_manager import standard_logging_setup +from ipapython.dn import DN + +logger = logging.getLogger(os.path.basename(__file__)) + + +def parse_options(): + usage = "%prog [options] \n" + usage += "%prog [options]\n" + parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information about the update(s)") + parser.add_option("-e", "--entry", dest="managed_entry", + default=None, type="string", + help="DN for the Managed Entry Definition") + parser.add_option("-l", "--list", dest="list_managed_entries", + action="store_true", + help="List available Managed Entries") + parser.add_option("-p", "--password", dest="dirman_password", + help="Directory Manager password") + + options, args = parser.parse_args() + + return options, args + +def get_dirman_password(): + """Prompt the user for the Directory Manager password and verify its + correctness. + """ + password = installutils.read_password("Directory Manager", confirm=False, + validate=True) + + return password + +def main(): + retval = 0 + def_dn = None + + installutils.check_server_configuration() + + options, args = parse_options() + + if options.list_managed_entries: + pass + elif len(args) != 1: + sys.exit("You must specify an action, either status, enable or disable") + elif args[0] != "enable" and args[0] != "disable" and args[0] != "status": + sys.exit("Unrecognized action [" + args[0] + "]") + standard_logging_setup(None, debug=options.debug) + + api.bootstrap( + context='cli', + in_server=True, + debug=options.debug, + confdir=paths.ETC_IPA) + api.finalize() + api.Backend.ldap2.connect(bind_pw=options.dirman_password) + + managed_entry_definitions_dn = DN( + ('cn', 'Definitions'), + ('cn', 'Managed Entries'), + ('cn', 'etc'), + api.env.basedn + ) + + filter = '(objectClass=extensibleObject)' + + if options.list_managed_entries: + # List available Managed Entry Plugins + managed_entries = None + try: + entries = api.Backend.ldap2.get_entries( + managed_entry_definitions_dn, api.Backend.ldap2.SCOPE_SUBTREE, filter) + except Exception as e: + logger.debug("Search for managed entries failed: %s", str(e)) + sys.exit("Unable to find managed entries at %s" % managed_entry_definitions_dn) + managed_entries = [entry.single_value['cn'] for entry in entries] + if managed_entries: + print("Available Managed Entry Definitions:") + for managed_entry in managed_entries: + print(managed_entry) + retval = 0 + sys.exit() + + if not options.managed_entry: + sys.exit("\nYou must specify a managed entry definition") + else: + def_dn = DN(('cn', options.managed_entry), managed_entry_definitions_dn) + + disabled = True + try: + entry = api.Backend.ldap2.get_entry(def_dn) + disable_attr = '(objectclass=disable)' + try: + org_filter = entry.single_value.get('originfilter') + disabled = re.search(r'%s' % disable_attr, org_filter) + except KeyError: + sys.exit("%s is not a valid Managed Entry" % def_dn) + except errors.NotFound: + sys.exit("%s is not a valid Managed Entry" % def_dn) + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + + if args[0] == "status": + if not disabled: + print("Plugin Enabled") + else: + print("Plugin Disabled") + return 0 + + if args[0] == "enable": + try: + if not disabled: + print("Plugin already Enabled") + retval = 2 + else: + # Remove disable_attr from filter + enable_attr = org_filter.replace(disable_attr, '') + #enable_attr = {'originfilter': enable_attr} + entry['originfilter'] = [enable_attr] + api.Backend.ldap2.update_entry(entry) + print("Enabling Plugin") + retval = 0 + except errors.NotFound: + print("Enabling Plugin") + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + elif args[0] == "disable": + # Set originFilter to objectclass=disabled + # In future we should we should dedicate an attribute for enabling/ + # disabling. + try: + if disabled: + print("Plugin already disabled") + retval = 2 + else: + if org_filter[:2] == '(&' and org_filter[-1] == ')': + disable_attr = org_filter[:2] + disable_attr + org_filter[2:] + else: + disable_attr = '(&%s(%s))' % (disable_attr, org_filter) + entry['originfilter'] = [disable_attr] + api.Backend.ldap2.update_entry(entry) + print("Disabling Plugin") + except errors.NotFound: + print("Plugin is already disabled") + retval = 2 + except errors.DatabaseError as dbe: + print("An error occurred while talking to the server.") + print(dbe) + retval = 1 + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + else: + retval = 1 + + api.Backend.ldap2.disconnect() + + return retval + +if __name__ == '__main__': + if not os.geteuid() == 0: + sys.exit("\nMust be run as root\n") + installutils.run_script(main, operation_name='ipa-managed-entries') diff --git a/install/tools/ipa-nis-manage b/install/tools/ipa-nis-manage deleted file mode 100755 index dbac9cb..0000000 --- a/install/tools/ipa-nis-manage +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/python3 -# Authors: Rob Crittenden -# Authors: Simo Sorce -# -# Copyright (C) 2009 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import sys -import os -from ipaplatform.paths import paths -try: - from optparse import OptionParser # pylint: disable=deprecated-module - from ipapython import ipautil, config - from ipaserver.install import installutils - from ipaserver.install.ldapupdate import LDAPUpdate - from ipalib import api, errors - from ipapython.ipa_log_manager import standard_logging_setup - from ipapython.dn import DN - from ipaplatform import services -except ImportError as e: - print("""\ -There was a problem importing one of the required Python modules. The -error was: - - %s -""" % e, file=sys.stderr) - sys.exit(1) - -nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config')) -compat_dn = DN(('cn', 'Schema Compatibility'), ('cn', 'plugins'), ('cn', 'config')) - -def parse_options(): - usage = "%prog [options] \n" - usage += "%prog [options]\n" - parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information about the update(s)") - parser.add_option("-y", dest="password", - help="File containing the Directory Manager password") - - config.add_standard_options(parser) - options, args = parser.parse_args() - - return options, args - -def get_dirman_password(): - """Prompt the user for the Directory Manager password and verify its - correctness. - """ - password = installutils.read_password("Directory Manager", confirm=False, validate=False, retry=False) - - return password - -def get_entry(dn): - """ - Return the entry for the given DN. If the entry is not found return - None. - """ - entry = None - try: - entry = api.Backend.ldap2.get_entry(dn) - except errors.NotFound: - pass - return entry - -def main(): - retval = 0 - files = [paths.NIS_ULDIF] - servicemsg = "" - - if os.getegid() != 0: - sys.exit('Must be root to use this tool.') - - installutils.check_server_configuration() - - options, args = parse_options() - - if len(args) != 1: - sys.exit("You must specify one action: enable | disable | status") - elif args[0] not in {"enable", "disable", "status"}: - sys.exit("Unrecognized action [" + args[0] + "]") - - standard_logging_setup(None, debug=options.debug) - dirman_password = "" - if options.password: - try: - pw = ipautil.template_file(options.password, []) - except IOError: - sys.exit("File \"%s\" not found or not readable" % options.password) - dirman_password = pw.strip() - else: - dirman_password = get_dirman_password() - if dirman_password is None: - sys.exit("Directory Manager password required") - - if not dirman_password: - sys.exit("No password supplied") - - api.bootstrap( - context='cli', confdir=paths.ETC_IPA, - debug=options.debug, in_server=True) - api.finalize() - api.Backend.ldap2.connect(bind_pw=dirman_password) - - if args[0] == "enable": - compat = get_entry(compat_dn) - if compat is None or compat.get('nsslapd-pluginenabled', [''])[0].lower() == 'off': - sys.exit("The compat plugin needs to be enabled: ipa-compat-manage enable") - entry = None - try: - entry = get_entry(nis_config_dn) - except errors.ExecutionError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - # Enable either the portmap or rpcbind service - portmap = services.knownservices.portmap - rpcbind = services.knownservices.rpcbind - - if portmap.is_installed(): - portmap.enable() - servicemsg = portmap.service_name - elif rpcbind.is_installed(): - rpcbind.enable() - servicemsg = rpcbind.service_name - else: - print("Unable to enable either %s or %s" % (portmap.service_name, rpcbind.service_name)) - retval = 3 - - # The cn=config entry for the plugin may already exist but it - # could be turned off, handle both cases. - if entry is None: - print("Enabling plugin") - ld = LDAPUpdate(dm_password=dirman_password, sub_dict={}, ldapi=True) - if ld.update(files) != True: - retval = 1 - elif entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'off': - print("Enabling plugin") - # Already configured, just enable the plugin - entry['nsslapd-pluginenabled'] = ['on'] - api.Backend.ldap2.update_entry(entry) - else: - print("Plugin already Enabled") - retval = 2 - - elif args[0] == "disable": - try: - entry = api.Backend.ldap2.get_entry(nis_config_dn, ['nsslapd-pluginenabled']) - entry['nsslapd-pluginenabled'] = ['off'] - api.Backend.ldap2.update_entry(entry) - except (errors.NotFound, errors.EmptyModlist): - print("Plugin is already disabled") - retval = 2 - except errors.LDAPError as lde: - print("An error occurred while talking to the server.") - print(lde) - retval = 1 - - elif args[0] == "status": - nis_entry = get_entry(nis_config_dn) - enabled = (nis_entry and - nis_entry.get( - 'nsslapd-pluginenabled', '')[0].lower() == "on") - if enabled: - print("Plugin is enabled") - retval = 0 - else: - print("Plugin is not enabled") - retval = 4 - - else: - retval = 1 - - if retval == 0: - if args[0] in {"enable", "disable"}: - print("This setting will not take effect until you restart " - "Directory Server.") - - if args[0] == "enable": - print("The %s service may need to be started." % servicemsg) - - api.Backend.ldap2.disconnect() - - return retval - -if __name__ == '__main__': - installutils.run_script(main, operation_name='ipa-nis-manage') diff --git a/install/tools/ipa-nis-manage.in b/install/tools/ipa-nis-manage.in new file mode 100644 index 0000000..ac53744 --- /dev/null +++ b/install/tools/ipa-nis-manage.in @@ -0,0 +1,205 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# Authors: Simo Sorce +# +# Copyright (C) 2009 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import sys +import os +from ipaplatform.paths import paths +try: + from optparse import OptionParser # pylint: disable=deprecated-module + from ipapython import ipautil, config + from ipaserver.install import installutils + from ipaserver.install.ldapupdate import LDAPUpdate + from ipalib import api, errors + from ipapython.ipa_log_manager import standard_logging_setup + from ipapython.dn import DN + from ipaplatform import services +except ImportError as e: + print("""\ +There was a problem importing one of the required Python modules. The +error was: + + %s +""" % e, file=sys.stderr) + sys.exit(1) + +nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config')) +compat_dn = DN(('cn', 'Schema Compatibility'), ('cn', 'plugins'), ('cn', 'config')) + +def parse_options(): + usage = "%prog [options] \n" + usage += "%prog [options]\n" + parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information about the update(s)") + parser.add_option("-y", dest="password", + help="File containing the Directory Manager password") + + config.add_standard_options(parser) + options, args = parser.parse_args() + + return options, args + +def get_dirman_password(): + """Prompt the user for the Directory Manager password and verify its + correctness. + """ + password = installutils.read_password("Directory Manager", confirm=False, validate=False, retry=False) + + return password + +def get_entry(dn): + """ + Return the entry for the given DN. If the entry is not found return + None. + """ + entry = None + try: + entry = api.Backend.ldap2.get_entry(dn) + except errors.NotFound: + pass + return entry + +def main(): + retval = 0 + files = [paths.NIS_ULDIF] + servicemsg = "" + + if os.getegid() != 0: + sys.exit('Must be root to use this tool.') + + installutils.check_server_configuration() + + options, args = parse_options() + + if len(args) != 1: + sys.exit("You must specify one action: enable | disable | status") + elif args[0] not in {"enable", "disable", "status"}: + sys.exit("Unrecognized action [" + args[0] + "]") + + standard_logging_setup(None, debug=options.debug) + dirman_password = "" + if options.password: + try: + pw = ipautil.template_file(options.password, []) + except IOError: + sys.exit("File \"%s\" not found or not readable" % options.password) + dirman_password = pw.strip() + else: + dirman_password = get_dirman_password() + if dirman_password is None: + sys.exit("Directory Manager password required") + + if not dirman_password: + sys.exit("No password supplied") + + api.bootstrap( + context='cli', confdir=paths.ETC_IPA, + debug=options.debug, in_server=True) + api.finalize() + api.Backend.ldap2.connect(bind_pw=dirman_password) + + if args[0] == "enable": + compat = get_entry(compat_dn) + if compat is None or compat.get('nsslapd-pluginenabled', [''])[0].lower() == 'off': + sys.exit("The compat plugin needs to be enabled: ipa-compat-manage enable") + entry = None + try: + entry = get_entry(nis_config_dn) + except errors.ExecutionError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + # Enable either the portmap or rpcbind service + portmap = services.knownservices.portmap + rpcbind = services.knownservices.rpcbind + + if portmap.is_installed(): + portmap.enable() + servicemsg = portmap.service_name + elif rpcbind.is_installed(): + rpcbind.enable() + servicemsg = rpcbind.service_name + else: + print("Unable to enable either %s or %s" % (portmap.service_name, rpcbind.service_name)) + retval = 3 + + # The cn=config entry for the plugin may already exist but it + # could be turned off, handle both cases. + if entry is None: + print("Enabling plugin") + ld = LDAPUpdate(dm_password=dirman_password, sub_dict={}, ldapi=True) + if ld.update(files) != True: + retval = 1 + elif entry.get('nsslapd-pluginenabled', [''])[0].lower() == 'off': + print("Enabling plugin") + # Already configured, just enable the plugin + entry['nsslapd-pluginenabled'] = ['on'] + api.Backend.ldap2.update_entry(entry) + else: + print("Plugin already Enabled") + retval = 2 + + elif args[0] == "disable": + try: + entry = api.Backend.ldap2.get_entry(nis_config_dn, ['nsslapd-pluginenabled']) + entry['nsslapd-pluginenabled'] = ['off'] + api.Backend.ldap2.update_entry(entry) + except (errors.NotFound, errors.EmptyModlist): + print("Plugin is already disabled") + retval = 2 + except errors.LDAPError as lde: + print("An error occurred while talking to the server.") + print(lde) + retval = 1 + + elif args[0] == "status": + nis_entry = get_entry(nis_config_dn) + enabled = (nis_entry and + nis_entry.get( + 'nsslapd-pluginenabled', '')[0].lower() == "on") + if enabled: + print("Plugin is enabled") + retval = 0 + else: + print("Plugin is not enabled") + retval = 4 + + else: + retval = 1 + + if retval == 0: + if args[0] in {"enable", "disable"}: + print("This setting will not take effect until you restart " + "Directory Server.") + + if args[0] == "enable": + print("The %s service may need to be started." % servicemsg) + + api.Backend.ldap2.disconnect() + + return retval + +if __name__ == '__main__': + installutils.run_script(main, operation_name='ipa-nis-manage') diff --git a/install/tools/ipa-otptoken-import b/install/tools/ipa-otptoken-import deleted file mode 100755 index c0076f1..0000000 --- a/install/tools/ipa-otptoken-import +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Nathaniel McCallum -# -# Copyright (C) 2014 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_otptoken_import import OTPTokenImport - -OTPTokenImport.run_cli() diff --git a/install/tools/ipa-otptoken-import.in b/install/tools/ipa-otptoken-import.in new file mode 100644 index 0000000..ef29e0a --- /dev/null +++ b/install/tools/ipa-otptoken-import.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Nathaniel McCallum +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_otptoken_import import OTPTokenImport + +OTPTokenImport.run_cli() diff --git a/install/tools/ipa-pki-retrieve-key b/install/tools/ipa-pki-retrieve-key deleted file mode 100755 index 95106c7..0000000 --- a/install/tools/ipa-pki-retrieve-key +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/python3 - -from __future__ import print_function - -import os -import sys -import traceback - -from ipalib import constants -from ipalib.config import Env -from ipaplatform.paths import paths -from ipaserver.secrets.client import CustodiaClient - - -def main(): - env = Env() - env._finalize() - - keyname = "ca_wrapped/" + sys.argv[1] - servername = sys.argv[2] - - service = constants.PKI_GSSAPI_SERVICE_NAME - client_keyfile = os.path.join(paths.PKI_TOMCAT, service + '.keys') - client_keytab = os.path.join(paths.PKI_TOMCAT, service + '.keytab') - - # pylint: disable=no-member - client = CustodiaClient( - client_service='%s@%s' % (service, env.host), server=servername, - realm=env.realm, ldap_uri="ldaps://" + env.host, - keyfile=client_keyfile, keytab=client_keytab, - ) - - # Print the response JSON to stdout; it is already in the format - # that Dogtag's ExternalProcessKeyRetriever expects - print(client.fetch_key(keyname, store=False)) - - -try: - main() -except BaseException: - traceback.print_exc() - sys.exit(1) diff --git a/install/tools/ipa-pki-retrieve-key.in b/install/tools/ipa-pki-retrieve-key.in new file mode 100644 index 0000000..d9f1ad3 --- /dev/null +++ b/install/tools/ipa-pki-retrieve-key.in @@ -0,0 +1,42 @@ +@PYTHONSHEBANG@ + +from __future__ import print_function + +import os +import sys +import traceback + +from ipalib import constants +from ipalib.config import Env +from ipaplatform.paths import paths +from ipaserver.secrets.client import CustodiaClient + + +def main(): + env = Env() + env._finalize() + + keyname = "ca_wrapped/" + sys.argv[1] + servername = sys.argv[2] + + service = constants.PKI_GSSAPI_SERVICE_NAME + client_keyfile = os.path.join(paths.PKI_TOMCAT, service + '.keys') + client_keytab = os.path.join(paths.PKI_TOMCAT, service + '.keytab') + + # pylint: disable=no-member + client = CustodiaClient( + client_service='%s@%s' % (service, env.host), server=servername, + realm=env.realm, ldap_uri="ldaps://" + env.host, + keyfile=client_keyfile, keytab=client_keytab, + ) + + # Print the response JSON to stdout; it is already in the format + # that Dogtag's ExternalProcessKeyRetriever expects + print(client.fetch_key(keyname, store=False)) + + +try: + main() +except BaseException: + traceback.print_exc() + sys.exit(1) diff --git a/install/tools/ipa-pkinit-manage b/install/tools/ipa-pkinit-manage deleted file mode 100755 index 0630c6e..0000000 --- a/install/tools/ipa-pkinit-manage +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/python3 -E -# -# Copyright (C) 2017 FreeIPA Contributors see COPYING for license -# - -from ipaserver.install.ipa_pkinit_manage import PKINITManage - -PKINITManage.run_cli() diff --git a/install/tools/ipa-pkinit-manage.in b/install/tools/ipa-pkinit-manage.in new file mode 100644 index 0000000..44493a3 --- /dev/null +++ b/install/tools/ipa-pkinit-manage.in @@ -0,0 +1,8 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# + +from ipaserver.install.ipa_pkinit_manage import PKINITManage + +PKINITManage.run_cli() diff --git a/install/tools/ipa-replica-conncheck b/install/tools/ipa-replica-conncheck deleted file mode 100755 index cc21cb7..0000000 --- a/install/tools/ipa-replica-conncheck +++ /dev/null @@ -1,660 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Martin Kosek -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging - -import ipaclient.install.ipachangeconf -from ipapython.config import IPAOptionParser -from ipapython.dn import DN -from ipapython import version -from ipapython import ipautil, certdb -from ipalib import api, errors, x509 -from ipaserver.install import installutils -# pylint: disable=deprecated-module -from optparse import OptionGroup, OptionValueError -# pylint: enable=deprecated-module -from ipapython.ipa_log_manager import standard_logging_setup -import copy -import sys -import os -import signal -import tempfile -import select -import socket -import time -import threading -import traceback -from socket import SOCK_STREAM, SOCK_DGRAM -import distutils.spawn -from ipaplatform.paths import paths -import gssapi - -logger = logging.getLogger(os.path.basename(__file__)) - -CONNECT_TIMEOUT = 5 -RESPONDER = None -QUIET = False -CCACHE_FILE = None -KRB5_CONFIG = None - - -class SshExec(object): - def __init__(self, user, addr): - self.user = user - self.addr = addr - self.cmd = distutils.spawn.find_executable('ssh') - # Bail if ssh is not installed - if self.cmd is None: - raise RuntimeError("ssh not installed") - - def __call__(self, command, verbose=False): - - tmpf = tempfile.NamedTemporaryFile() - cmd = [ - self.cmd, - '-o StrictHostKeychecking=no', - '-o UserKnownHostsFile=%s' % tmpf.name, - '-o GSSAPIAuthentication=yes', - '-o User=%s' % self.user, - '%s' % self.addr, - command - ] - if verbose: - cmd.insert(1, '-v') - - env = dict() - if KRB5_CONFIG is not None: - env['KRB5_CONFIG'] = KRB5_CONFIG - elif 'KRB5_CONFIG' in os.environ: - env['KRB5_CONFIG'] = os.environ['KRB5_CONFIG'] - if CCACHE_FILE is not None: - env['KRB5CCNAME'] = CCACHE_FILE - elif 'KRB5CCNAME' in os.environ: - env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] - - return ipautil.run(cmd, env=env, raiseonerr=False, - capture_output=True, capture_error=True) - - -class CheckedPort(object): - def __init__(self, port, port_type, description): - self.port = port - self.port_type = port_type - self.description = description - - -BASE_PORTS = [ - CheckedPort(389, SOCK_STREAM, "Directory Service: Unsecure port"), - CheckedPort(636, SOCK_STREAM, "Directory Service: Secure port"), - CheckedPort(88, SOCK_STREAM, "Kerberos KDC: TCP"), - CheckedPort(88, SOCK_DGRAM, "Kerberos KDC: UDP"), - CheckedPort(464, SOCK_STREAM, "Kerberos Kpasswd: TCP"), - CheckedPort(464, SOCK_DGRAM, "Kerberos Kpasswd: UDP"), - CheckedPort(80, SOCK_STREAM, "HTTP Server: Unsecure port"), - CheckedPort(443, SOCK_STREAM, "HTTP Server: Secure port"), - ] - - -def parse_options(): - def ca_cert_file_callback(option, opt, value, parser): - if not os.path.exists(value): - raise OptionValueError( - "%s option '%s' does not exist" % (opt, value)) - if not os.path.isfile(value): - raise OptionValueError( - "%s option '%s' is not a file" % (opt, value)) - if not os.path.isabs(value): - raise OptionValueError( - "%s option '%s' is not an absolute file path" % (opt, value)) - - try: - x509.load_certificate_list_from_file(value) - except Exception: - raise OptionValueError( - "%s option '%s' is not a valid certificate file" % - (opt, value)) - - parser.values.ca_cert_file = value - - parser = IPAOptionParser(version=version.VERSION) - - replica_group = OptionGroup(parser, "on-replica options") - replica_group.add_option("-m", "--master", dest="master", - help="Master address with running IPA for output connection check") - replica_group.add_option("-a", "--auto-master-check", dest="auto_master_check", - action="store_true", - default=False, - help="Automatically execute connection check on master") - replica_group.add_option("-r", "--realm", dest="realm", - help="Realm name") - replica_group.add_option("-k", "--kdc", dest="kdc", - help="Master KDC. Defaults to master address") - replica_group.add_option("-p", "--principal", dest="principal", - default=None, help="Principal to use to log in to remote master") - replica_group.add_option("-w", "--password", dest="password", sensitive=True, - help="Password for the principal") - replica_group.add_option("--ca-cert-file", dest="ca_cert_file", - type="string", action="callback", - callback=ca_cert_file_callback, - help="load the CA certificate from this file") - parser.add_option_group(replica_group) - - - master_group = OptionGroup(parser, "on-master options") - master_group.add_option("-R", "--replica", dest="replica", - help="Address of remote replica machine to check against") - parser.add_option_group(master_group) - - common_group = OptionGroup(parser, "common options") - common_group.add_option("-c", "--check-ca", dest="check_ca", - action="store_true", - default=False, - help="Check also ports for Certificate Authority " - "(for servers installed before IPA 3.1)") - - common_group.add_option("", "--hostname", dest="hostname", - help="The hostname of this server (FQDN). " - "By default the result of getfqdn() call from " - "Python's socket module is used.") - parser.add_option_group(common_group) - - parser.add_option("-d", "--debug", dest="debug", - action="store_true", - default=False, help="Print debugging information") - parser.add_option("-q", "--quiet", dest="quiet", - action="store_true", - default=False, help="Output only errors") - parser.add_option("--no-log", dest="log_to_file", action="store_false", - default=True, help="Do not log into file") - - options, _args = parser.parse_args() - safe_options = parser.get_safe_opts(options) - - if options.master and options.replica: - parser.error("on-master and on-replica options are mutually exclusive!") - - if options.master: - if options.auto_master_check and not options.realm: - parser.error("Realm is parameter is required to connect to remote master!") - if not os.getegid() == 0: - parser.error("You can only run on-replica part as root.") - - if options.master and not options.kdc: - options.kdc = options.master - - if not options.master and not options.replica: - parser.error("No action: you should select either --replica or --master option.") - - if not options.hostname: - options.hostname = socket.getfqdn() - - return safe_options, options - - -def logging_setup(options): - log_file = None - - if os.getegid() == 0 and options.log_to_file: - log_file = paths.IPAREPLICA_CONNCHECK_LOG - - standard_logging_setup(log_file, verbose=(not options.quiet), - debug=options.debug, console_format='%(message)s') - - -def sigterm_handler(signum, frame): - # do what SIGINT does (raise a KeyboardInterrupt) - sigint_handler = signal.getsignal(signal.SIGINT) - if callable(sigint_handler): - sigint_handler(signum, frame) - - -def configure_krb5_conf(realm, kdc, filename): - - krbconf = ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer") - krbconf.setOptionAssignment((" = ", " ")) - krbconf.setSectionNameDelimiters(("[","]")) - krbconf.setSubSectionDelimiters(("{","}")) - krbconf.setIndent((""," "," ")) - - opts = [{'name':'comment', 'type':'comment', 'value':'File created by ipa-replica-conncheck'}, - {'name':'empty', 'type':'empty'}] - - #[libdefaults] - libdefaults = [{'name':'default_realm', 'type':'option', 'value':realm}] - libdefaults.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'}) - libdefaults.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'true'}) - libdefaults.append({'name':'rdns', 'type':'option', 'value':'false'}) - libdefaults.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'}) - libdefaults.append({'name':'forwardable', 'type':'option', 'value':'true'}) - libdefaults.append({'name':'udp_preference_limit', 'type':'option', 'value':'0'}) - - opts.append({'name':'libdefaults', 'type':'section', 'value': libdefaults}) - opts.append({'name':'empty', 'type':'empty'}) - - #the following are necessary only if DNS discovery does not work - #[realms] - realms_info =[{'name':'kdc', 'type':'option', 'value':ipautil.format_netloc(kdc, 88)}, - {'name':'master_kdc', 'type':'option', 'value':ipautil.format_netloc(kdc, 88)}, - {'name':'admin_server', 'type':'option', 'value':ipautil.format_netloc(kdc, 749)}] - realms = [{'name':realm, 'type':'subsection', 'value':realms_info}] - - opts.append({'name':'realms', 'type':'section', 'value':realms}) - opts.append({'name':'empty', 'type':'empty'}) - - #[appdefaults] - pamopts = [{'name':'debug', 'type':'option', 'value':'false'}, - {'name':'ticket_lifetime', 'type':'option', 'value':'36000'}, - {'name':'renew_lifetime', 'type':'option', 'value':'36000'}, - {'name':'forwardable', 'type':'option', 'value':'true'}, - {'name':'krb4_convert', 'type':'option', 'value':'false'}] - appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}] - opts.append({'name':'appdefaults', 'type':'section', 'value':appopts}) - - logger.debug("Writing temporary Kerberos configuration to %s:\n%s", - filename, krbconf.dump(opts)) - - krbconf.newConf(filename, opts) - - -class PortResponder(threading.Thread): - - PROTO = {socket.SOCK_STREAM: 'tcp', - socket.SOCK_DGRAM: 'udp'} - - def __init__(self, ports): - """ - ports: a list of CheckedPort - """ - super(PortResponder, self).__init__() - # copy ports to avoid the need to synchronize it between threads - self.ports = copy.deepcopy(ports) - self._sockets = [] - self._close = False - self._close_lock = threading.Lock() - self.responder_data = b'FreeIPA' - self.ports_opened = False - self.ports_open_cond = threading.Condition() - - def run(self): - logger.debug('Starting listening thread.') - - for port in self.ports: - self._bind_to_port(port.port, port.port_type) - with self.ports_open_cond: - self.ports_opened = True - logger.debug('Ports opened, notify original thread') - self.ports_open_cond.notify() - - while not self._is_closing(): - ready_socks, _socks1, _socks2 = select.select( - self._sockets, [], [], 1) - if ready_socks: - ready_sock = ready_socks[0] - self._respond(ready_sock) - - for sock in self._sockets: - port = sock.getsockname()[1] - proto = PortResponder.PROTO[sock.type] - sock.close() - logger.debug('%d %s: Stopped listening', port, proto) - - def _is_closing(self): - with self._close_lock: # pylint: disable=not-context-manager - return self._close - - def _bind_to_port(self, port, socket_type): - # Use IPv6 socket as it is able to accept both IPv6 and IPv4 - # connections. Since IPv6 kernel module is required by other - # parts of IPA, it should always be available. - family = socket.AF_INET6 - host = '::' # all available interfaces - proto = PortResponder.PROTO[socket_type] - - try: - sock = socket.socket(family, socket_type) - - # Make sure IPv4 clients can connect to IPv6 socket - sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - - if socket_type == socket.SOCK_STREAM: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - sock.bind((host, port)) - if socket_type == socket.SOCK_STREAM: - # There might be a delay before accepting the connection, - # because a single thread is used to handle all the - # connections. Thus a backlog size of at least 1 is needed. - sock.listen(1) - - logger.debug('%d %s: Started listening', port, proto) - except socket.error: - logger.warning('%d %s: Failed to bind', port, proto) - logger.debug("%s", traceback.format_exc()) - else: - self._sockets.append(sock) - - def _respond(self, sock): - port = sock.getsockname()[1] - if sock.type == socket.SOCK_STREAM: - connection, addr = sock.accept() - try: - connection.sendall(self.responder_data) - logger.debug('%d tcp: Responded to %s', port, addr[0]) - finally: - connection.close() - elif sock.type == socket.SOCK_DGRAM: - _data, addr = sock.recvfrom(1) - sock.sendto(self.responder_data, addr) - logger.debug('%d udp: Responded to %s', port, addr[0]) - - def stop(self): - logger.debug('Stopping listening thread.') - - with self._close_lock: # pylint: disable=not-context-manager - self._close = True - - -def port_check(host, port_list): - ports_failed = [] - ports_udp_warning = [] # conncheck could not verify that port is open - log_level = { - SOCK_DGRAM: logging.WARNING, - SOCK_STREAM: logging.ERROR - } - for port in port_list: - try: - port_open = ipautil.host_port_open( - host, port.port, port.port_type, - socket_timeout=CONNECT_TIMEOUT, log_errors=True, - log_level=log_level[port.port_type]) - except socket.gaierror: - raise RuntimeError("Port check failed! Unable to resolve host name '%s'" % host) - if port_open: - result = "OK" - else: - if port.port_type == socket.SOCK_DGRAM: - ports_udp_warning.append(port) - result = "WARNING" - else: - ports_failed.append(port) - result = "FAILED" - logger.info(" %s (%d): %s", port.description, port.port, result) - - if ports_udp_warning: - logger.warning( - ("The following UDP ports could not be verified as open: %s\n" - "This can happen if they are already bound to an application\n" - "and ipa-replica-conncheck cannot attach own UDP responder."), - ", ".join(str(port.port) for port in ports_udp_warning)) - - if ports_failed: - msg_ports = [] - for port in ports_failed: - port_type_text = "TCP" if port.port_type == SOCK_STREAM else "UDP" - msg_ports.append('%d (%s)' % (port.port, port_type_text)) - raise RuntimeError("Port check failed! Inaccessible port(s): %s" \ - % ", ".join(msg_ports)) - - -def main(): - global RESPONDER - safe_options, options = parse_options() - - logging_setup(options) - logger.debug('%s was invoked with options: %s', sys.argv[0], safe_options) - logger.debug("missing options might be asked for interactively later\n") - logger.debug('IPA version %s', version.VENDOR_VERSION) - - signal.signal(signal.SIGTERM, sigterm_handler) - - required_ports = BASE_PORTS - if options.check_ca: - # Check old Dogtag CA replication port - # New installs with unified databases use main DS port (checked above) - required_ports.append(CheckedPort(7389, SOCK_STREAM, - "PKI-CA: Directory Service port")) - - if options.replica: - logger.info("Check connection from master to remote replica '%s':", - options.replica) - port_check(options.replica, required_ports) - logger.info("\nConnection from master to replica is OK.") - - # kinit to foreign master - if options.master: - # check ports on master first - logger.info("Check connection from replica to remote master '%s':", - options.master) - tcp_ports = [ port for port in required_ports if port.port_type == SOCK_STREAM ] - udp_ports = [ port for port in required_ports if port.port_type == SOCK_DGRAM ] - port_check(options.master, tcp_ports) - - if udp_ports: - logger.info("\nThe following list of ports use UDP protocol " - "and would need to be\n" - "checked manually:") - for port in udp_ports: - result = "SKIPPED" - logger.info(" %s (%d): %s", - port.description, port.port, result) - - logger.info("\nConnection from replica to master is OK.") - - # create listeners - logger.info("Start listening on required ports for remote " - "master check") - - RESPONDER = PortResponder(required_ports) - RESPONDER.start() - - with RESPONDER.ports_open_cond: - if not RESPONDER.ports_opened: - logger.debug('Original thread stopped') - RESPONDER.ports_open_cond.wait() - logger.debug('Original thread resumed') - - remote_check_opts = ['--replica %s' % options.hostname] - - if options.auto_master_check: - logger.info("Get credentials to log in to remote master") - cred = None - if options.principal is None: - # Check if ccache is available - try: - logger.debug('KRB5CCNAME set to %s', - os.environ.get('KRB5CCNAME', None)) - # get default creds, will raise if none found - cred = gssapi.creds.Credentials() - principal = str(cred.name) - except gssapi.raw.misc.GSSError as e: - logger.debug('Failed to find default ccache: %s', e) - # Use admin as the default principal - principal = "admin" - else: - principal = options.principal - - if cred is None: - (krb_fd, krb_name) = tempfile.mkstemp() - os.close(krb_fd) - configure_krb5_conf(options.realm, options.kdc, krb_name) - global KRB5_CONFIG - KRB5_CONFIG = krb_name - (ccache_fd, ccache_name) = tempfile.mkstemp() - os.close(ccache_fd) - global CCACHE_FILE - CCACHE_FILE = ccache_name - - if principal.find('@') == -1: - principal = '%s@%s' % (principal, options.realm) - - if options.password: - password=options.password - else: - password = installutils.read_password(principal, confirm=False, - validate=False, retry=False) - if password is None: - sys.exit("Principal password required") - - - result = ipautil.run([paths.KINIT, principal], - env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE}, - stdin=password, raiseonerr=False, capture_error=True) - if result.returncode != 0: - raise RuntimeError("Cannot acquire Kerberos ticket: %s" % - result.error_output) - - # Verify kinit was actually successful - result = ipautil.run([paths.BIN_KVNO, - 'host/%s' % options.master], - env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE}, - raiseonerr=False, capture_error=True) - if result.returncode != 0: - raise RuntimeError("Could not get ticket for master server: %s" % - result.error_output) - # Now that the cred cache file is initialized, - # use it for the IPA API calls - os.environ['KRB5CCNAME'] = CCACHE_FILE - - try: - logger.info("Check RPC connection to remote master") - - xmlrpc_uri = ('https://%s/ipa/xml' % - ipautil.format_netloc(options.master)) - - if options.ca_cert_file: - nss_dir = None - else: - nss_dir = paths.IPA_NSSDB_DIR - - with certdb.NSSDatabase(nss_dir) as nss_db: - if options.ca_cert_file: - nss_db.create_db() - ca_certs = x509.load_certificate_list_from_file( - options.ca_cert_file) - for ca_cert in ca_certs: - nss_db.add_cert( - ca_cert, - str(DN(ca_cert.subject)), - certdb.EXTERNAL_CA_TRUST_FLAGS) - - api.bootstrap(context='client', - confdir=paths.ETC_IPA, - xmlrpc_uri=xmlrpc_uri, - nss_dir=nss_db.secdir) - api.finalize() - try: - api.Backend.rpcclient.connect() - api.Command.ping() - except Exception as e: - logger.info( - "Could not connect to the remote host: %s", e) - raise - - logger.info("Execute check on remote master") - try: - result = api.Backend.rpcclient.forward( - 'server_conncheck', - ipautil.fsdecode(options.master), - ipautil.fsdecode(options.hostname), - version=u'2.162', - ) - except (errors.CommandError, errors.NetworkError) as e: - logger.info( - "Remote master does not support check over RPC: " - "%s", e) - raise - except errors.PublicError as e: - returncode = 1 - stderr = e - else: - for message in result['messages']: - logger.info('%s', message['message']) - returncode = int(not result['result']) - stderr = ("ipa-replica-conncheck returned non-zero " - "exit code") - finally: - if api.Backend.rpcclient.isconnected(): - api.Backend.rpcclient.disconnect() - except Exception as e: - logger.debug("RPC connection failed: %s", e) - logger.info("Retrying using SSH...") - - # Ticket 5812 Always qualify requests for admin - user = principal - try: - ssh = SshExec(user, options.master) - except RuntimeError as e: - logger.warning("WARNING: %s, skipping ssh test", e) - return 0 - - logger.info("Check SSH connection to remote master") - result = ssh('echo OK', verbose=True) - if result.returncode != 0: - logger.debug('%s', result.error_output) - raise RuntimeError( - 'Could not SSH to remote host.\n' - 'See /var/log/ipareplica-conncheck.log for more ' - 'information.') - - logger.info("Execute check on remote master") - result = ssh( - "/usr/sbin/ipa-replica-conncheck " + - " ".join(remote_check_opts)) - returncode = result.returncode - stderr = result.error_output - logger.info('%s', result.output) - if returncode != 0: - raise RuntimeError( - "Remote master check failed with following " - "error message(s):\n%s" % stderr) - else: - # wait until user test is ready - logger.info( - "Listeners are started. Use CTRL+C to terminate the listening " - "part after the test.\n\n" - "Please run the following command on remote master:\n" - "/usr/sbin/ipa-replica-conncheck %s", - " ".join(remote_check_opts)) - time.sleep(3600) - logger.info( - "Connection check timeout: terminating listening program") - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - logger.info("\nCleaning up...") - sys.exit(1) - except RuntimeError as e: - logger.error('ERROR: %s', e) - sys.exit(1) - finally: - if RESPONDER is not None: - RESPONDER.stop() - RESPONDER.join() - for file_name in (CCACHE_FILE, KRB5_CONFIG): - if file_name: - try: - os.remove(file_name) - except OSError: - pass diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in new file mode 100644 index 0000000..82fa170 --- /dev/null +++ b/install/tools/ipa-replica-conncheck.in @@ -0,0 +1,660 @@ +@PYTHONSHEBANG@ +# Authors: Martin Kosek +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging + +import ipaclient.install.ipachangeconf +from ipapython.config import IPAOptionParser +from ipapython.dn import DN +from ipapython import version +from ipapython import ipautil, certdb +from ipalib import api, errors, x509 +from ipaserver.install import installutils +# pylint: disable=deprecated-module +from optparse import OptionGroup, OptionValueError +# pylint: enable=deprecated-module +from ipapython.ipa_log_manager import standard_logging_setup +import copy +import sys +import os +import signal +import tempfile +import select +import socket +import time +import threading +import traceback +from socket import SOCK_STREAM, SOCK_DGRAM +import distutils.spawn +from ipaplatform.paths import paths +import gssapi + +logger = logging.getLogger(os.path.basename(__file__)) + +CONNECT_TIMEOUT = 5 +RESPONDER = None +QUIET = False +CCACHE_FILE = None +KRB5_CONFIG = None + + +class SshExec(object): + def __init__(self, user, addr): + self.user = user + self.addr = addr + self.cmd = distutils.spawn.find_executable('ssh') + # Bail if ssh is not installed + if self.cmd is None: + raise RuntimeError("ssh not installed") + + def __call__(self, command, verbose=False): + + tmpf = tempfile.NamedTemporaryFile() + cmd = [ + self.cmd, + '-o StrictHostKeychecking=no', + '-o UserKnownHostsFile=%s' % tmpf.name, + '-o GSSAPIAuthentication=yes', + '-o User=%s' % self.user, + '%s' % self.addr, + command + ] + if verbose: + cmd.insert(1, '-v') + + env = dict() + if KRB5_CONFIG is not None: + env['KRB5_CONFIG'] = KRB5_CONFIG + elif 'KRB5_CONFIG' in os.environ: + env['KRB5_CONFIG'] = os.environ['KRB5_CONFIG'] + if CCACHE_FILE is not None: + env['KRB5CCNAME'] = CCACHE_FILE + elif 'KRB5CCNAME' in os.environ: + env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] + + return ipautil.run(cmd, env=env, raiseonerr=False, + capture_output=True, capture_error=True) + + +class CheckedPort(object): + def __init__(self, port, port_type, description): + self.port = port + self.port_type = port_type + self.description = description + + +BASE_PORTS = [ + CheckedPort(389, SOCK_STREAM, "Directory Service: Unsecure port"), + CheckedPort(636, SOCK_STREAM, "Directory Service: Secure port"), + CheckedPort(88, SOCK_STREAM, "Kerberos KDC: TCP"), + CheckedPort(88, SOCK_DGRAM, "Kerberos KDC: UDP"), + CheckedPort(464, SOCK_STREAM, "Kerberos Kpasswd: TCP"), + CheckedPort(464, SOCK_DGRAM, "Kerberos Kpasswd: UDP"), + CheckedPort(80, SOCK_STREAM, "HTTP Server: Unsecure port"), + CheckedPort(443, SOCK_STREAM, "HTTP Server: Secure port"), + ] + + +def parse_options(): + def ca_cert_file_callback(option, opt, value, parser): + if not os.path.exists(value): + raise OptionValueError( + "%s option '%s' does not exist" % (opt, value)) + if not os.path.isfile(value): + raise OptionValueError( + "%s option '%s' is not a file" % (opt, value)) + if not os.path.isabs(value): + raise OptionValueError( + "%s option '%s' is not an absolute file path" % (opt, value)) + + try: + x509.load_certificate_list_from_file(value) + except Exception: + raise OptionValueError( + "%s option '%s' is not a valid certificate file" % + (opt, value)) + + parser.values.ca_cert_file = value + + parser = IPAOptionParser(version=version.VERSION) + + replica_group = OptionGroup(parser, "on-replica options") + replica_group.add_option("-m", "--master", dest="master", + help="Master address with running IPA for output connection check") + replica_group.add_option("-a", "--auto-master-check", dest="auto_master_check", + action="store_true", + default=False, + help="Automatically execute connection check on master") + replica_group.add_option("-r", "--realm", dest="realm", + help="Realm name") + replica_group.add_option("-k", "--kdc", dest="kdc", + help="Master KDC. Defaults to master address") + replica_group.add_option("-p", "--principal", dest="principal", + default=None, help="Principal to use to log in to remote master") + replica_group.add_option("-w", "--password", dest="password", sensitive=True, + help="Password for the principal") + replica_group.add_option("--ca-cert-file", dest="ca_cert_file", + type="string", action="callback", + callback=ca_cert_file_callback, + help="load the CA certificate from this file") + parser.add_option_group(replica_group) + + + master_group = OptionGroup(parser, "on-master options") + master_group.add_option("-R", "--replica", dest="replica", + help="Address of remote replica machine to check against") + parser.add_option_group(master_group) + + common_group = OptionGroup(parser, "common options") + common_group.add_option("-c", "--check-ca", dest="check_ca", + action="store_true", + default=False, + help="Check also ports for Certificate Authority " + "(for servers installed before IPA 3.1)") + + common_group.add_option("", "--hostname", dest="hostname", + help="The hostname of this server (FQDN). " + "By default the result of getfqdn() call from " + "Python's socket module is used.") + parser.add_option_group(common_group) + + parser.add_option("-d", "--debug", dest="debug", + action="store_true", + default=False, help="Print debugging information") + parser.add_option("-q", "--quiet", dest="quiet", + action="store_true", + default=False, help="Output only errors") + parser.add_option("--no-log", dest="log_to_file", action="store_false", + default=True, help="Do not log into file") + + options, _args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + if options.master and options.replica: + parser.error("on-master and on-replica options are mutually exclusive!") + + if options.master: + if options.auto_master_check and not options.realm: + parser.error("Realm is parameter is required to connect to remote master!") + if not os.getegid() == 0: + parser.error("You can only run on-replica part as root.") + + if options.master and not options.kdc: + options.kdc = options.master + + if not options.master and not options.replica: + parser.error("No action: you should select either --replica or --master option.") + + if not options.hostname: + options.hostname = socket.getfqdn() + + return safe_options, options + + +def logging_setup(options): + log_file = None + + if os.getegid() == 0 and options.log_to_file: + log_file = paths.IPAREPLICA_CONNCHECK_LOG + + standard_logging_setup(log_file, verbose=(not options.quiet), + debug=options.debug, console_format='%(message)s') + + +def sigterm_handler(signum, frame): + # do what SIGINT does (raise a KeyboardInterrupt) + sigint_handler = signal.getsignal(signal.SIGINT) + if callable(sigint_handler): + sigint_handler(signum, frame) + + +def configure_krb5_conf(realm, kdc, filename): + + krbconf = ipaclient.install.ipachangeconf.IPAChangeConf("IPA Installer") + krbconf.setOptionAssignment((" = ", " ")) + krbconf.setSectionNameDelimiters(("[","]")) + krbconf.setSubSectionDelimiters(("{","}")) + krbconf.setIndent((""," "," ")) + + opts = [{'name':'comment', 'type':'comment', 'value':'File created by ipa-replica-conncheck'}, + {'name':'empty', 'type':'empty'}] + + #[libdefaults] + libdefaults = [{'name':'default_realm', 'type':'option', 'value':realm}] + libdefaults.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'}) + libdefaults.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'true'}) + libdefaults.append({'name':'rdns', 'type':'option', 'value':'false'}) + libdefaults.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'}) + libdefaults.append({'name':'forwardable', 'type':'option', 'value':'true'}) + libdefaults.append({'name':'udp_preference_limit', 'type':'option', 'value':'0'}) + + opts.append({'name':'libdefaults', 'type':'section', 'value': libdefaults}) + opts.append({'name':'empty', 'type':'empty'}) + + #the following are necessary only if DNS discovery does not work + #[realms] + realms_info =[{'name':'kdc', 'type':'option', 'value':ipautil.format_netloc(kdc, 88)}, + {'name':'master_kdc', 'type':'option', 'value':ipautil.format_netloc(kdc, 88)}, + {'name':'admin_server', 'type':'option', 'value':ipautil.format_netloc(kdc, 749)}] + realms = [{'name':realm, 'type':'subsection', 'value':realms_info}] + + opts.append({'name':'realms', 'type':'section', 'value':realms}) + opts.append({'name':'empty', 'type':'empty'}) + + #[appdefaults] + pamopts = [{'name':'debug', 'type':'option', 'value':'false'}, + {'name':'ticket_lifetime', 'type':'option', 'value':'36000'}, + {'name':'renew_lifetime', 'type':'option', 'value':'36000'}, + {'name':'forwardable', 'type':'option', 'value':'true'}, + {'name':'krb4_convert', 'type':'option', 'value':'false'}] + appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}] + opts.append({'name':'appdefaults', 'type':'section', 'value':appopts}) + + logger.debug("Writing temporary Kerberos configuration to %s:\n%s", + filename, krbconf.dump(opts)) + + krbconf.newConf(filename, opts) + + +class PortResponder(threading.Thread): + + PROTO = {socket.SOCK_STREAM: 'tcp', + socket.SOCK_DGRAM: 'udp'} + + def __init__(self, ports): + """ + ports: a list of CheckedPort + """ + super(PortResponder, self).__init__() + # copy ports to avoid the need to synchronize it between threads + self.ports = copy.deepcopy(ports) + self._sockets = [] + self._close = False + self._close_lock = threading.Lock() + self.responder_data = b'FreeIPA' + self.ports_opened = False + self.ports_open_cond = threading.Condition() + + def run(self): + logger.debug('Starting listening thread.') + + for port in self.ports: + self._bind_to_port(port.port, port.port_type) + with self.ports_open_cond: + self.ports_opened = True + logger.debug('Ports opened, notify original thread') + self.ports_open_cond.notify() + + while not self._is_closing(): + ready_socks, _socks1, _socks2 = select.select( + self._sockets, [], [], 1) + if ready_socks: + ready_sock = ready_socks[0] + self._respond(ready_sock) + + for sock in self._sockets: + port = sock.getsockname()[1] + proto = PortResponder.PROTO[sock.type] + sock.close() + logger.debug('%d %s: Stopped listening', port, proto) + + def _is_closing(self): + with self._close_lock: # pylint: disable=not-context-manager + return self._close + + def _bind_to_port(self, port, socket_type): + # Use IPv6 socket as it is able to accept both IPv6 and IPv4 + # connections. Since IPv6 kernel module is required by other + # parts of IPA, it should always be available. + family = socket.AF_INET6 + host = '::' # all available interfaces + proto = PortResponder.PROTO[socket_type] + + try: + sock = socket.socket(family, socket_type) + + # Make sure IPv4 clients can connect to IPv6 socket + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + + if socket_type == socket.SOCK_STREAM: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + sock.bind((host, port)) + if socket_type == socket.SOCK_STREAM: + # There might be a delay before accepting the connection, + # because a single thread is used to handle all the + # connections. Thus a backlog size of at least 1 is needed. + sock.listen(1) + + logger.debug('%d %s: Started listening', port, proto) + except socket.error: + logger.warning('%d %s: Failed to bind', port, proto) + logger.debug("%s", traceback.format_exc()) + else: + self._sockets.append(sock) + + def _respond(self, sock): + port = sock.getsockname()[1] + if sock.type == socket.SOCK_STREAM: + connection, addr = sock.accept() + try: + connection.sendall(self.responder_data) + logger.debug('%d tcp: Responded to %s', port, addr[0]) + finally: + connection.close() + elif sock.type == socket.SOCK_DGRAM: + _data, addr = sock.recvfrom(1) + sock.sendto(self.responder_data, addr) + logger.debug('%d udp: Responded to %s', port, addr[0]) + + def stop(self): + logger.debug('Stopping listening thread.') + + with self._close_lock: # pylint: disable=not-context-manager + self._close = True + + +def port_check(host, port_list): + ports_failed = [] + ports_udp_warning = [] # conncheck could not verify that port is open + log_level = { + SOCK_DGRAM: logging.WARNING, + SOCK_STREAM: logging.ERROR + } + for port in port_list: + try: + port_open = ipautil.host_port_open( + host, port.port, port.port_type, + socket_timeout=CONNECT_TIMEOUT, log_errors=True, + log_level=log_level[port.port_type]) + except socket.gaierror: + raise RuntimeError("Port check failed! Unable to resolve host name '%s'" % host) + if port_open: + result = "OK" + else: + if port.port_type == socket.SOCK_DGRAM: + ports_udp_warning.append(port) + result = "WARNING" + else: + ports_failed.append(port) + result = "FAILED" + logger.info(" %s (%d): %s", port.description, port.port, result) + + if ports_udp_warning: + logger.warning( + ("The following UDP ports could not be verified as open: %s\n" + "This can happen if they are already bound to an application\n" + "and ipa-replica-conncheck cannot attach own UDP responder."), + ", ".join(str(port.port) for port in ports_udp_warning)) + + if ports_failed: + msg_ports = [] + for port in ports_failed: + port_type_text = "TCP" if port.port_type == SOCK_STREAM else "UDP" + msg_ports.append('%d (%s)' % (port.port, port_type_text)) + raise RuntimeError("Port check failed! Inaccessible port(s): %s" \ + % ", ".join(msg_ports)) + + +def main(): + global RESPONDER + safe_options, options = parse_options() + + logging_setup(options) + logger.debug('%s was invoked with options: %s', sys.argv[0], safe_options) + logger.debug("missing options might be asked for interactively later\n") + logger.debug('IPA version %s', version.VENDOR_VERSION) + + signal.signal(signal.SIGTERM, sigterm_handler) + + required_ports = BASE_PORTS + if options.check_ca: + # Check old Dogtag CA replication port + # New installs with unified databases use main DS port (checked above) + required_ports.append(CheckedPort(7389, SOCK_STREAM, + "PKI-CA: Directory Service port")) + + if options.replica: + logger.info("Check connection from master to remote replica '%s':", + options.replica) + port_check(options.replica, required_ports) + logger.info("\nConnection from master to replica is OK.") + + # kinit to foreign master + if options.master: + # check ports on master first + logger.info("Check connection from replica to remote master '%s':", + options.master) + tcp_ports = [ port for port in required_ports if port.port_type == SOCK_STREAM ] + udp_ports = [ port for port in required_ports if port.port_type == SOCK_DGRAM ] + port_check(options.master, tcp_ports) + + if udp_ports: + logger.info("\nThe following list of ports use UDP protocol " + "and would need to be\n" + "checked manually:") + for port in udp_ports: + result = "SKIPPED" + logger.info(" %s (%d): %s", + port.description, port.port, result) + + logger.info("\nConnection from replica to master is OK.") + + # create listeners + logger.info("Start listening on required ports for remote " + "master check") + + RESPONDER = PortResponder(required_ports) + RESPONDER.start() + + with RESPONDER.ports_open_cond: + if not RESPONDER.ports_opened: + logger.debug('Original thread stopped') + RESPONDER.ports_open_cond.wait() + logger.debug('Original thread resumed') + + remote_check_opts = ['--replica %s' % options.hostname] + + if options.auto_master_check: + logger.info("Get credentials to log in to remote master") + cred = None + if options.principal is None: + # Check if ccache is available + try: + logger.debug('KRB5CCNAME set to %s', + os.environ.get('KRB5CCNAME', None)) + # get default creds, will raise if none found + cred = gssapi.creds.Credentials() + principal = str(cred.name) + except gssapi.raw.misc.GSSError as e: + logger.debug('Failed to find default ccache: %s', e) + # Use admin as the default principal + principal = "admin" + else: + principal = options.principal + + if cred is None: + (krb_fd, krb_name) = tempfile.mkstemp() + os.close(krb_fd) + configure_krb5_conf(options.realm, options.kdc, krb_name) + global KRB5_CONFIG + KRB5_CONFIG = krb_name + (ccache_fd, ccache_name) = tempfile.mkstemp() + os.close(ccache_fd) + global CCACHE_FILE + CCACHE_FILE = ccache_name + + if principal.find('@') == -1: + principal = '%s@%s' % (principal, options.realm) + + if options.password: + password=options.password + else: + password = installutils.read_password(principal, confirm=False, + validate=False, retry=False) + if password is None: + sys.exit("Principal password required") + + + result = ipautil.run([paths.KINIT, principal], + env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE}, + stdin=password, raiseonerr=False, capture_error=True) + if result.returncode != 0: + raise RuntimeError("Cannot acquire Kerberos ticket: %s" % + result.error_output) + + # Verify kinit was actually successful + result = ipautil.run([paths.BIN_KVNO, + 'host/%s' % options.master], + env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE}, + raiseonerr=False, capture_error=True) + if result.returncode != 0: + raise RuntimeError("Could not get ticket for master server: %s" % + result.error_output) + # Now that the cred cache file is initialized, + # use it for the IPA API calls + os.environ['KRB5CCNAME'] = CCACHE_FILE + + try: + logger.info("Check RPC connection to remote master") + + xmlrpc_uri = ('https://%s/ipa/xml' % + ipautil.format_netloc(options.master)) + + if options.ca_cert_file: + nss_dir = None + else: + nss_dir = paths.IPA_NSSDB_DIR + + with certdb.NSSDatabase(nss_dir) as nss_db: + if options.ca_cert_file: + nss_db.create_db() + ca_certs = x509.load_certificate_list_from_file( + options.ca_cert_file) + for ca_cert in ca_certs: + nss_db.add_cert( + ca_cert, + str(DN(ca_cert.subject)), + certdb.EXTERNAL_CA_TRUST_FLAGS) + + api.bootstrap(context='client', + confdir=paths.ETC_IPA, + xmlrpc_uri=xmlrpc_uri, + nss_dir=nss_db.secdir) + api.finalize() + try: + api.Backend.rpcclient.connect() + api.Command.ping() + except Exception as e: + logger.info( + "Could not connect to the remote host: %s", e) + raise + + logger.info("Execute check on remote master") + try: + result = api.Backend.rpcclient.forward( + 'server_conncheck', + ipautil.fsdecode(options.master), + ipautil.fsdecode(options.hostname), + version=u'2.162', + ) + except (errors.CommandError, errors.NetworkError) as e: + logger.info( + "Remote master does not support check over RPC: " + "%s", e) + raise + except errors.PublicError as e: + returncode = 1 + stderr = e + else: + for message in result['messages']: + logger.info('%s', message['message']) + returncode = int(not result['result']) + stderr = ("ipa-replica-conncheck returned non-zero " + "exit code") + finally: + if api.Backend.rpcclient.isconnected(): + api.Backend.rpcclient.disconnect() + except Exception as e: + logger.debug("RPC connection failed: %s", e) + logger.info("Retrying using SSH...") + + # Ticket 5812 Always qualify requests for admin + user = principal + try: + ssh = SshExec(user, options.master) + except RuntimeError as e: + logger.warning("WARNING: %s, skipping ssh test", e) + return 0 + + logger.info("Check SSH connection to remote master") + result = ssh('echo OK', verbose=True) + if result.returncode != 0: + logger.debug('%s', result.error_output) + raise RuntimeError( + 'Could not SSH to remote host.\n' + 'See /var/log/ipareplica-conncheck.log for more ' + 'information.') + + logger.info("Execute check on remote master") + result = ssh( + "/usr/sbin/ipa-replica-conncheck " + + " ".join(remote_check_opts)) + returncode = result.returncode + stderr = result.error_output + logger.info('%s', result.output) + if returncode != 0: + raise RuntimeError( + "Remote master check failed with following " + "error message(s):\n%s" % stderr) + else: + # wait until user test is ready + logger.info( + "Listeners are started. Use CTRL+C to terminate the listening " + "part after the test.\n\n" + "Please run the following command on remote master:\n" + "/usr/sbin/ipa-replica-conncheck %s", + " ".join(remote_check_opts)) + time.sleep(3600) + logger.info( + "Connection check timeout: terminating listening program") + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + logger.info("\nCleaning up...") + sys.exit(1) + except RuntimeError as e: + logger.error('ERROR: %s', e) + sys.exit(1) + finally: + if RESPONDER is not None: + RESPONDER.stop() + RESPONDER.join() + for file_name in (CCACHE_FILE, KRB5_CONFIG): + if file_name: + try: + os.remove(file_name) + except OSError: + pass diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install deleted file mode 100755 index 63a54e0..0000000 --- a/install/tools/ipa-replica-install +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Karl MacMillan -# -# Copyright (C) 2007 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install import ipa_replica_install - -ipa_replica_install.run() diff --git a/install/tools/ipa-replica-install.in b/install/tools/ipa-replica-install.in new file mode 100644 index 0000000..4c7f413 --- /dev/null +++ b/install/tools/ipa-replica-install.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Karl MacMillan +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install import ipa_replica_install + +ipa_replica_install.run() diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage deleted file mode 100755 index 06fefd4..0000000 --- a/install/tools/ipa-replica-manage +++ /dev/null @@ -1,1641 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Karl MacMillan -# -# Copyright (C) 2007 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import logging -import sys -import os - -import re -import ldap -import socket -import traceback - -# pylint: disable=import-error -from six.moves.urllib.parse import urlparse -from six.moves.xmlrpc_client import MAXINT -# pylint: enable=import-error - -from ipaclient.install import ipadiscovery -from ipapython import ipautil -from ipaserver.install import replication, dsinstance, installutils -from ipaserver.install import bindinstance, cainstance -from ipaserver.install import opendnssecinstance, dnskeysyncinstance -from ipapython import version, ipaldap -from ipalib import api, errors -from ipalib.util import has_managed_topology, verify_host_resolvable -from ipapython.ipa_log_manager import standard_logging_setup -from ipapython.dn import DN -from ipapython.config import IPAOptionParser -from ipaplatform.paths import paths - -logger = logging.getLogger(os.path.basename(__file__)) - -# dict of command name and tuples of min/max num of args needed -commands = { - "list":(0, 1, "[master fqdn]", ""), - "list-ruv":(0, 0, "", ""), - "connect":(1, 2, " [other master fqdn]", - "must provide the name of the servers to connect"), - "disconnect":(1, 2, " [other master fqdn]", - "must provide the name of the server to disconnect"), - "del":(1, 1, "", - "must provide hostname of master to delete"), - "re-initialize":(0, 0, "", ""), - "force-sync":(0, 0, "", ""), - "clean-ruv":(1, 1, "Replica ID of to clean", "must provide replica ID to clean"), - "abort-clean-ruv":(1, 1, "Replica ID to abort cleaning", "must provide replica ID to abort cleaning"), - "list-clean-ruv":(0, 0, "", ""), - "clean-dangling-ruv":(0, 0, "", ""), - "dnarange-show":(0, 1, "[master fqdn]", ""), - "dnanextrange-show":(0, 1, "", ""), - "dnarange-set":(2, 2, " ", "must provide a master and ID range"), - "dnanextrange-set":(2, 2, " ", "must provide a master and ID range"), -} - -# tuple of commands that work with ca tree and need Directory Manager password -dirman_passwd_req_commands = ("list-ruv", "clean-ruv", "abort-clean-ruv", - "clean-dangling-ruv") - - -class NoRUVsFound(Exception): - pass - - -def parse_options(): - parser = IPAOptionParser(version=version.VERSION) - parser.add_option("-H", "--host", dest="host", help="starting host") - parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") - parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, - help="provide additional information") - parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, - help="provide additional debug information") - parser.add_option("-f", "--force", dest="force", action="store_true", default=False, - help="ignore some types of errors") - parser.add_option("-c", "--cleanup", dest="cleanup", action="store_true", default=False, - help="DANGER: clean up references to a ghost master") - parser.add_option("--binddn", dest="binddn", default=None, type="dn", - help="Bind DN to use with remote server") - parser.add_option("--bindpw", dest="bindpw", default=None, - help="Password for Bind DN to use with remote server") - parser.add_option("--winsync", dest="winsync", action="store_true", default=False, - help="This is a Windows Sync Agreement") - parser.add_option("--cacert", dest="cacert", default=None, - help="Full path and filename of CA certificate to use with TLS/SSL to the remote server") - parser.add_option("--win-subtree", dest="win_subtree", default=None, - help="DN of Windows subtree containing the users you want to sync (default cn=Users, v[1]: - err = "too many arguments" - else: - valid_syntax = True - if err: - parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2])) - - if not valid_syntax: - cmdstr = " | ".join(commands.keys()) - parser.error("must provide a command [%s]" % cmdstr) - - return options, args - -def test_connection(realm, host, nolookup=False): - """ - Make a GSSAPI connection to the remote LDAP server to test out credentials. - - This is used so we can fall back to promping for the DM password. - - returns True if connection successful, False otherwise - """ - try: - if not nolookup: - enforce_host_existence(host) - replman = replication.ReplicationManager(realm, host, None) - replman.find_replication_agreements() - del replman - return True - except errors.ACIError: - return False - except errors.NotFound: - # We do a search in cn=config. NotFound in this case means no - # permission - return False - except ldap.LOCAL_ERROR: - # more than likely a GSSAPI error - return False - -def list_replicas(realm, host, replica, dirman_passwd, verbose, nolookup=False): - - if not nolookup: - enforce_host_existence(host) - if replica is not None: - enforce_host_existence(replica) - - is_replica = False - winsync_peer = None - peers = {} - - try: - ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) - conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) - if dirman_passwd: - conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, - bind_password=dirman_passwd) - else: - conn.gssapi_bind() - except Exception as e: - print("Failed to connect to host '%s': %s" % (host, str(e))) - return - - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) - try: - entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) - except Exception as e: - print("Failed to read master data from '%s': %s" % (host, str(e))) - return - else: - for ent in entries: - peers[ent.single_value['cn']] = ['master', ''] - - dn = DN(('cn', 'replicas'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) - try: - entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) - except Exception: - pass - else: - for ent in entries: - config_string = ent.single_value['ipaConfigString'] - peers[ent.single_value['cn']] = config_string.split(':') - - if not replica: - for k, p in peers.items(): - print('%s: %s' % (k, p[0])) - return - - # ok we are being ask for info about a specific replica - for k, p in peers.items(): - if replica == k: - is_replica = True - if p[0] == 'winsync': - winsync_peer = p[1] - - if not is_replica: - print("Cannot find %s in public server list" % replica) - return - - try: - if winsync_peer: - repl = replication.ReplicationManager(realm, winsync_peer, - dirman_passwd) - _cn, dn = repl.agreement_dn(replica) - entries = repl.conn.get_entries( - dn, conn.SCOPE_BASE, - "(objectclass=nsDSWindowsReplicationAgreement)") - ent_type = 'winsync' - else: - repl = replication.ReplicationManager(realm, replica, - dirman_passwd) - entries = repl.find_replication_agreements() - ent_type = 'replica' - except Exception as e: - print("Failed to get data from '%s': %s" % (replica, e)) - return - - for entry in entries: - print('%s: %s' % (entry.single_value.get('nsds5replicahost'), ent_type)) - - if verbose: - print(" last init status: %s" % entry.single_value.get( - 'nsds5replicalastinitstatus')) - print(" last init ended: %s" % str(ipautil.parse_generalized_time( - entry.single_value['nsds5replicalastinitend']))) - print(" last update status: %s" % entry.single_value.get( - 'nsds5replicalastupdatestatus')) - print(" last update ended: %s" % str( - ipautil.parse_generalized_time( - entry.single_value['nsds5replicalastupdateend']))) - -def del_link(realm, replica1, replica2, dirman_passwd, force=False): - """ - Delete a replication agreement from host A to host B. - - @realm: the Kerberos realm - @replica1: the hostname of master A - @replica2: the hostname of master B - @dirman_passwd: the Directory Manager password - @force: force deletion even if one server is down - """ - - repl2 = None - what = "Removal of IPA replication agreement" - managed_topology = has_managed_topology(api) - - try: - repl1 = replication.ReplicationManager(realm, replica1, dirman_passwd) - type1 = repl1.get_agreement_type(replica2) - except errors.NotFound: - # it's possible that the agreement could not have been found because of - # the new topology plugin naming convention: -to- instead of - # meTo. - if managed_topology: - print("'%s' has no winsync replication agreement for '%s'" % (replica1, replica2)) - exit_on_managed_topology(what) - else: - print("'%s' has no replication agreement for '%s'" % (replica1, replica2)) - return False - except Exception as e: - print("Failed to determine agreement type for '%s': %s" % (replica2, e)) - - if type1 == replication.IPA_REPLICA and managed_topology: - exit_on_managed_topology(what) - - repl_list = repl1.find_ipa_replication_agreements() - if not force and len(repl_list) <= 1 and type1 == replication.IPA_REPLICA: - print("Cannot remove the last replication link of '%s'" % replica1) - print("Please use the 'del' command to remove it from the domain") - return False - - if type1 == replication.IPA_REPLICA: - try: - repl2 = replication.ReplicationManager(realm, replica2, dirman_passwd) - - repl_list = repl2.find_ipa_replication_agreements() - if not force and len(repl_list) <= 1: - print("Cannot remove the last replication link of '%s'" % replica2) - print("Please use the 'del' command to remove it from the domain") - return False - - except errors.NotFound: - print("'%s' has no replication agreement for '%s'" % (replica2, replica1)) - if not force: - return False - except Exception as e: - print("Failed to get list of agreements from '%s': %s" % (replica2, e)) - if not force: - return False - - if repl2 and type1 == replication.IPA_REPLICA: - failed = False - try: - repl2.set_readonly(readonly=True) - repl2.force_sync(repl2.conn, replica1) - _cn, dn = repl2.agreement_dn(repl1.conn.host) - repl2.wait_for_repl_update(repl2.conn, dn, 30) - (range_start, range_max) = repl2.get_DNA_range(repl2.conn.host) - (next_start, next_max) = repl2.get_DNA_next_range(repl2.conn.host) - if range_start is not None: - if not store_DNA_range(repl1, range_start, range_max, repl2.conn.host, realm, dirman_passwd): - print("Unable to save DNA range %d-%d" % (range_start, range_max)) - if next_start is not None: - if not store_DNA_range(repl1, next_start, next_max, repl2.conn.host, realm, dirman_passwd): - print("Unable to save DNA range %d-%d" % (next_start, next_max)) - repl2.set_readonly(readonly=False) - repl2.delete_agreement(replica1) - repl2.delete_referral(replica1) - repl2.set_readonly(readonly=False) - except Exception as e: - print("Unable to remove agreement on %s: %s" % (replica2, e)) - failed = True - - if failed: - if force: - print("Forcing removal on '%s'" % replica1) - print("Any DNA range on '%s' will be lost" % replica2) - else: - return False - - if not repl2 and force: - print("Forcing removal on '%s'" % replica1) - print("Any DNA range on '%s' will be lost" % replica2) - - repl1.delete_agreement(replica2) - repl1.delete_referral(replica2) - - if type1 == replication.WINSYNC: - try: - dn = DN(('cn', replica2), ('cn', 'replicas'), ('cn', 'ipa'), ('cn', 'etc'), - ipautil.realm_to_suffix(realm)) - entries = repl1.conn.get_entries(dn, repl1.conn.SCOPE_SUBTREE) - if entries: - entries.sort(key=lambda x: len(x.dn), reverse=True) - for entry in entries: - repl1.conn.delete_entry(entry) - except Exception as e: - print("Error deleting winsync replica shared info: %s" % e) - - print("Deleted replication agreement from '%s' to '%s'" % (replica1, replica2)) - - return True - -def get_ruv(realm, host, dirman_passwd, nolookup=False, ca=False): - """ - Return the RUV entries as a list of tuples: (hostname, rid) - """ - - if not nolookup: - enforce_host_existence(host) - - try: - if ca: - thisrepl = replication.get_cs_replication_manager(realm, host, dirman_passwd) - else: - thisrepl = replication.ReplicationManager(realm, host, dirman_passwd) - except Exception as e: - logger.debug("%s", traceback.format_exc()) - raise RuntimeError("Failed to connect to server {host}: {err}" - .format(host=host, err=e)) - - search_filter = '(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))' - try: - entries = thisrepl.conn.get_entries( - thisrepl.db_suffix, thisrepl.conn.SCOPE_SUBTREE, search_filter, - ['nsds50ruv']) - except errors.NotFound: - logger.debug("%s", traceback.format_exc()) - raise NoRUVsFound("No RUV records found.") - - servers = [] - for e in entries: - for ruv in e['nsds50ruv']: - if ruv.startswith('{replicageneration'): - continue - data = re.match('\{replica (\d+) (ldap://.*:\d+)\}(\s+\w+\s+\w*){0,1}', ruv) - if data: - rid = data.group(1) - ( - _scheme, netloc, _path, _params, _query, _fragment - ) = urlparse(data.group(2)) - servers.append((netloc, rid)) - else: - print("unable to decode: %s" % ruv) - - return servers - - -def get_ruv_both_suffixes(realm, host, dirman_passwd, verbose, nolookup=False): - """ - Get RUVs for both domain and ipaca suffixes - """ - ruvs = {} - fail_gracefully = True - - try: - ruvs['ca'] = get_ruv(realm, host, dirman_passwd, nolookup, True) - except (NoRUVsFound, RuntimeError) as e: - err = "Failed to get CS-RUVs from {host}: {err}".format(host=host, - err=e) - if isinstance(e, RuntimeError): - fail_gracefully = False - if verbose: - print(err) - logger.debug('%s', err) - try: - ruvs['domain'] = get_ruv(realm, host, dirman_passwd, nolookup) - except (NoRUVsFound, RuntimeError) as e: - err = "Failed to get RUVs from {host}: {err}".format(host=host, err=e) - if isinstance(e, RuntimeError): - if not fail_gracefully: - raise - if verbose: - print(err) - logger.debug('%s', err) - - if not ruvs.keys(): - raise NoRUVsFound("No RUV records found.") - - return ruvs - - -def list_ruv(realm, host, dirman_passwd, verbose, nolookup=False): - """ - List the Replica Update Vectors on this host to get the available - replica IDs. - """ - try: - servers = get_ruv_both_suffixes(realm, host, dirman_passwd, - verbose, nolookup) - except (NoRUVsFound, RuntimeError) as e: - print(e) - sys.exit(0 if isinstance(e, NoRUVsFound) else 1) - - print('Replica Update Vectors:') - if servers.get('domain'): - for netloc, rid in servers['domain']: - print("\t{name}: {id}".format(name=netloc, id=rid)) - else: - print('\tNo RUVs found.') - - print('Certificate Server Replica Update Vectors:') - if servers.get('ca'): - for netloc, rid in servers['ca']: - print("\t{name}: {id}".format(name=netloc, id=rid)) - else: - print('\tNo CS-RUVs found.') - - -def get_rid_by_host(realm, sourcehost, host, dirman_passwd, nolookup=False): - """ - Try to determine the RID by host name. - """ - try: - servers = get_ruv(realm, sourcehost, dirman_passwd, nolookup) - except RuntimeError as e: - print(e) - sys.exit(1) - except NoRUVsFound as e: - print(e) - servers = [] - for (netloc, rid) in servers: - if '%s:389' % host == netloc: - return int(rid) - - -def clean_ruv(realm, ruv, options): - """ - Given an RID create a CLEANALLRUV task to clean it up. - """ - try: - ruv = int(ruv) - except ValueError: - sys.exit("Replica ID must be an integer: %s" % ruv) - - try: - servers = get_ruv_both_suffixes(realm, options.host, - options.dirman_passwd, - options.verbose, - options.nolookup) - except (NoRUVsFound, RuntimeError) as e: - print(e) - sys.exit(0 if isinstance(e, NoRUVsFound) else 1) - - tree_found = None - for tree, ruvs in servers.items(): - for netloc, rid in ruvs: - if ruv == int(rid): - tree_found = tree - hostname = netloc - break - if tree_found: - break - - if not tree_found: - sys.exit("Replica ID %s not found" % ruv) - - if tree_found == 'ca': - print("Clean the Certificate Server Replication Update Vector for %s" - % hostname) - else: - print("Clean the Replication Update Vector for %s" % hostname) - - if not options.force: - print() - print("Cleaning the wrong replica ID will cause that server to no") - print("longer replicate so it may miss updates while the process") - print("is running. It would need to be re-initialized to maintain") - print("consistency. Be very careful.") - if not ipautil.user_input("Continue to clean?", False): - sys.exit("Aborted") - - if tree_found == 'ca': - thisrepl = replication.get_cs_replication_manager(realm, options.host, - options.dirman_passwd) - else: - thisrepl = replication.ReplicationManager(realm, options.host, - options.dirman_passwd) - thisrepl.cleanallruv(ruv) - print("Cleanup task created") - - -def abort_clean_ruv(realm, ruv, options): - """ - Given an RID abort a CLEANALLRUV task. - """ - try: - ruv = int(ruv) - except ValueError: - sys.exit("Replica ID must be an integer: %s" % ruv) - - try: - servers = get_ruv_both_suffixes(realm, options.host, - options.dirman_passwd, - options.verbose, - options.nolookup) - except (NoRUVsFound, RuntimeError) as e: - print(e) - sys.exit(0 if isinstance(e, NoRUVsFound) else 1) - - tree_found = None - for tree, ruvs in servers.items(): - for netloc, rid in ruvs: - if ruv == int(rid): - tree_found = tree - hostname = netloc - break - if tree_found: - break - - if not tree_found: - sys.exit("Replica ID %s not found" % ruv) - - print("Aborting the clean Replication Update Vector task for %s" % hostname) - print() - if tree_found == 'ca': - thisrepl = replication.get_cs_replication_manager(realm, options.host, - options.dirman_passwd) - else: - thisrepl = replication.ReplicationManager(realm, options.host, - options.dirman_passwd) - thisrepl.abortcleanallruv(ruv, options.force) - - print("Cleanup task stopped") - - -def list_clean_ruv(realm, host, dirman_passwd, verbose, nolookup=False): - """ - List all clean RUV tasks. - """ - - if not nolookup: - enforce_host_existence(host) - - repl = replication.ReplicationManager(realm, host, dirman_passwd) - dn = DN(('cn', 'cleanallruv'),('cn', 'tasks'), ('cn', 'config')) - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) - except errors.NotFound: - print("No CLEANALLRUV tasks running") - else: - print("CLEANALLRUV tasks") - for entry in entries: - name = entry.single_value['cn'].replace('clean ', '') - status = entry.single_value.get('nsTaskStatus') - print("RID %s: %s" % (name, status)) - if verbose: - print(str(dn)) - print(entry.single_value.get('nstasklog')) - - print() - - dn = DN(('cn', 'abort cleanallruv'),('cn', 'tasks'), ('cn', 'config')) - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) - except errors.NotFound: - print("No abort CLEANALLRUV tasks running") - else: - print("Abort CLEANALLRUV tasks") - for entry in entries: - name = entry.single_value['cn'].replace('abort ', '') - status = entry.single_value.get('nsTaskStatus') - print("RID %s: %s" % (name, status)) - if verbose: - print(str(dn)) - print(entry.single_value.get('nstasklog')) - - -def clean_dangling_ruvs(realm, host, options): - """ - Cleans all RUVs and CS-RUVs that are left in the system from - uninstalled replicas - """ - ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) - conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) - try: - conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, - bind_password=options.dirman_passwd) - - # get all masters - masters_dn = DN(api.env.container_masters, api.env.basedn) - masters = conn.get_entries(masters_dn, conn.SCOPE_ONELEVEL) - info = {} - - # check whether CAs are configured on those masters - for master in masters: - info[master.single_value['cn']] = { - 'online': False, # is the host online? - 'ca': False, # does the host have ca configured? - 'ruvs': set(), # ruvs on the host - 'csruvs': set(), # csruvs on the host - 'clean_ruv': set(), # ruvs to be cleaned from the host - 'clean_csruv': set() # csruvs to be cleaned from the host - } - try: - ca_dn = DN(('cn', 'ca'), master.dn) - conn.get_entry(ca_dn) - info[master.single_value['cn']]['ca'] = True - except errors.NotFound: - continue - - except Exception as e: - sys.exit( - "Failed to get data from '{host}' while trying to " - "list replicas: {error}" - .format(host=host, error=e) - ) - finally: - conn.unbind() - - replica_dn = DN(('cn', 'replica'), ('cn', api.env.basedn), - ('cn', 'mapping tree'), ('cn', 'config')) - - csreplica_dn = DN(('cn', 'replica'), ('cn', 'o=ipaca'), - ('cn', 'mapping tree'), ('cn', 'config')) - - ruvs = set() - csruvs = set() - offlines = set() - for master_cn, master_info in info.items(): - try: - ldap_uri = ipaldap.get_ldap_uri(master_cn, 636, cacert=paths.IPA_CA_CRT) - conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) - conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, - bind_password=options.dirman_passwd) - master_info['online'] = True - except Exception: - print("The server '{host}' appears to be offline." - .format(host=master_cn)) - offlines.add(master_cn) - continue - try: - try: - entry = conn.get_entry(replica_dn) - ruv = (master_cn, entry.single_value.get('nsDS5ReplicaID')) - # the check whether ruv is already in ruvs is performed - # by the set type - ruvs.add(ruv) - except errors.NotFound: - pass - - if master_info['ca']: - try: - entry = conn.get_entry(csreplica_dn) - csruv = (master_cn, - entry.single_value.get('nsDS5ReplicaID')) - csruvs.add(csruv) - except errors.NotFound: - pass - - try: - ruv_dict = get_ruv_both_suffixes(realm, master_cn, - options.dirman_passwd, - options.verbose, - options.nolookup) - except (RuntimeError, NoRUVsFound): - continue - - # get_ruv_both_suffixes returns server names with :port - # This needs needs to be split off - if ruv_dict.get('domain'): - master_info['ruvs'] = set([ - (re.sub(':\d+', '', x), y) - for (x, y) in ruv_dict['domain'] - ]) - if ruv_dict.get('ca'): - master_info['csruvs'] = set([ - (re.sub(':\d+', '', x), y) - for (x, y) in ruv_dict['ca'] - ]) - except Exception as e: - sys.exit("Failed to obtain information from '{host}': {error}" - .format(host=master_cn, error=str(e))) - finally: - conn.unbind() - - dangles = False - # get the dangling RUVs - for master_info in info.values(): - if master_info['online']: - for ruv in master_info['ruvs']: - if (ruv not in ruvs) and (ruv[0] not in offlines): - master_info['clean_ruv'].add(ruv) - dangles = True - - # if ca is not configured, there will be no csruvs in master_info - for csruv in master_info['csruvs']: - if (csruv not in csruvs) and (csruv[0] not in offlines): - master_info['clean_csruv'].add(csruv) - dangles = True - - if not dangles: - print('No dangling RUVs found') - sys.exit(0) - - print('These RUVs are dangling and will be removed:') - for master_cn, master_info in info.items(): - if master_info['online'] and (master_info['clean_ruv'] or - master_info['clean_csruv']): - print('Host: {m}'.format(m=master_cn)) - print('\tRUVs:') - for ruv in master_info['clean_ruv']: - print('\t\tid: {id}, hostname: {host}' - .format(id=ruv[1], host=ruv[0])) - - print('\tCS-RUVs:') - for csruv in master_info['clean_csruv']: - print('\t\tid: {id}, hostname: {host}' - .format(id=csruv[1], host=csruv[0])) - - if not options.force and not ipautil.user_input("Proceed with cleaning?", False): - sys.exit("Aborted") - - options.force = True - cleaned = set() - for master_cn, master_info in info.items(): - options.host = master_cn - for ruv in master_info['clean_ruv']: - if ruv[1] not in cleaned: - cleaned.add(ruv[1]) - clean_ruv(realm, ruv[1], options) - for csruv in master_info['clean_csruv']: - if csruv[1] not in cleaned: - cleaned.add(csruv[1]) - clean_ruv(realm, csruv[1], options) - - -def check_last_link(delrepl, realm, dirman_passwd, force): - """ - We don't want to orphan a server when deleting another one. If you have - a topology that looks like this: - - A B - | | - | | - | | - C---- D - - If we try to delete host D it will orphan host B. - - What we need to do is if the master being deleted has only a single - agreement, connect to that master and make sure it has agreements with - more than just this master. - - @delrepl: a ReplicationManager object of the master being deleted - - returns: hostname of orphaned server or None - """ - replica_entries = delrepl.find_ipa_replication_agreements() - - replica_names = [rep.single_value.get('nsds5replicahost') - for rep in replica_entries] - - orphaned = [] - # Connect to each remote server and see what agreements it has - for replica in replica_names: - try: - repl = replication.ReplicationManager(realm, replica, dirman_passwd) - except errors.NetworkError: - print("Unable to validate that '%s' will not be orphaned." % replica) - - if not force and not ipautil.user_input("Continue to delete?", False): - sys.exit("Aborted") - continue - - entries = repl.find_ipa_replication_agreements() - names = [rep.single_value.get('nsds5replicahost') - for rep in entries] - - if len(names) == 1 and names[0] == delrepl.hostname: - orphaned.append(replica) - - if len(orphaned): - return ', '.join(orphaned) - else: - return None - - -def enforce_host_existence(host, message=None): - if host is None: - return - - try: - verify_host_resolvable(host) - except errors.DNSNotARecordError as ex: - if message is None: - message = "Unknown host %s: %s" % (host, ex) - sys.exit(message) - -def ensure_last_services(conn, hostname, masters, options): - """ - 1. When deleting master, check if there will be at least one remaining - DNS and CA server. - 2. Pick CA renewal master - - Return this_services, other_services, ca_hostname - """ - - this_services = [] - other_services = [] - ca_hostname = None - - for master in masters: - master_cn = master['cn'][0] - try: - services = conn.get_entries(master['dn'], conn.SCOPE_ONELEVEL) - except errors.NotFound: - continue - services_cns = [s.single_value['cn'] for s in services] - if master_cn == hostname: - this_services = services_cns - else: - other_services.append(services_cns) - if ca_hostname is None and 'CA' in services_cns: - ca_hostname = master_cn - - if 'CA' in this_services and not any(['CA' in o for o in other_services]): - print("Deleting this server is not allowed as it would leave your installation without a CA.") - sys.exit(1) - - other_dns = True - if 'DNS' in this_services and not any(['DNS' in o for o in other_services]): - other_dns = False - print("Deleting this server will leave your installation without a DNS.") - if not options.force and not ipautil.user_input("Continue to delete?", False): - sys.exit("Deletion aborted") - - # test if replica is not DNSSEC master - # allow to delete it if is last DNS server - if 'DNS' in this_services and other_dns and not options.force: - dnssec_masters = opendnssecinstance.get_dnssec_key_masters(conn) - if hostname in dnssec_masters: - print("Replica is active DNSSEC key master. Uninstall could break your DNS system.") - print("Please disable or replace DNSSEC key master first.") - sys.exit("Deletion aborted") - - ca = cainstance.CAInstance(api.env.realm) - if ca.is_renewal_master(hostname): - try: - ca.set_renewal_master(options.host) - except errors.NotFound: - ca.set_renewal_master(ca_hostname) - - return this_services, other_services, ca_hostname - - -def cleanup_server_dns_entries(realm, hostname, suffix, options): - try: - if bindinstance.dns_container_exists(suffix): - bindinstance.remove_master_dns_records(hostname, realm) - dnskeysyncinstance.remove_replica_public_keys(hostname) - except Exception as e: - print("Failed to cleanup %s DNS entries: %s" % (hostname, e)) - print("You may need to manually remove them from the tree") - - -def del_master(realm, hostname, options): - - if has_managed_topology(api): - del_master_managed(realm, hostname, options) - else: - del_master_direct(realm, hostname, options) - -def del_master_managed(realm, hostname, options): - """ - Removing of master in managed_topology - """ - - hostname_u = ipautil.fsdecode(hostname) - if hostname == options.host: - print("Can't remove itself: %s" % (options.host)) - sys.exit(1) - - server_del_options = dict( - force=options.cleanup, - ignore_topology_disconnect=options.force, - ignore_last_of_role=options.force - ) - - try: - replication.run_server_del_as_cli( - api, hostname_u, **server_del_options) - except Exception as e: - sys.exit(e) - - -def del_master_direct(realm, hostname, options): - """ - Removing of master for realm without managed topology - (domain level < DOMAIN_LEVEL_1) - """ - - force_del = False - delrepl = None - - # 1. Connect to the local server - try: - thisrepl = replication.ReplicationManager(realm, options.host, - options.dirman_passwd) - except Exception as e: - print("Failed to connect to server %s: %s" % (options.host, e)) - sys.exit(1) - - # 2. Ensure we have an agreement with the master - agreement = thisrepl.get_replication_agreement(hostname) - if agreement is None: - if options.cleanup: - """ - We have no agreement with the current master, so this is a - candidate for cleanup. This is VERY dangerous to do because it - removes that master from the list of masters. If the master - were to try to come back online it wouldn't work at all. - """ - print("Cleaning a master is irreversible.") - print("This should not normally be require, so use cautiously.") - if not ipautil.user_input("Continue to clean master?", False): - sys.exit("Cleanup aborted") - thisrepl.replica_cleanup(hostname, realm, force=True) - sys.exit(0) - else: - sys.exit("'%s' has no replication agreement for '%s'" % (options.host, hostname)) - - # 3. If an IPA agreement connect to the master to be removed. - repltype = thisrepl.get_agreement_type(hostname) - if repltype == replication.IPA_REPLICA: - winsync = False - try: - delrepl = replication.ReplicationManager(realm, hostname, options.dirman_passwd) - except Exception as e: - print("Connection to '%s' failed: %s" % (hostname, e)) - if not options.force: - print("Unable to delete replica '%s'" % hostname) - sys.exit(1) - else: - print("Forcing removal of %s" % hostname) - force_del = True - - if force_del: - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), thisrepl.suffix) - entries = thisrepl.conn.get_entries( - dn, thisrepl.conn.SCOPE_ONELEVEL) - replica_names = [] - for entry in entries: - replica_names.append(entry.single_value['cn']) - # The host we're removing gets included in this list, remove it. - # Otherwise we try to delete an agreement from the host to itself. - try: - replica_names.remove(hostname) - except ValueError: - pass - else: - # Get list of agreements. - replica_entries = delrepl.find_ipa_replication_agreements() - replica_names = [rep.single_value.get('nsds5replicahost') - for rep in replica_entries] - else: - # WINSYNC replica, delete agreement from current host - winsync = True - replica_names = [options.host] - - if not winsync and not options.force: - print("Deleting a master is irreversible.") - print("To reconnect to the remote master you will need to prepare " \ - "a new replica file") - print("and re-install.") - if not ipautil.user_input("Continue to delete?", False): - sys.exit("Deletion aborted") - - # Check for orphans if the remote server is up. - if delrepl and not winsync: - try: - masters = api.Command.server_find( - '', sizelimit=0, no_members=False)['result'] - except Exception as e: - masters = [] - print("Failed to read masters data from '%s': %s" % ( - delrepl.hostname, e)) - print("Skipping calculation to determine if one or more masters would be orphaned.") - if not options.force: - sys.exit(1) - - # This only applies if we have more than 2 IPA servers, otherwise - # there is no chance of an orphan. - if len(masters) > 2: - orphaned_server = check_last_link(delrepl, realm, options.dirman_passwd, options.force) - if orphaned_server is not None: - print("Deleting this server will orphan '%s'. " % orphaned_server) - print("You will need to reconfigure your replication topology to delete this server.") - sys.exit(1) - - # 4. Check that we are not leaving the installation without CA and/or DNS - # And pick new CA master. - ensure_last_services(thisrepl.conn, hostname, masters, options) - else: - print("Skipping calculation to determine if one or more masters would be orphaned.") - - # Save the RID value before we start deleting - if repltype == replication.IPA_REPLICA: - rid = get_rid_by_host(realm, options.host, hostname, - options.dirman_passwd, options.nolookup) - - # 4. Remove each agreement - - print("Deleting replication agreements between %s and %s" % (hostname, ', '.join(replica_names))) - for r in replica_names: - try: - if not del_link(realm, r, hostname, options.dirman_passwd, force=True): - print("Unable to remove replication agreement for %s from %s." % (hostname, r)) - except Exception as e: - print(("There were issues removing a connection for %s " - "from %s: %s" % (hostname, r, e))) - - # 5. Clean RUV for the deleted master - if repltype == replication.IPA_REPLICA and rid is not None: - try: - thisrepl.cleanallruv(rid) - except KeyboardInterrupt: - print("Wait for task interrupted. It will continue to run in the background") - - # 6. Finally clean up the removed replica common entries. - try: - thisrepl.replica_cleanup(hostname, realm, force=True) - except Exception as e: - print("Failed to cleanup %s entries: %s" % (hostname, e)) - print("You may need to manually remove them from the tree") - - # 7. And clean up the removed replica DNS entries if any. - cleanup_server_dns_entries(realm, hostname, thisrepl.suffix, options) - -def add_link(realm, replica1, replica2, dirman_passwd, options): - - if not options.nolookup: - for check_host in [replica1,replica2]: - enforce_host_existence(check_host) - - if options.winsync: - if not options.binddn or not options.bindpw or not options.cacert or not options.passsync: - logger.error("The arguments --binddn, --bindpw, --passsync and " - "--cacert are required to create a winsync agreement") - sys.exit(1) - if os.getegid() != 0: - logger.error("winsync agreements need to be created as root") - sys.exit(1) - elif has_managed_topology(api): - exit_on_managed_topology("Creation of IPA replication agreement") - - try: - repl = replication.ReplicationManager(realm, replica1, dirman_passwd) - except errors.NotFound: - print("Cannot find replica '%s'" % replica1) - return - except Exception as e: - print("Failed to connect to '%s': %s" % (replica1, e)) - return - - # See if we already have an agreement with this host - try: - if repl.get_agreement_type(replica2) == replication.WINSYNC: - agreement = repl.get_replication_agreement(replica2) - sys.exit("winsync agreement already exists on subtree %s" % - agreement.single_value.get('nsds7WindowsReplicaSubtree')) - else: - sys.exit("A replication agreement to %s already exists" % replica2) - except errors.NotFound: - pass - - if options.cacert: - # have to install the given CA cert before doing anything else - ds = dsinstance.DsInstance(realm_name=realm) - if not ds.add_ca_cert(options.cacert): - print("Could not load the required CA certificate file [%s]" % options.cacert) - return - else: - print("Added CA certificate %s to certificate database for %s" % (options.cacert, replica1)) - - # need to wait until cacert is installed as that command may restart - # the directory server and kill the connection - try: - repl1 = replication.ReplicationManager(realm, replica1, dirman_passwd) - except errors.NotFound: - print("Cannot find replica '%s'" % replica1) - return - except Exception as e: - print("Failed to connect to '%s': %s" % (replica1, e)) - return - - if options.winsync: - repl1.setup_winsync_replication(replica2, - options.binddn, options.bindpw, - options.passsync, options.win_subtree, - options.cacert) - else: - # Check if the master entry exists for both servers. - # If one of the tree misses one of the entries, it means one of the - # replicas was fully deleted previously and needs to be reinstalled - # from scratch - try: - masters_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), (api.env.basedn)) - master1_dn = DN(('cn', replica1), masters_dn) - master2_dn = DN(('cn', replica2), masters_dn) - - repl1.conn.get_entry(master1_dn) - repl1.conn.get_entry(master2_dn) - - repl2 = replication.ReplicationManager(realm, replica2, dirman_passwd) - repl2.conn.get_entry(master1_dn) - repl2.conn.get_entry(master2_dn) - - except errors.NotFound: - standard_logging_setup(console_format='%(message)s') - - ds = ipadiscovery.IPADiscovery() - ret = ds.search(servers=[replica2]) - - if ret == ipadiscovery.NOT_IPA_SERVER: - sys.exit("Connection unsuccessful: %s is not an IPA Server." % - replica2) - elif ret == 0: # success - sys.exit("Connection unsuccessful: %s is an IPA Server, " - "but it might be unknown, foreign or previously deleted " - "one." % replica2) - else: - sys.exit("Connection to %s unsuccessful." % replica2) - - repl1.setup_gssapi_replication(replica2, DN(('cn', 'Directory Manager')), dirman_passwd) - print("Connected '%s' to '%s'" % (replica1, replica2)) - -def re_initialize(realm, thishost, fromhost, dirman_passwd, nolookup=False): - - if not nolookup: - for check_host in [thishost, fromhost]: - enforce_host_existence(check_host) - - thisrepl = replication.ReplicationManager(realm, thishost, dirman_passwd) - agreement = thisrepl.get_replication_agreement(fromhost) - if agreement is None: - sys.exit("'%s' has no replication agreement for '%s'" % (thishost, fromhost)) - repltype = thisrepl.get_agreement_type(fromhost) - if repltype == replication.WINSYNC: - # With winsync we don't have a "remote" agreement, it is all local - repl = replication.ReplicationManager(realm, thishost, dirman_passwd) - repl.initialize_replication(agreement.dn, repl.conn) - repl.wait_for_repl_init(repl.conn, agreement.dn) - else: - repl = replication.ReplicationManager(realm, fromhost, dirman_passwd) - agreement = repl.get_replication_agreement(thishost) - - try: - thisrepl.enable_agreement(fromhost) - repl.enable_agreement(thishost) - except errors.NotFound as e: - sys.exit(e) - - repl.force_sync(repl.conn, thishost) - - repl.initialize_replication(agreement.dn, repl.conn) - repl.wait_for_repl_init(repl.conn, agreement.dn) - - # If the agreement doesn't have nsDS5ReplicatedAttributeListTotal it means - # we did not replicate memberOf, do so now. - if not agreement.single_value.get('nsDS5ReplicatedAttributeListTotal'): - ds = dsinstance.DsInstance(realm_name=realm) - ds.ldapi = os.getegid() == 0 - ds.init_memberof() - -def force_sync(realm, thishost, fromhost, dirman_passwd, nolookup=False): - - if not nolookup: - for check_host in [thishost, fromhost]: - enforce_host_existence(check_host) - - thisrepl = replication.ReplicationManager(realm, thishost, dirman_passwd) - agreement = thisrepl.get_replication_agreement(fromhost) - if agreement is None: - sys.exit("'%s' has no replication agreement for '%s'" % (thishost, fromhost)) - repltype = thisrepl.get_agreement_type(fromhost) - if repltype == replication.WINSYNC: - # With winsync we don't have a "remote" agreement, it is all local - repl = replication.ReplicationManager(realm, thishost, dirman_passwd) - repl.force_sync(repl.conn, fromhost) - else: - ds = dsinstance.DsInstance(realm_name=realm) - ds.ldapi = os.getegid() == 0 - ds.replica_manage_time_skew(prevent=False) - repl = replication.ReplicationManager(realm, fromhost, dirman_passwd) - repl.force_sync(repl.conn, thishost) - agreement = repl.get_replication_agreement(thishost) - repl.wait_for_repl_init(repl.conn, agreement.dn) - ds.replica_manage_time_skew(prevent=True) - -def show_DNA_ranges(hostname, master, realm, dirman_passwd, nextrange=False, - nolookup=False): - """ - Display the DNA ranges for all current masters. - - hostname: hostname of the master we're listing from - master: specific master to show, or None for all - realm: our realm, needed to create a connection - dirman_passwd: the DM password, needed to create a connection - nextrange: if False then show main range, if True then show next - - Returns nothing - """ - - if not nolookup: - enforce_host_existence(hostname) - if master is not None: - enforce_host_existence(master) - - try: - repl = replication.ReplicationManager(realm, hostname, dirman_passwd) - except Exception as e: - sys.exit("Connection failed: %s" % e) - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix) - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) - except Exception: - return False - - for ent in entries: - remote = ent.single_value['cn'] - if master is not None and remote != master: - continue - try: - repl2 = replication.ReplicationManager(realm, remote, dirman_passwd) - except Exception as e: - print("%s: Connection failed: %s" % (remote, e)) - continue - if not nextrange: - try: - (start, max) = repl2.get_DNA_range(remote) - except errors.NotFound: - print("%s: No permission to read DNA configuration" % remote) - continue - if start is None: - print("%s: No range set" % remote) - else: - print("%s: %s-%s" % (remote, start, max)) - else: - try: - (next_start, next_max) = repl2.get_DNA_next_range(remote) - except errors.NotFound: - print("%s: No permission to read DNA configuration" % remote) - continue - if next_start is None: - print("%s: No on-deck range set" % remote) - else: - print("%s: %s-%s" % (remote, next_start, next_max)) - - return False - - -def store_DNA_range(repl, range_start, range_max, deleted_master, realm, - dirman_passwd): - """ - Given a DNA range try to save it in a remaining master in the - on-deck (dnaNextRange) value. - - Return True if range was saved, False if not - - This function focuses on finding an available master. - - repl: ReplicaMaster object for the master we're deleting from - range_start: The DNA next value - range_max: The DNA max value - deleted_master: The hostname of the master to be deleted - realm: our realm, needed to create a connection - dirman_passwd: the DM password, needed to create a connection - """ - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix) - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) - except Exception: - return False - - for ent in entries: - candidate = ent.single_value['cn'] - if candidate == deleted_master: - continue - try: - repl2 = replication.ReplicationManager(realm, candidate, dirman_passwd) - except Exception as e: - print("Connection failed: %s" % e) - continue - next_start, _next_max = repl2.get_DNA_next_range(candidate) - if next_start is None: - try: - return repl2.save_DNA_next_range(range_start, range_max) - except Exception as e: - print('%s: %s' % (candidate, e)) - - return False - - -def set_DNA_range(hostname, range, realm, dirman_passwd, next_range=False, - nolookup=False): - """ - Given a DNA range try to change it on the designated master. - - The range must not overlap with any other ranges and must be within - one of the IPA local ranges as defined in cn=ranges. - - Setting an on-deck range of 0-0 removes the range. - - Return True if range was saved, False if not - - hostname: hostname of the master to set the range on - range: The DNA range to set - realm: our realm, needed to create a connection - dirman_passwd: the DM password, needed to create a connection - next_range: if True then setting a next-range, otherwise a DNA range. - """ - def validate_range(range, allow_all_zero=False): - """ - Do some basic sanity checking on the range. - - Returns None if ok, a string if an error. - """ - try: - (dna_next, dna_max) = range.split('-', 1) - except ValueError: - return "Invalid range, must be the form x-y" - - try: - dna_next = int(dna_next) - dna_max = int(dna_max) - except ValueError: - return "The range must consist of integers" - - if dna_next == 0 and dna_max == 0 and allow_all_zero: - return None - - if dna_next <= 0 or dna_max <= 0 or dna_next >= MAXINT or dna_max >= MAXINT: - return "The range must consist of positive integers between 1 and %d" % MAXINT - - if dna_next >= dna_max: - return "Invalid range" - - return None - - def range_intersection(s1, s2, r1, r2): - return max(s1, r1) <= min(s2, r2) - - if not nolookup: - enforce_host_existence(hostname) - - err = validate_range(range, allow_all_zero=next_range) - if err is not None: - sys.exit(err) - - # Normalize the range - (dna_next, dna_max) = range.split('-', 1) - dna_next = int(dna_next) - dna_max = int(dna_max) - - try: - repl = replication.ReplicationManager(realm, hostname, dirman_passwd) - except Exception as e: - sys.exit("Connection failed: %s" % e) - if dna_next > 0: - # Verify that the new range doesn't overlap with an existing range - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix) - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) - except Exception as e: - sys.exit("Failed to read master data from '%s': %s" % (repl.conn.host, str(e))) - else: - for ent in entries: - master = ent.single_value['cn'] - if master == hostname and not next_range: - continue - try: - repl2 = replication.ReplicationManager(realm, master, dirman_passwd) - except Exception as e: - print("Connection to %s failed: %s" % (master, e)) - print("Overlap not checked.") - continue - try: - (entry_start, entry_max) = repl2.get_DNA_range(master) - except errors.NotFound: - print("%s: No permission to read DNA configuration" % master) - continue - if (entry_start is not None and - range_intersection(entry_start, entry_max, - dna_next, dna_max)): - sys.exit("New range overlaps the DNA range on %s" % master) - (entry_start, entry_max) = repl2.get_DNA_next_range(master) - if (entry_start is not None and - range_intersection(entry_start, entry_max, - dna_next, dna_max)): - sys.exit("New range overlaps the DNA next range on %s" % master) - del(repl2) - - # Verify that this is within one of the IPA domain ranges. - dn = DN(('cn','ranges'), ('cn','etc'), repl.suffix) - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL, - "(objectclass=ipaDomainIDRange)") - except errors.NotFound as e: - sys.exit('Unable to load IPA ranges: {err}'.format(err=e)) - - for ent in entries: - entry_start = int(ent.single_value['ipabaseid']) - entry_max = entry_start + int(ent.single_value['ipaidrangesize']) - if dna_next >= entry_start and dna_max <= entry_max: - break - else: - sys.exit("New range does not fit within existing IPA ranges. See ipa help idrange command") - - # If this falls within any of the AD ranges then it fails. - try: - entries = repl.conn.get_entries(dn, repl.conn.SCOPE_BASE, - "(objectclass=ipatrustedaddomainrange)") - except errors.NotFound: - entries = [] - - for ent in entries: - entry_start = int(ent.single_value['ipabaseid']) - entry_max = entry_start + int(ent.single_value['ipaidrangesize']) - if range_intersection(dna_next, dna_max, entry_start, entry_max): - sys.exit("New range overlaps with a Trust range. See ipa help idrange command") - - if next_range: - try: - if not repl.save_DNA_next_range(dna_next, dna_max): - sys.exit("Updating next range failed") - except errors.EmptyModlist: - sys.exit("No changes to make") - except errors.NotFound: - sys.exit("No permission to update ranges") - except Exception as e: - sys.exit("Updating next range failed: %s" % e) - else: - try: - if not repl.save_DNA_range(dna_next, dna_max): - sys.exit("Updating range failed") - except errors.EmptyModlist: - sys.exit("No changes to make") - except errors.NotFound: - sys.exit("No permission to update ranges") - except Exception as e: - sys.exit("Updating range failed: %s" % e) - - -def exit_on_managed_topology(what): - sys.exit("{0} is deprecated with managed IPA replication topology. " - "Please use `ipa topologysegment-*` commands to manage " - "the topology.".format(what)) - -def main(options, args): - if os.getegid() == 0: - installutils.check_server_configuration() - elif not os.path.exists(paths.IPA_DEFAULT_CONF): - sys.exit("IPA is not configured on this system.") - - api.bootstrap( - context='cli', confdir=paths.ETC_IPA, - in_server=True, verbose=options.verbose, debug=options.debug - ) - api.finalize() - - dirman_passwd = None - realm = api.env.realm - - if options.host: - host = options.host - else: - host = installutils.get_fqdn() - - options.host = host - - if options.dirman_passwd: - dirman_passwd = options.dirman_passwd - else: - if (not test_connection(realm, host, options.nolookup) or - args[0] in dirman_passwd_req_commands): - dirman_passwd = installutils.read_password("Directory Manager", - confirm=False, validate=False, retry=False) - if dirman_passwd is None or ( - not dirman_passwd and args[0] in dirman_passwd_req_commands): - sys.exit("Directory Manager password required") - - options.dirman_passwd = dirman_passwd - - # Initialize the LDAP connection - api.Backend.ldap2.connect(bind_pw=options.dirman_passwd) - - if args[0] == "list": - replica = None - if len(args) == 2: - replica = args[1] - list_replicas(realm, host, replica, dirman_passwd, options.verbose, - options.nolookup) - elif args[0] == "list-ruv": - list_ruv(realm, host, dirman_passwd, options.verbose, options.nolookup) - elif args[0] == "del": - del_master(realm, args[1], options) - elif args[0] == "re-initialize": - if not options.fromhost: - print("re-initialize requires the option --from ") - sys.exit(1) - re_initialize(realm, host, options.fromhost, dirman_passwd, - options.nolookup) - elif args[0] == "force-sync": - if not options.fromhost: - print("force-sync requires the option --from ") - sys.exit(1) - force_sync(realm, host, options.fromhost, options.dirman_passwd, - options.nolookup) - elif args[0] == "connect": - if len(args) == 3: - replica1 = args[1] - replica2 = args[2] - elif len(args) == 2: - replica1 = host - replica2 = args[1] - add_link(realm, replica1, replica2, dirman_passwd, options) - elif args[0] == "disconnect": - if len(args) == 3: - replica1 = args[1] - replica2 = args[2] - elif len(args) == 2: - replica1 = host - replica2 = args[1] - del_link(realm, replica1, replica2, dirman_passwd) - elif args[0] == "clean-ruv": - clean_ruv(realm, args[1], options) - elif args[0] == "abort-clean-ruv": - abort_clean_ruv(realm, args[1], options) - elif args[0] == "list-clean-ruv": - list_clean_ruv(realm, host, dirman_passwd, options.verbose, - options.nolookup) - elif args[0] == "clean-dangling-ruv": - clean_dangling_ruvs(realm, host, options) - elif args[0] == "dnarange-show": - if len(args) == 2: - master = args[1] - else: - master = None - show_DNA_ranges(host, master, realm, dirman_passwd, False, - options.nolookup) - elif args[0] == "dnanextrange-show": - if len(args) == 2: - master = args[1] - else: - master = None - show_DNA_ranges(host, master, realm, dirman_passwd, True, - options.nolookup) - elif args[0] == "dnarange-set": - set_DNA_range(args[1], args[2], realm, dirman_passwd, next_range=False, - nolookup=options.nolookup) - elif args[0] == "dnanextrange-set": - set_DNA_range(args[1], args[2], realm, dirman_passwd, next_range=True, - nolookup=options.nolookup) - - api.Backend.ldap2.disconnect() - -try: - options, args = parse_options() - main(options, args) -except KeyboardInterrupt: - sys.exit(1) -except SystemExit as e: - sys.exit(e) -except RuntimeError as e: - sys.exit(e) -except socket.timeout: - print("Connection timed out.") - sys.exit(1) -except Exception as e: - if options.verbose: - traceback.print_exc(file=sys.stdout) - else: - print( - "Re-run {} with --verbose option to get more information".format( - sys.argv[0]) - ) - - print("Unexpected error: %s" % str(e)) - sys.exit(1) diff --git a/install/tools/ipa-replica-manage.in b/install/tools/ipa-replica-manage.in new file mode 100644 index 0000000..d69f5ef --- /dev/null +++ b/install/tools/ipa-replica-manage.in @@ -0,0 +1,1641 @@ +@PYTHONSHEBANG@ +# Authors: Karl MacMillan +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import logging +import sys +import os + +import re +import ldap +import socket +import traceback + +# pylint: disable=import-error +from six.moves.urllib.parse import urlparse +from six.moves.xmlrpc_client import MAXINT +# pylint: enable=import-error + +from ipaclient.install import ipadiscovery +from ipapython import ipautil +from ipaserver.install import replication, dsinstance, installutils +from ipaserver.install import bindinstance, cainstance +from ipaserver.install import opendnssecinstance, dnskeysyncinstance +from ipapython import version, ipaldap +from ipalib import api, errors +from ipalib.util import has_managed_topology, verify_host_resolvable +from ipapython.ipa_log_manager import standard_logging_setup +from ipapython.dn import DN +from ipapython.config import IPAOptionParser +from ipaplatform.paths import paths + +logger = logging.getLogger(os.path.basename(__file__)) + +# dict of command name and tuples of min/max num of args needed +commands = { + "list":(0, 1, "[master fqdn]", ""), + "list-ruv":(0, 0, "", ""), + "connect":(1, 2, " [other master fqdn]", + "must provide the name of the servers to connect"), + "disconnect":(1, 2, " [other master fqdn]", + "must provide the name of the server to disconnect"), + "del":(1, 1, "", + "must provide hostname of master to delete"), + "re-initialize":(0, 0, "", ""), + "force-sync":(0, 0, "", ""), + "clean-ruv":(1, 1, "Replica ID of to clean", "must provide replica ID to clean"), + "abort-clean-ruv":(1, 1, "Replica ID to abort cleaning", "must provide replica ID to abort cleaning"), + "list-clean-ruv":(0, 0, "", ""), + "clean-dangling-ruv":(0, 0, "", ""), + "dnarange-show":(0, 1, "[master fqdn]", ""), + "dnanextrange-show":(0, 1, "", ""), + "dnarange-set":(2, 2, " ", "must provide a master and ID range"), + "dnanextrange-set":(2, 2, " ", "must provide a master and ID range"), +} + +# tuple of commands that work with ca tree and need Directory Manager password +dirman_passwd_req_commands = ("list-ruv", "clean-ruv", "abort-clean-ruv", + "clean-dangling-ruv") + + +class NoRUVsFound(Exception): + pass + + +def parse_options(): + parser = IPAOptionParser(version=version.VERSION) + parser.add_option("-H", "--host", dest="host", help="starting host") + parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") + parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="provide additional information") + parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, + help="provide additional debug information") + parser.add_option("-f", "--force", dest="force", action="store_true", default=False, + help="ignore some types of errors") + parser.add_option("-c", "--cleanup", dest="cleanup", action="store_true", default=False, + help="DANGER: clean up references to a ghost master") + parser.add_option("--binddn", dest="binddn", default=None, type="dn", + help="Bind DN to use with remote server") + parser.add_option("--bindpw", dest="bindpw", default=None, + help="Password for Bind DN to use with remote server") + parser.add_option("--winsync", dest="winsync", action="store_true", default=False, + help="This is a Windows Sync Agreement") + parser.add_option("--cacert", dest="cacert", default=None, + help="Full path and filename of CA certificate to use with TLS/SSL to the remote server") + parser.add_option("--win-subtree", dest="win_subtree", default=None, + help="DN of Windows subtree containing the users you want to sync (default cn=Users, v[1]: + err = "too many arguments" + else: + valid_syntax = True + if err: + parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2])) + + if not valid_syntax: + cmdstr = " | ".join(commands.keys()) + parser.error("must provide a command [%s]" % cmdstr) + + return options, args + +def test_connection(realm, host, nolookup=False): + """ + Make a GSSAPI connection to the remote LDAP server to test out credentials. + + This is used so we can fall back to promping for the DM password. + + returns True if connection successful, False otherwise + """ + try: + if not nolookup: + enforce_host_existence(host) + replman = replication.ReplicationManager(realm, host, None) + replman.find_replication_agreements() + del replman + return True + except errors.ACIError: + return False + except errors.NotFound: + # We do a search in cn=config. NotFound in this case means no + # permission + return False + except ldap.LOCAL_ERROR: + # more than likely a GSSAPI error + return False + +def list_replicas(realm, host, replica, dirman_passwd, verbose, nolookup=False): + + if not nolookup: + enforce_host_existence(host) + if replica is not None: + enforce_host_existence(replica) + + is_replica = False + winsync_peer = None + peers = {} + + try: + ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) + conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) + if dirman_passwd: + conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, + bind_password=dirman_passwd) + else: + conn.gssapi_bind() + except Exception as e: + print("Failed to connect to host '%s': %s" % (host, str(e))) + return + + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) + try: + entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) + except Exception as e: + print("Failed to read master data from '%s': %s" % (host, str(e))) + return + else: + for ent in entries: + peers[ent.single_value['cn']] = ['master', ''] + + dn = DN(('cn', 'replicas'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) + try: + entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) + except Exception: + pass + else: + for ent in entries: + config_string = ent.single_value['ipaConfigString'] + peers[ent.single_value['cn']] = config_string.split(':') + + if not replica: + for k, p in peers.items(): + print('%s: %s' % (k, p[0])) + return + + # ok we are being ask for info about a specific replica + for k, p in peers.items(): + if replica == k: + is_replica = True + if p[0] == 'winsync': + winsync_peer = p[1] + + if not is_replica: + print("Cannot find %s in public server list" % replica) + return + + try: + if winsync_peer: + repl = replication.ReplicationManager(realm, winsync_peer, + dirman_passwd) + _cn, dn = repl.agreement_dn(replica) + entries = repl.conn.get_entries( + dn, conn.SCOPE_BASE, + "(objectclass=nsDSWindowsReplicationAgreement)") + ent_type = 'winsync' + else: + repl = replication.ReplicationManager(realm, replica, + dirman_passwd) + entries = repl.find_replication_agreements() + ent_type = 'replica' + except Exception as e: + print("Failed to get data from '%s': %s" % (replica, e)) + return + + for entry in entries: + print('%s: %s' % (entry.single_value.get('nsds5replicahost'), ent_type)) + + if verbose: + print(" last init status: %s" % entry.single_value.get( + 'nsds5replicalastinitstatus')) + print(" last init ended: %s" % str(ipautil.parse_generalized_time( + entry.single_value['nsds5replicalastinitend']))) + print(" last update status: %s" % entry.single_value.get( + 'nsds5replicalastupdatestatus')) + print(" last update ended: %s" % str( + ipautil.parse_generalized_time( + entry.single_value['nsds5replicalastupdateend']))) + +def del_link(realm, replica1, replica2, dirman_passwd, force=False): + """ + Delete a replication agreement from host A to host B. + + @realm: the Kerberos realm + @replica1: the hostname of master A + @replica2: the hostname of master B + @dirman_passwd: the Directory Manager password + @force: force deletion even if one server is down + """ + + repl2 = None + what = "Removal of IPA replication agreement" + managed_topology = has_managed_topology(api) + + try: + repl1 = replication.ReplicationManager(realm, replica1, dirman_passwd) + type1 = repl1.get_agreement_type(replica2) + except errors.NotFound: + # it's possible that the agreement could not have been found because of + # the new topology plugin naming convention: -to- instead of + # meTo. + if managed_topology: + print("'%s' has no winsync replication agreement for '%s'" % (replica1, replica2)) + exit_on_managed_topology(what) + else: + print("'%s' has no replication agreement for '%s'" % (replica1, replica2)) + return False + except Exception as e: + print("Failed to determine agreement type for '%s': %s" % (replica2, e)) + + if type1 == replication.IPA_REPLICA and managed_topology: + exit_on_managed_topology(what) + + repl_list = repl1.find_ipa_replication_agreements() + if not force and len(repl_list) <= 1 and type1 == replication.IPA_REPLICA: + print("Cannot remove the last replication link of '%s'" % replica1) + print("Please use the 'del' command to remove it from the domain") + return False + + if type1 == replication.IPA_REPLICA: + try: + repl2 = replication.ReplicationManager(realm, replica2, dirman_passwd) + + repl_list = repl2.find_ipa_replication_agreements() + if not force and len(repl_list) <= 1: + print("Cannot remove the last replication link of '%s'" % replica2) + print("Please use the 'del' command to remove it from the domain") + return False + + except errors.NotFound: + print("'%s' has no replication agreement for '%s'" % (replica2, replica1)) + if not force: + return False + except Exception as e: + print("Failed to get list of agreements from '%s': %s" % (replica2, e)) + if not force: + return False + + if repl2 and type1 == replication.IPA_REPLICA: + failed = False + try: + repl2.set_readonly(readonly=True) + repl2.force_sync(repl2.conn, replica1) + _cn, dn = repl2.agreement_dn(repl1.conn.host) + repl2.wait_for_repl_update(repl2.conn, dn, 30) + (range_start, range_max) = repl2.get_DNA_range(repl2.conn.host) + (next_start, next_max) = repl2.get_DNA_next_range(repl2.conn.host) + if range_start is not None: + if not store_DNA_range(repl1, range_start, range_max, repl2.conn.host, realm, dirman_passwd): + print("Unable to save DNA range %d-%d" % (range_start, range_max)) + if next_start is not None: + if not store_DNA_range(repl1, next_start, next_max, repl2.conn.host, realm, dirman_passwd): + print("Unable to save DNA range %d-%d" % (next_start, next_max)) + repl2.set_readonly(readonly=False) + repl2.delete_agreement(replica1) + repl2.delete_referral(replica1) + repl2.set_readonly(readonly=False) + except Exception as e: + print("Unable to remove agreement on %s: %s" % (replica2, e)) + failed = True + + if failed: + if force: + print("Forcing removal on '%s'" % replica1) + print("Any DNA range on '%s' will be lost" % replica2) + else: + return False + + if not repl2 and force: + print("Forcing removal on '%s'" % replica1) + print("Any DNA range on '%s' will be lost" % replica2) + + repl1.delete_agreement(replica2) + repl1.delete_referral(replica2) + + if type1 == replication.WINSYNC: + try: + dn = DN(('cn', replica2), ('cn', 'replicas'), ('cn', 'ipa'), ('cn', 'etc'), + ipautil.realm_to_suffix(realm)) + entries = repl1.conn.get_entries(dn, repl1.conn.SCOPE_SUBTREE) + if entries: + entries.sort(key=lambda x: len(x.dn), reverse=True) + for entry in entries: + repl1.conn.delete_entry(entry) + except Exception as e: + print("Error deleting winsync replica shared info: %s" % e) + + print("Deleted replication agreement from '%s' to '%s'" % (replica1, replica2)) + + return True + +def get_ruv(realm, host, dirman_passwd, nolookup=False, ca=False): + """ + Return the RUV entries as a list of tuples: (hostname, rid) + """ + + if not nolookup: + enforce_host_existence(host) + + try: + if ca: + thisrepl = replication.get_cs_replication_manager(realm, host, dirman_passwd) + else: + thisrepl = replication.ReplicationManager(realm, host, dirman_passwd) + except Exception as e: + logger.debug("%s", traceback.format_exc()) + raise RuntimeError("Failed to connect to server {host}: {err}" + .format(host=host, err=e)) + + search_filter = '(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))' + try: + entries = thisrepl.conn.get_entries( + thisrepl.db_suffix, thisrepl.conn.SCOPE_SUBTREE, search_filter, + ['nsds50ruv']) + except errors.NotFound: + logger.debug("%s", traceback.format_exc()) + raise NoRUVsFound("No RUV records found.") + + servers = [] + for e in entries: + for ruv in e['nsds50ruv']: + if ruv.startswith('{replicageneration'): + continue + data = re.match('\{replica (\d+) (ldap://.*:\d+)\}(\s+\w+\s+\w*){0,1}', ruv) + if data: + rid = data.group(1) + ( + _scheme, netloc, _path, _params, _query, _fragment + ) = urlparse(data.group(2)) + servers.append((netloc, rid)) + else: + print("unable to decode: %s" % ruv) + + return servers + + +def get_ruv_both_suffixes(realm, host, dirman_passwd, verbose, nolookup=False): + """ + Get RUVs for both domain and ipaca suffixes + """ + ruvs = {} + fail_gracefully = True + + try: + ruvs['ca'] = get_ruv(realm, host, dirman_passwd, nolookup, True) + except (NoRUVsFound, RuntimeError) as e: + err = "Failed to get CS-RUVs from {host}: {err}".format(host=host, + err=e) + if isinstance(e, RuntimeError): + fail_gracefully = False + if verbose: + print(err) + logger.debug('%s', err) + try: + ruvs['domain'] = get_ruv(realm, host, dirman_passwd, nolookup) + except (NoRUVsFound, RuntimeError) as e: + err = "Failed to get RUVs from {host}: {err}".format(host=host, err=e) + if isinstance(e, RuntimeError): + if not fail_gracefully: + raise + if verbose: + print(err) + logger.debug('%s', err) + + if not ruvs.keys(): + raise NoRUVsFound("No RUV records found.") + + return ruvs + + +def list_ruv(realm, host, dirman_passwd, verbose, nolookup=False): + """ + List the Replica Update Vectors on this host to get the available + replica IDs. + """ + try: + servers = get_ruv_both_suffixes(realm, host, dirman_passwd, + verbose, nolookup) + except (NoRUVsFound, RuntimeError) as e: + print(e) + sys.exit(0 if isinstance(e, NoRUVsFound) else 1) + + print('Replica Update Vectors:') + if servers.get('domain'): + for netloc, rid in servers['domain']: + print("\t{name}: {id}".format(name=netloc, id=rid)) + else: + print('\tNo RUVs found.') + + print('Certificate Server Replica Update Vectors:') + if servers.get('ca'): + for netloc, rid in servers['ca']: + print("\t{name}: {id}".format(name=netloc, id=rid)) + else: + print('\tNo CS-RUVs found.') + + +def get_rid_by_host(realm, sourcehost, host, dirman_passwd, nolookup=False): + """ + Try to determine the RID by host name. + """ + try: + servers = get_ruv(realm, sourcehost, dirman_passwd, nolookup) + except RuntimeError as e: + print(e) + sys.exit(1) + except NoRUVsFound as e: + print(e) + servers = [] + for (netloc, rid) in servers: + if '%s:389' % host == netloc: + return int(rid) + + +def clean_ruv(realm, ruv, options): + """ + Given an RID create a CLEANALLRUV task to clean it up. + """ + try: + ruv = int(ruv) + except ValueError: + sys.exit("Replica ID must be an integer: %s" % ruv) + + try: + servers = get_ruv_both_suffixes(realm, options.host, + options.dirman_passwd, + options.verbose, + options.nolookup) + except (NoRUVsFound, RuntimeError) as e: + print(e) + sys.exit(0 if isinstance(e, NoRUVsFound) else 1) + + tree_found = None + for tree, ruvs in servers.items(): + for netloc, rid in ruvs: + if ruv == int(rid): + tree_found = tree + hostname = netloc + break + if tree_found: + break + + if not tree_found: + sys.exit("Replica ID %s not found" % ruv) + + if tree_found == 'ca': + print("Clean the Certificate Server Replication Update Vector for %s" + % hostname) + else: + print("Clean the Replication Update Vector for %s" % hostname) + + if not options.force: + print() + print("Cleaning the wrong replica ID will cause that server to no") + print("longer replicate so it may miss updates while the process") + print("is running. It would need to be re-initialized to maintain") + print("consistency. Be very careful.") + if not ipautil.user_input("Continue to clean?", False): + sys.exit("Aborted") + + if tree_found == 'ca': + thisrepl = replication.get_cs_replication_manager(realm, options.host, + options.dirman_passwd) + else: + thisrepl = replication.ReplicationManager(realm, options.host, + options.dirman_passwd) + thisrepl.cleanallruv(ruv) + print("Cleanup task created") + + +def abort_clean_ruv(realm, ruv, options): + """ + Given an RID abort a CLEANALLRUV task. + """ + try: + ruv = int(ruv) + except ValueError: + sys.exit("Replica ID must be an integer: %s" % ruv) + + try: + servers = get_ruv_both_suffixes(realm, options.host, + options.dirman_passwd, + options.verbose, + options.nolookup) + except (NoRUVsFound, RuntimeError) as e: + print(e) + sys.exit(0 if isinstance(e, NoRUVsFound) else 1) + + tree_found = None + for tree, ruvs in servers.items(): + for netloc, rid in ruvs: + if ruv == int(rid): + tree_found = tree + hostname = netloc + break + if tree_found: + break + + if not tree_found: + sys.exit("Replica ID %s not found" % ruv) + + print("Aborting the clean Replication Update Vector task for %s" % hostname) + print() + if tree_found == 'ca': + thisrepl = replication.get_cs_replication_manager(realm, options.host, + options.dirman_passwd) + else: + thisrepl = replication.ReplicationManager(realm, options.host, + options.dirman_passwd) + thisrepl.abortcleanallruv(ruv, options.force) + + print("Cleanup task stopped") + + +def list_clean_ruv(realm, host, dirman_passwd, verbose, nolookup=False): + """ + List all clean RUV tasks. + """ + + if not nolookup: + enforce_host_existence(host) + + repl = replication.ReplicationManager(realm, host, dirman_passwd) + dn = DN(('cn', 'cleanallruv'),('cn', 'tasks'), ('cn', 'config')) + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) + except errors.NotFound: + print("No CLEANALLRUV tasks running") + else: + print("CLEANALLRUV tasks") + for entry in entries: + name = entry.single_value['cn'].replace('clean ', '') + status = entry.single_value.get('nsTaskStatus') + print("RID %s: %s" % (name, status)) + if verbose: + print(str(dn)) + print(entry.single_value.get('nstasklog')) + + print() + + dn = DN(('cn', 'abort cleanallruv'),('cn', 'tasks'), ('cn', 'config')) + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) + except errors.NotFound: + print("No abort CLEANALLRUV tasks running") + else: + print("Abort CLEANALLRUV tasks") + for entry in entries: + name = entry.single_value['cn'].replace('abort ', '') + status = entry.single_value.get('nsTaskStatus') + print("RID %s: %s" % (name, status)) + if verbose: + print(str(dn)) + print(entry.single_value.get('nstasklog')) + + +def clean_dangling_ruvs(realm, host, options): + """ + Cleans all RUVs and CS-RUVs that are left in the system from + uninstalled replicas + """ + ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) + conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) + try: + conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, + bind_password=options.dirman_passwd) + + # get all masters + masters_dn = DN(api.env.container_masters, api.env.basedn) + masters = conn.get_entries(masters_dn, conn.SCOPE_ONELEVEL) + info = {} + + # check whether CAs are configured on those masters + for master in masters: + info[master.single_value['cn']] = { + 'online': False, # is the host online? + 'ca': False, # does the host have ca configured? + 'ruvs': set(), # ruvs on the host + 'csruvs': set(), # csruvs on the host + 'clean_ruv': set(), # ruvs to be cleaned from the host + 'clean_csruv': set() # csruvs to be cleaned from the host + } + try: + ca_dn = DN(('cn', 'ca'), master.dn) + conn.get_entry(ca_dn) + info[master.single_value['cn']]['ca'] = True + except errors.NotFound: + continue + + except Exception as e: + sys.exit( + "Failed to get data from '{host}' while trying to " + "list replicas: {error}" + .format(host=host, error=e) + ) + finally: + conn.unbind() + + replica_dn = DN(('cn', 'replica'), ('cn', api.env.basedn), + ('cn', 'mapping tree'), ('cn', 'config')) + + csreplica_dn = DN(('cn', 'replica'), ('cn', 'o=ipaca'), + ('cn', 'mapping tree'), ('cn', 'config')) + + ruvs = set() + csruvs = set() + offlines = set() + for master_cn, master_info in info.items(): + try: + ldap_uri = ipaldap.get_ldap_uri(master_cn, 636, cacert=paths.IPA_CA_CRT) + conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) + conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, + bind_password=options.dirman_passwd) + master_info['online'] = True + except Exception: + print("The server '{host}' appears to be offline." + .format(host=master_cn)) + offlines.add(master_cn) + continue + try: + try: + entry = conn.get_entry(replica_dn) + ruv = (master_cn, entry.single_value.get('nsDS5ReplicaID')) + # the check whether ruv is already in ruvs is performed + # by the set type + ruvs.add(ruv) + except errors.NotFound: + pass + + if master_info['ca']: + try: + entry = conn.get_entry(csreplica_dn) + csruv = (master_cn, + entry.single_value.get('nsDS5ReplicaID')) + csruvs.add(csruv) + except errors.NotFound: + pass + + try: + ruv_dict = get_ruv_both_suffixes(realm, master_cn, + options.dirman_passwd, + options.verbose, + options.nolookup) + except (RuntimeError, NoRUVsFound): + continue + + # get_ruv_both_suffixes returns server names with :port + # This needs needs to be split off + if ruv_dict.get('domain'): + master_info['ruvs'] = set([ + (re.sub(':\d+', '', x), y) + for (x, y) in ruv_dict['domain'] + ]) + if ruv_dict.get('ca'): + master_info['csruvs'] = set([ + (re.sub(':\d+', '', x), y) + for (x, y) in ruv_dict['ca'] + ]) + except Exception as e: + sys.exit("Failed to obtain information from '{host}': {error}" + .format(host=master_cn, error=str(e))) + finally: + conn.unbind() + + dangles = False + # get the dangling RUVs + for master_info in info.values(): + if master_info['online']: + for ruv in master_info['ruvs']: + if (ruv not in ruvs) and (ruv[0] not in offlines): + master_info['clean_ruv'].add(ruv) + dangles = True + + # if ca is not configured, there will be no csruvs in master_info + for csruv in master_info['csruvs']: + if (csruv not in csruvs) and (csruv[0] not in offlines): + master_info['clean_csruv'].add(csruv) + dangles = True + + if not dangles: + print('No dangling RUVs found') + sys.exit(0) + + print('These RUVs are dangling and will be removed:') + for master_cn, master_info in info.items(): + if master_info['online'] and (master_info['clean_ruv'] or + master_info['clean_csruv']): + print('Host: {m}'.format(m=master_cn)) + print('\tRUVs:') + for ruv in master_info['clean_ruv']: + print('\t\tid: {id}, hostname: {host}' + .format(id=ruv[1], host=ruv[0])) + + print('\tCS-RUVs:') + for csruv in master_info['clean_csruv']: + print('\t\tid: {id}, hostname: {host}' + .format(id=csruv[1], host=csruv[0])) + + if not options.force and not ipautil.user_input("Proceed with cleaning?", False): + sys.exit("Aborted") + + options.force = True + cleaned = set() + for master_cn, master_info in info.items(): + options.host = master_cn + for ruv in master_info['clean_ruv']: + if ruv[1] not in cleaned: + cleaned.add(ruv[1]) + clean_ruv(realm, ruv[1], options) + for csruv in master_info['clean_csruv']: + if csruv[1] not in cleaned: + cleaned.add(csruv[1]) + clean_ruv(realm, csruv[1], options) + + +def check_last_link(delrepl, realm, dirman_passwd, force): + """ + We don't want to orphan a server when deleting another one. If you have + a topology that looks like this: + + A B + | | + | | + | | + C---- D + + If we try to delete host D it will orphan host B. + + What we need to do is if the master being deleted has only a single + agreement, connect to that master and make sure it has agreements with + more than just this master. + + @delrepl: a ReplicationManager object of the master being deleted + + returns: hostname of orphaned server or None + """ + replica_entries = delrepl.find_ipa_replication_agreements() + + replica_names = [rep.single_value.get('nsds5replicahost') + for rep in replica_entries] + + orphaned = [] + # Connect to each remote server and see what agreements it has + for replica in replica_names: + try: + repl = replication.ReplicationManager(realm, replica, dirman_passwd) + except errors.NetworkError: + print("Unable to validate that '%s' will not be orphaned." % replica) + + if not force and not ipautil.user_input("Continue to delete?", False): + sys.exit("Aborted") + continue + + entries = repl.find_ipa_replication_agreements() + names = [rep.single_value.get('nsds5replicahost') + for rep in entries] + + if len(names) == 1 and names[0] == delrepl.hostname: + orphaned.append(replica) + + if len(orphaned): + return ', '.join(orphaned) + else: + return None + + +def enforce_host_existence(host, message=None): + if host is None: + return + + try: + verify_host_resolvable(host) + except errors.DNSNotARecordError as ex: + if message is None: + message = "Unknown host %s: %s" % (host, ex) + sys.exit(message) + +def ensure_last_services(conn, hostname, masters, options): + """ + 1. When deleting master, check if there will be at least one remaining + DNS and CA server. + 2. Pick CA renewal master + + Return this_services, other_services, ca_hostname + """ + + this_services = [] + other_services = [] + ca_hostname = None + + for master in masters: + master_cn = master['cn'][0] + try: + services = conn.get_entries(master['dn'], conn.SCOPE_ONELEVEL) + except errors.NotFound: + continue + services_cns = [s.single_value['cn'] for s in services] + if master_cn == hostname: + this_services = services_cns + else: + other_services.append(services_cns) + if ca_hostname is None and 'CA' in services_cns: + ca_hostname = master_cn + + if 'CA' in this_services and not any(['CA' in o for o in other_services]): + print("Deleting this server is not allowed as it would leave your installation without a CA.") + sys.exit(1) + + other_dns = True + if 'DNS' in this_services and not any(['DNS' in o for o in other_services]): + other_dns = False + print("Deleting this server will leave your installation without a DNS.") + if not options.force and not ipautil.user_input("Continue to delete?", False): + sys.exit("Deletion aborted") + + # test if replica is not DNSSEC master + # allow to delete it if is last DNS server + if 'DNS' in this_services and other_dns and not options.force: + dnssec_masters = opendnssecinstance.get_dnssec_key_masters(conn) + if hostname in dnssec_masters: + print("Replica is active DNSSEC key master. Uninstall could break your DNS system.") + print("Please disable or replace DNSSEC key master first.") + sys.exit("Deletion aborted") + + ca = cainstance.CAInstance(api.env.realm) + if ca.is_renewal_master(hostname): + try: + ca.set_renewal_master(options.host) + except errors.NotFound: + ca.set_renewal_master(ca_hostname) + + return this_services, other_services, ca_hostname + + +def cleanup_server_dns_entries(realm, hostname, suffix, options): + try: + if bindinstance.dns_container_exists(suffix): + bindinstance.remove_master_dns_records(hostname, realm) + dnskeysyncinstance.remove_replica_public_keys(hostname) + except Exception as e: + print("Failed to cleanup %s DNS entries: %s" % (hostname, e)) + print("You may need to manually remove them from the tree") + + +def del_master(realm, hostname, options): + + if has_managed_topology(api): + del_master_managed(realm, hostname, options) + else: + del_master_direct(realm, hostname, options) + +def del_master_managed(realm, hostname, options): + """ + Removing of master in managed_topology + """ + + hostname_u = ipautil.fsdecode(hostname) + if hostname == options.host: + print("Can't remove itself: %s" % (options.host)) + sys.exit(1) + + server_del_options = dict( + force=options.cleanup, + ignore_topology_disconnect=options.force, + ignore_last_of_role=options.force + ) + + try: + replication.run_server_del_as_cli( + api, hostname_u, **server_del_options) + except Exception as e: + sys.exit(e) + + +def del_master_direct(realm, hostname, options): + """ + Removing of master for realm without managed topology + (domain level < DOMAIN_LEVEL_1) + """ + + force_del = False + delrepl = None + + # 1. Connect to the local server + try: + thisrepl = replication.ReplicationManager(realm, options.host, + options.dirman_passwd) + except Exception as e: + print("Failed to connect to server %s: %s" % (options.host, e)) + sys.exit(1) + + # 2. Ensure we have an agreement with the master + agreement = thisrepl.get_replication_agreement(hostname) + if agreement is None: + if options.cleanup: + """ + We have no agreement with the current master, so this is a + candidate for cleanup. This is VERY dangerous to do because it + removes that master from the list of masters. If the master + were to try to come back online it wouldn't work at all. + """ + print("Cleaning a master is irreversible.") + print("This should not normally be require, so use cautiously.") + if not ipautil.user_input("Continue to clean master?", False): + sys.exit("Cleanup aborted") + thisrepl.replica_cleanup(hostname, realm, force=True) + sys.exit(0) + else: + sys.exit("'%s' has no replication agreement for '%s'" % (options.host, hostname)) + + # 3. If an IPA agreement connect to the master to be removed. + repltype = thisrepl.get_agreement_type(hostname) + if repltype == replication.IPA_REPLICA: + winsync = False + try: + delrepl = replication.ReplicationManager(realm, hostname, options.dirman_passwd) + except Exception as e: + print("Connection to '%s' failed: %s" % (hostname, e)) + if not options.force: + print("Unable to delete replica '%s'" % hostname) + sys.exit(1) + else: + print("Forcing removal of %s" % hostname) + force_del = True + + if force_del: + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), thisrepl.suffix) + entries = thisrepl.conn.get_entries( + dn, thisrepl.conn.SCOPE_ONELEVEL) + replica_names = [] + for entry in entries: + replica_names.append(entry.single_value['cn']) + # The host we're removing gets included in this list, remove it. + # Otherwise we try to delete an agreement from the host to itself. + try: + replica_names.remove(hostname) + except ValueError: + pass + else: + # Get list of agreements. + replica_entries = delrepl.find_ipa_replication_agreements() + replica_names = [rep.single_value.get('nsds5replicahost') + for rep in replica_entries] + else: + # WINSYNC replica, delete agreement from current host + winsync = True + replica_names = [options.host] + + if not winsync and not options.force: + print("Deleting a master is irreversible.") + print("To reconnect to the remote master you will need to prepare " \ + "a new replica file") + print("and re-install.") + if not ipautil.user_input("Continue to delete?", False): + sys.exit("Deletion aborted") + + # Check for orphans if the remote server is up. + if delrepl and not winsync: + try: + masters = api.Command.server_find( + '', sizelimit=0, no_members=False)['result'] + except Exception as e: + masters = [] + print("Failed to read masters data from '%s': %s" % ( + delrepl.hostname, e)) + print("Skipping calculation to determine if one or more masters would be orphaned.") + if not options.force: + sys.exit(1) + + # This only applies if we have more than 2 IPA servers, otherwise + # there is no chance of an orphan. + if len(masters) > 2: + orphaned_server = check_last_link(delrepl, realm, options.dirman_passwd, options.force) + if orphaned_server is not None: + print("Deleting this server will orphan '%s'. " % orphaned_server) + print("You will need to reconfigure your replication topology to delete this server.") + sys.exit(1) + + # 4. Check that we are not leaving the installation without CA and/or DNS + # And pick new CA master. + ensure_last_services(thisrepl.conn, hostname, masters, options) + else: + print("Skipping calculation to determine if one or more masters would be orphaned.") + + # Save the RID value before we start deleting + if repltype == replication.IPA_REPLICA: + rid = get_rid_by_host(realm, options.host, hostname, + options.dirman_passwd, options.nolookup) + + # 4. Remove each agreement + + print("Deleting replication agreements between %s and %s" % (hostname, ', '.join(replica_names))) + for r in replica_names: + try: + if not del_link(realm, r, hostname, options.dirman_passwd, force=True): + print("Unable to remove replication agreement for %s from %s." % (hostname, r)) + except Exception as e: + print(("There were issues removing a connection for %s " + "from %s: %s" % (hostname, r, e))) + + # 5. Clean RUV for the deleted master + if repltype == replication.IPA_REPLICA and rid is not None: + try: + thisrepl.cleanallruv(rid) + except KeyboardInterrupt: + print("Wait for task interrupted. It will continue to run in the background") + + # 6. Finally clean up the removed replica common entries. + try: + thisrepl.replica_cleanup(hostname, realm, force=True) + except Exception as e: + print("Failed to cleanup %s entries: %s" % (hostname, e)) + print("You may need to manually remove them from the tree") + + # 7. And clean up the removed replica DNS entries if any. + cleanup_server_dns_entries(realm, hostname, thisrepl.suffix, options) + +def add_link(realm, replica1, replica2, dirman_passwd, options): + + if not options.nolookup: + for check_host in [replica1,replica2]: + enforce_host_existence(check_host) + + if options.winsync: + if not options.binddn or not options.bindpw or not options.cacert or not options.passsync: + logger.error("The arguments --binddn, --bindpw, --passsync and " + "--cacert are required to create a winsync agreement") + sys.exit(1) + if os.getegid() != 0: + logger.error("winsync agreements need to be created as root") + sys.exit(1) + elif has_managed_topology(api): + exit_on_managed_topology("Creation of IPA replication agreement") + + try: + repl = replication.ReplicationManager(realm, replica1, dirman_passwd) + except errors.NotFound: + print("Cannot find replica '%s'" % replica1) + return + except Exception as e: + print("Failed to connect to '%s': %s" % (replica1, e)) + return + + # See if we already have an agreement with this host + try: + if repl.get_agreement_type(replica2) == replication.WINSYNC: + agreement = repl.get_replication_agreement(replica2) + sys.exit("winsync agreement already exists on subtree %s" % + agreement.single_value.get('nsds7WindowsReplicaSubtree')) + else: + sys.exit("A replication agreement to %s already exists" % replica2) + except errors.NotFound: + pass + + if options.cacert: + # have to install the given CA cert before doing anything else + ds = dsinstance.DsInstance(realm_name=realm) + if not ds.add_ca_cert(options.cacert): + print("Could not load the required CA certificate file [%s]" % options.cacert) + return + else: + print("Added CA certificate %s to certificate database for %s" % (options.cacert, replica1)) + + # need to wait until cacert is installed as that command may restart + # the directory server and kill the connection + try: + repl1 = replication.ReplicationManager(realm, replica1, dirman_passwd) + except errors.NotFound: + print("Cannot find replica '%s'" % replica1) + return + except Exception as e: + print("Failed to connect to '%s': %s" % (replica1, e)) + return + + if options.winsync: + repl1.setup_winsync_replication(replica2, + options.binddn, options.bindpw, + options.passsync, options.win_subtree, + options.cacert) + else: + # Check if the master entry exists for both servers. + # If one of the tree misses one of the entries, it means one of the + # replicas was fully deleted previously and needs to be reinstalled + # from scratch + try: + masters_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), (api.env.basedn)) + master1_dn = DN(('cn', replica1), masters_dn) + master2_dn = DN(('cn', replica2), masters_dn) + + repl1.conn.get_entry(master1_dn) + repl1.conn.get_entry(master2_dn) + + repl2 = replication.ReplicationManager(realm, replica2, dirman_passwd) + repl2.conn.get_entry(master1_dn) + repl2.conn.get_entry(master2_dn) + + except errors.NotFound: + standard_logging_setup(console_format='%(message)s') + + ds = ipadiscovery.IPADiscovery() + ret = ds.search(servers=[replica2]) + + if ret == ipadiscovery.NOT_IPA_SERVER: + sys.exit("Connection unsuccessful: %s is not an IPA Server." % + replica2) + elif ret == 0: # success + sys.exit("Connection unsuccessful: %s is an IPA Server, " + "but it might be unknown, foreign or previously deleted " + "one." % replica2) + else: + sys.exit("Connection to %s unsuccessful." % replica2) + + repl1.setup_gssapi_replication(replica2, DN(('cn', 'Directory Manager')), dirman_passwd) + print("Connected '%s' to '%s'" % (replica1, replica2)) + +def re_initialize(realm, thishost, fromhost, dirman_passwd, nolookup=False): + + if not nolookup: + for check_host in [thishost, fromhost]: + enforce_host_existence(check_host) + + thisrepl = replication.ReplicationManager(realm, thishost, dirman_passwd) + agreement = thisrepl.get_replication_agreement(fromhost) + if agreement is None: + sys.exit("'%s' has no replication agreement for '%s'" % (thishost, fromhost)) + repltype = thisrepl.get_agreement_type(fromhost) + if repltype == replication.WINSYNC: + # With winsync we don't have a "remote" agreement, it is all local + repl = replication.ReplicationManager(realm, thishost, dirman_passwd) + repl.initialize_replication(agreement.dn, repl.conn) + repl.wait_for_repl_init(repl.conn, agreement.dn) + else: + repl = replication.ReplicationManager(realm, fromhost, dirman_passwd) + agreement = repl.get_replication_agreement(thishost) + + try: + thisrepl.enable_agreement(fromhost) + repl.enable_agreement(thishost) + except errors.NotFound as e: + sys.exit(e) + + repl.force_sync(repl.conn, thishost) + + repl.initialize_replication(agreement.dn, repl.conn) + repl.wait_for_repl_init(repl.conn, agreement.dn) + + # If the agreement doesn't have nsDS5ReplicatedAttributeListTotal it means + # we did not replicate memberOf, do so now. + if not agreement.single_value.get('nsDS5ReplicatedAttributeListTotal'): + ds = dsinstance.DsInstance(realm_name=realm) + ds.ldapi = os.getegid() == 0 + ds.init_memberof() + +def force_sync(realm, thishost, fromhost, dirman_passwd, nolookup=False): + + if not nolookup: + for check_host in [thishost, fromhost]: + enforce_host_existence(check_host) + + thisrepl = replication.ReplicationManager(realm, thishost, dirman_passwd) + agreement = thisrepl.get_replication_agreement(fromhost) + if agreement is None: + sys.exit("'%s' has no replication agreement for '%s'" % (thishost, fromhost)) + repltype = thisrepl.get_agreement_type(fromhost) + if repltype == replication.WINSYNC: + # With winsync we don't have a "remote" agreement, it is all local + repl = replication.ReplicationManager(realm, thishost, dirman_passwd) + repl.force_sync(repl.conn, fromhost) + else: + ds = dsinstance.DsInstance(realm_name=realm) + ds.ldapi = os.getegid() == 0 + ds.replica_manage_time_skew(prevent=False) + repl = replication.ReplicationManager(realm, fromhost, dirman_passwd) + repl.force_sync(repl.conn, thishost) + agreement = repl.get_replication_agreement(thishost) + repl.wait_for_repl_init(repl.conn, agreement.dn) + ds.replica_manage_time_skew(prevent=True) + +def show_DNA_ranges(hostname, master, realm, dirman_passwd, nextrange=False, + nolookup=False): + """ + Display the DNA ranges for all current masters. + + hostname: hostname of the master we're listing from + master: specific master to show, or None for all + realm: our realm, needed to create a connection + dirman_passwd: the DM password, needed to create a connection + nextrange: if False then show main range, if True then show next + + Returns nothing + """ + + if not nolookup: + enforce_host_existence(hostname) + if master is not None: + enforce_host_existence(master) + + try: + repl = replication.ReplicationManager(realm, hostname, dirman_passwd) + except Exception as e: + sys.exit("Connection failed: %s" % e) + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix) + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) + except Exception: + return False + + for ent in entries: + remote = ent.single_value['cn'] + if master is not None and remote != master: + continue + try: + repl2 = replication.ReplicationManager(realm, remote, dirman_passwd) + except Exception as e: + print("%s: Connection failed: %s" % (remote, e)) + continue + if not nextrange: + try: + (start, max) = repl2.get_DNA_range(remote) + except errors.NotFound: + print("%s: No permission to read DNA configuration" % remote) + continue + if start is None: + print("%s: No range set" % remote) + else: + print("%s: %s-%s" % (remote, start, max)) + else: + try: + (next_start, next_max) = repl2.get_DNA_next_range(remote) + except errors.NotFound: + print("%s: No permission to read DNA configuration" % remote) + continue + if next_start is None: + print("%s: No on-deck range set" % remote) + else: + print("%s: %s-%s" % (remote, next_start, next_max)) + + return False + + +def store_DNA_range(repl, range_start, range_max, deleted_master, realm, + dirman_passwd): + """ + Given a DNA range try to save it in a remaining master in the + on-deck (dnaNextRange) value. + + Return True if range was saved, False if not + + This function focuses on finding an available master. + + repl: ReplicaMaster object for the master we're deleting from + range_start: The DNA next value + range_max: The DNA max value + deleted_master: The hostname of the master to be deleted + realm: our realm, needed to create a connection + dirman_passwd: the DM password, needed to create a connection + """ + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix) + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) + except Exception: + return False + + for ent in entries: + candidate = ent.single_value['cn'] + if candidate == deleted_master: + continue + try: + repl2 = replication.ReplicationManager(realm, candidate, dirman_passwd) + except Exception as e: + print("Connection failed: %s" % e) + continue + next_start, _next_max = repl2.get_DNA_next_range(candidate) + if next_start is None: + try: + return repl2.save_DNA_next_range(range_start, range_max) + except Exception as e: + print('%s: %s' % (candidate, e)) + + return False + + +def set_DNA_range(hostname, range, realm, dirman_passwd, next_range=False, + nolookup=False): + """ + Given a DNA range try to change it on the designated master. + + The range must not overlap with any other ranges and must be within + one of the IPA local ranges as defined in cn=ranges. + + Setting an on-deck range of 0-0 removes the range. + + Return True if range was saved, False if not + + hostname: hostname of the master to set the range on + range: The DNA range to set + realm: our realm, needed to create a connection + dirman_passwd: the DM password, needed to create a connection + next_range: if True then setting a next-range, otherwise a DNA range. + """ + def validate_range(range, allow_all_zero=False): + """ + Do some basic sanity checking on the range. + + Returns None if ok, a string if an error. + """ + try: + (dna_next, dna_max) = range.split('-', 1) + except ValueError: + return "Invalid range, must be the form x-y" + + try: + dna_next = int(dna_next) + dna_max = int(dna_max) + except ValueError: + return "The range must consist of integers" + + if dna_next == 0 and dna_max == 0 and allow_all_zero: + return None + + if dna_next <= 0 or dna_max <= 0 or dna_next >= MAXINT or dna_max >= MAXINT: + return "The range must consist of positive integers between 1 and %d" % MAXINT + + if dna_next >= dna_max: + return "Invalid range" + + return None + + def range_intersection(s1, s2, r1, r2): + return max(s1, r1) <= min(s2, r2) + + if not nolookup: + enforce_host_existence(hostname) + + err = validate_range(range, allow_all_zero=next_range) + if err is not None: + sys.exit(err) + + # Normalize the range + (dna_next, dna_max) = range.split('-', 1) + dna_next = int(dna_next) + dna_max = int(dna_max) + + try: + repl = replication.ReplicationManager(realm, hostname, dirman_passwd) + except Exception as e: + sys.exit("Connection failed: %s" % e) + if dna_next > 0: + # Verify that the new range doesn't overlap with an existing range + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix) + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL) + except Exception as e: + sys.exit("Failed to read master data from '%s': %s" % (repl.conn.host, str(e))) + else: + for ent in entries: + master = ent.single_value['cn'] + if master == hostname and not next_range: + continue + try: + repl2 = replication.ReplicationManager(realm, master, dirman_passwd) + except Exception as e: + print("Connection to %s failed: %s" % (master, e)) + print("Overlap not checked.") + continue + try: + (entry_start, entry_max) = repl2.get_DNA_range(master) + except errors.NotFound: + print("%s: No permission to read DNA configuration" % master) + continue + if (entry_start is not None and + range_intersection(entry_start, entry_max, + dna_next, dna_max)): + sys.exit("New range overlaps the DNA range on %s" % master) + (entry_start, entry_max) = repl2.get_DNA_next_range(master) + if (entry_start is not None and + range_intersection(entry_start, entry_max, + dna_next, dna_max)): + sys.exit("New range overlaps the DNA next range on %s" % master) + del(repl2) + + # Verify that this is within one of the IPA domain ranges. + dn = DN(('cn','ranges'), ('cn','etc'), repl.suffix) + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL, + "(objectclass=ipaDomainIDRange)") + except errors.NotFound as e: + sys.exit('Unable to load IPA ranges: {err}'.format(err=e)) + + for ent in entries: + entry_start = int(ent.single_value['ipabaseid']) + entry_max = entry_start + int(ent.single_value['ipaidrangesize']) + if dna_next >= entry_start and dna_max <= entry_max: + break + else: + sys.exit("New range does not fit within existing IPA ranges. See ipa help idrange command") + + # If this falls within any of the AD ranges then it fails. + try: + entries = repl.conn.get_entries(dn, repl.conn.SCOPE_BASE, + "(objectclass=ipatrustedaddomainrange)") + except errors.NotFound: + entries = [] + + for ent in entries: + entry_start = int(ent.single_value['ipabaseid']) + entry_max = entry_start + int(ent.single_value['ipaidrangesize']) + if range_intersection(dna_next, dna_max, entry_start, entry_max): + sys.exit("New range overlaps with a Trust range. See ipa help idrange command") + + if next_range: + try: + if not repl.save_DNA_next_range(dna_next, dna_max): + sys.exit("Updating next range failed") + except errors.EmptyModlist: + sys.exit("No changes to make") + except errors.NotFound: + sys.exit("No permission to update ranges") + except Exception as e: + sys.exit("Updating next range failed: %s" % e) + else: + try: + if not repl.save_DNA_range(dna_next, dna_max): + sys.exit("Updating range failed") + except errors.EmptyModlist: + sys.exit("No changes to make") + except errors.NotFound: + sys.exit("No permission to update ranges") + except Exception as e: + sys.exit("Updating range failed: %s" % e) + + +def exit_on_managed_topology(what): + sys.exit("{0} is deprecated with managed IPA replication topology. " + "Please use `ipa topologysegment-*` commands to manage " + "the topology.".format(what)) + +def main(options, args): + if os.getegid() == 0: + installutils.check_server_configuration() + elif not os.path.exists(paths.IPA_DEFAULT_CONF): + sys.exit("IPA is not configured on this system.") + + api.bootstrap( + context='cli', confdir=paths.ETC_IPA, + in_server=True, verbose=options.verbose, debug=options.debug + ) + api.finalize() + + dirman_passwd = None + realm = api.env.realm + + if options.host: + host = options.host + else: + host = installutils.get_fqdn() + + options.host = host + + if options.dirman_passwd: + dirman_passwd = options.dirman_passwd + else: + if (not test_connection(realm, host, options.nolookup) or + args[0] in dirman_passwd_req_commands): + dirman_passwd = installutils.read_password("Directory Manager", + confirm=False, validate=False, retry=False) + if dirman_passwd is None or ( + not dirman_passwd and args[0] in dirman_passwd_req_commands): + sys.exit("Directory Manager password required") + + options.dirman_passwd = dirman_passwd + + # Initialize the LDAP connection + api.Backend.ldap2.connect(bind_pw=options.dirman_passwd) + + if args[0] == "list": + replica = None + if len(args) == 2: + replica = args[1] + list_replicas(realm, host, replica, dirman_passwd, options.verbose, + options.nolookup) + elif args[0] == "list-ruv": + list_ruv(realm, host, dirman_passwd, options.verbose, options.nolookup) + elif args[0] == "del": + del_master(realm, args[1], options) + elif args[0] == "re-initialize": + if not options.fromhost: + print("re-initialize requires the option --from ") + sys.exit(1) + re_initialize(realm, host, options.fromhost, dirman_passwd, + options.nolookup) + elif args[0] == "force-sync": + if not options.fromhost: + print("force-sync requires the option --from ") + sys.exit(1) + force_sync(realm, host, options.fromhost, options.dirman_passwd, + options.nolookup) + elif args[0] == "connect": + if len(args) == 3: + replica1 = args[1] + replica2 = args[2] + elif len(args) == 2: + replica1 = host + replica2 = args[1] + add_link(realm, replica1, replica2, dirman_passwd, options) + elif args[0] == "disconnect": + if len(args) == 3: + replica1 = args[1] + replica2 = args[2] + elif len(args) == 2: + replica1 = host + replica2 = args[1] + del_link(realm, replica1, replica2, dirman_passwd) + elif args[0] == "clean-ruv": + clean_ruv(realm, args[1], options) + elif args[0] == "abort-clean-ruv": + abort_clean_ruv(realm, args[1], options) + elif args[0] == "list-clean-ruv": + list_clean_ruv(realm, host, dirman_passwd, options.verbose, + options.nolookup) + elif args[0] == "clean-dangling-ruv": + clean_dangling_ruvs(realm, host, options) + elif args[0] == "dnarange-show": + if len(args) == 2: + master = args[1] + else: + master = None + show_DNA_ranges(host, master, realm, dirman_passwd, False, + options.nolookup) + elif args[0] == "dnanextrange-show": + if len(args) == 2: + master = args[1] + else: + master = None + show_DNA_ranges(host, master, realm, dirman_passwd, True, + options.nolookup) + elif args[0] == "dnarange-set": + set_DNA_range(args[1], args[2], realm, dirman_passwd, next_range=False, + nolookup=options.nolookup) + elif args[0] == "dnanextrange-set": + set_DNA_range(args[1], args[2], realm, dirman_passwd, next_range=True, + nolookup=options.nolookup) + + api.Backend.ldap2.disconnect() + +try: + options, args = parse_options() + main(options, args) +except KeyboardInterrupt: + sys.exit(1) +except SystemExit as e: + sys.exit(e) +except RuntimeError as e: + sys.exit(e) +except socket.timeout: + print("Connection timed out.") + sys.exit(1) +except Exception as e: + if options.verbose: + traceback.print_exc(file=sys.stdout) + else: + print( + "Re-run {} with --verbose option to get more information".format( + sys.argv[0]) + ) + + print("Unexpected error: %s" % str(e)) + sys.exit(1) diff --git a/install/tools/ipa-replica-prepare b/install/tools/ipa-replica-prepare deleted file mode 100755 index ae551b5..0000000 --- a/install/tools/ipa-replica-prepare +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Petr Viktorin -# -# Copyright (C) 2012 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_replica_prepare import ReplicaPrepare - -ReplicaPrepare.run_cli() diff --git a/install/tools/ipa-replica-prepare.in b/install/tools/ipa-replica-prepare.in new file mode 100644 index 0000000..f02ecaf --- /dev/null +++ b/install/tools/ipa-replica-prepare.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Petr Viktorin +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_replica_prepare import ReplicaPrepare + +ReplicaPrepare.run_cli() diff --git a/install/tools/ipa-restore b/install/tools/ipa-restore deleted file mode 100755 index e742f02..0000000 --- a/install/tools/ipa-restore +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Rob Crittenden -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_restore import Restore - -Restore.run_cli() diff --git a/install/tools/ipa-restore.in b/install/tools/ipa-restore.in new file mode 100644 index 0000000..34d912b --- /dev/null +++ b/install/tools/ipa-restore.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Rob Crittenden +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_restore import Restore + +Restore.run_cli() diff --git a/install/tools/ipa-server-certinstall b/install/tools/ipa-server-certinstall deleted file mode 100755 index 22067e8..0000000 --- a/install/tools/ipa-server-certinstall +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Jan Cholasta -# -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_server_certinstall import ServerCertInstall - -ServerCertInstall.run_cli() diff --git a/install/tools/ipa-server-certinstall.in b/install/tools/ipa-server-certinstall.in new file mode 100644 index 0000000..b461bc7 --- /dev/null +++ b/install/tools/ipa-server-certinstall.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Jan Cholasta +# +# Copyright (C) 2013 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_server_certinstall import ServerCertInstall + +ServerCertInstall.run_cli() diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install deleted file mode 100755 index 2878eb1..0000000 --- a/install/tools/ipa-server-install +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Karl MacMillan -# Simo Sorce -# Rob Crittenden -# -# Copyright (C) 2007-2014 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install import ipa_server_install - -ipa_server_install.run() diff --git a/install/tools/ipa-server-install.in b/install/tools/ipa-server-install.in new file mode 100644 index 0000000..a8f706f --- /dev/null +++ b/install/tools/ipa-server-install.in @@ -0,0 +1,25 @@ +@PYTHONSHEBANG@ +# Authors: Karl MacMillan +# Simo Sorce +# Rob Crittenden +# +# Copyright (C) 2007-2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install import ipa_server_install + +ipa_server_install.run() diff --git a/install/tools/ipa-server-upgrade b/install/tools/ipa-server-upgrade deleted file mode 100755 index a3a36cd..0000000 --- a/install/tools/ipa-server-upgrade +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -# Documentation can be found at: -# http://freeipa.org/page/LdapUpdate -# http://www.freeipa.org/page/V4/Server_Upgrade_Refactoring - -from ipaserver.install.ipa_server_upgrade import ServerUpgrade - -ServerUpgrade.run_cli() diff --git a/install/tools/ipa-server-upgrade.in b/install/tools/ipa-server-upgrade.in new file mode 100644 index 0000000..03a9c12 --- /dev/null +++ b/install/tools/ipa-server-upgrade.in @@ -0,0 +1,12 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +# Documentation can be found at: +# http://freeipa.org/page/LdapUpdate +# http://www.freeipa.org/page/V4/Server_Upgrade_Refactoring + +from ipaserver.install.ipa_server_upgrade import ServerUpgrade + +ServerUpgrade.run_cli() diff --git a/install/tools/ipa-winsync-migrate b/install/tools/ipa-winsync-migrate deleted file mode 100755 index 8b30f84..0000000 --- a/install/tools/ipa-winsync-migrate +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -E -# Authors: Tomas Babej -# -# Copyright (C) 2015 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from ipaserver.install.ipa_winsync_migrate import WinsyncMigrate - -WinsyncMigrate.run_cli() diff --git a/install/tools/ipa-winsync-migrate.in b/install/tools/ipa-winsync-migrate.in new file mode 100644 index 0000000..c8ac19f --- /dev/null +++ b/install/tools/ipa-winsync-migrate.in @@ -0,0 +1,23 @@ +@PYTHONSHEBANG@ +# Authors: Tomas Babej +# +# Copyright (C) 2015 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from ipaserver.install.ipa_winsync_migrate import WinsyncMigrate + +WinsyncMigrate.run_cli() diff --git a/install/tools/ipactl b/install/tools/ipactl deleted file mode 100755 index 82368c4..0000000 --- a/install/tools/ipactl +++ /dev/null @@ -1,604 +0,0 @@ -#!/usr/bin/python3 -# Authors: Simo Sorce -# -# Copyright (C) 2008-2010 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from __future__ import print_function - -import sys -import os -import json - -import ldapurl - -from ipaserver.install import service, installutils -from ipaserver.install.dsinstance import config_dirname -from ipaserver.install.installutils import is_ipa_configured, ScriptError -from ipalib import api, errors -from ipapython.ipaldap import LDAPClient -from ipapython.ipautil import wait_for_open_ports, wait_for_open_socket -from ipapython.ipautil import run -from ipapython import config -from ipaplatform.tasks import tasks -from ipapython.dn import DN -from ipaplatform import services -from ipaplatform.paths import paths - -MSG_HINT_IGNORE_SERVICE_FAILURE = ( - "Hint: You can use --ignore-service-failure option for forced start in " - "case that a non-critical service failed" -) - - -class IpactlError(ScriptError): - pass - -def check_IPA_configuration(): - if not is_ipa_configured(): - # LSB status code 6: program is not configured - raise IpactlError("IPA is not configured " + - "(see man pages of ipa-server-install for help)", 6) - -def deduplicate(lst): - """Remove duplicates and preserve order. - Returns copy of list with preserved order and removed duplicates. - """ - new_lst = [] - s = set(lst) - for i in lst: - if i in s: - s.remove(i) - new_lst.append(i) - - return new_lst - -def is_dirsrv_debugging_enabled(): - """ - Check the 389-ds instance to see if debugging is enabled. - If so we suppress that in our output. - - returns True or False - """ - debugging = False - serverid = installutils.realm_to_serverid(api.env.realm) - dselist = [config_dirname(serverid)] - for dse in dselist: - try: - fd = open(dse + 'dse.ldif', 'r') - except IOError: - continue - lines = fd.readlines() - fd.close() - for line in lines: - if line.lower().startswith('nsslapd-errorlog-level'): - _option, value = line.split(':') - if int(value) > 0: - debugging = True - - return debugging - -def get_capture_output(service, debug): - """ - We want to display any output of a start/stop command with the - exception of 389-ds when debugging is enabled because it outputs - tons and tons of information. - """ - if service == 'dirsrv' and not debug and is_dirsrv_debugging_enabled(): - print(' debugging enabled, suppressing output.') - return True - else: - return False - -def parse_options(): - usage = "%prog start|stop|restart|status\n" - parser = config.IPAOptionParser(usage=usage, - formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information") - parser.add_option("-f", "--force", action="store_true", dest="force", - help="Force IPA to start. Combine options " - "--skip-version-check and --ignore-service-failures") - parser.add_option("--ignore-service-failures", action="store_true", - dest="ignore_service_failures", - help="If any service start fails, do not rollback the " - "services, continue with the operation") - parser.add_option("--skip-version-check", action="store_true", - dest="skip_version_check", default=False, - help="skip version check") - - options, args = parser.parse_args() - safe_options = parser.get_safe_opts(options) - - if options.force: - options.ignore_service_failures = True - options.skip_version_check = True - - return safe_options, options, args - -def emit_err(err): - sys.stderr.write(err + '\n') - - -def version_check(): - try: - installutils.check_version() - except (installutils.UpgradeMissingVersionError, - installutils.UpgradeDataOlderVersionError) as exc: - emit_err("IPA version error: %s" % exc) - except installutils.UpgradeVersionError as e: - emit_err("IPA version error: %s" % e) - else: - return - - emit_err("Automatically running upgrade, for details see {}".format( - paths.IPAUPGRADE_LOG)) - emit_err("Be patient, this may take a few minutes.") - - # Fork out to call ipa-server-upgrade so that logging is sane. - result = run([paths.IPA_SERVER_UPGRADE], raiseonerr=False, - capture_error=True) - if result.returncode != 0: - emit_err("Automatic upgrade failed: %s" % result.error_output) - emit_err("See the upgrade log for more details and/or run {} again". - format(paths.IPA_SERVER_UPGRADE)) - raise IpactlError("Aborting ipactl") - - -def get_config(dirsrv): - base = DN(('cn', api.env.host), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) - srcfilter = '(ipaConfigString=enabledService)' - attrs = ['cn', 'ipaConfigString'] - if not dirsrv.is_running(): - raise IpactlError("Failed to get list of services to probe status:\n" + - "Directory Server is stopped", 3) - - try: - # The start/restart functions already wait for the server to be - # started. What we are doing with this wait is really checking to see - # if the server is listening at all. - lurl = ldapurl.LDAPUrl(api.env.ldap_uri) - if lurl.urlscheme == 'ldapi': - wait_for_open_socket(lurl.hostport, timeout=api.env.startup_timeout) - else: - (host, port) = lurl.hostport.split(':') - wait_for_open_ports(host, [int(port)], timeout=api.env.startup_timeout) - con = LDAPClient(api.env.ldap_uri) - con.external_bind() - res = con.get_entries( - base, - filter=srcfilter, - attrs_list=attrs, - scope=con.SCOPE_SUBTREE, - time_limit=10) - except errors.NetworkError: - # LSB status code 3: program is not running - raise IpactlError("Failed to get list of services to probe status:\n" + - "Directory Server is stopped", 3) - except errors.NotFound: - masters_list = [] - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) - attrs = ['cn'] - try: - entries = con.get_entries(dn, con.SCOPE_ONELEVEL, attrs_list=attrs) - except Exception as e: - masters_list.append("No master found because of error: %s" % str(e)) - else: - for master_entry in entries: - masters_list.append(master_entry.single_value['cn']) - - masters = "\n".join(masters_list) - - raise IpactlError("Failed to get list of services to probe status!\n" - "Configured hostname '%s' does not match any master server in LDAP:\n%s" - % (api.env.host, masters)) - except Exception as e: - raise IpactlError("Unknown error when retrieving list of services from LDAP: " + str(e)) - - svc_list = [] - - for entry in res: - name = entry.single_value['cn'] - for p in entry['ipaConfigString']: - if p.startswith('startOrder '): - try: - order = int(p.split()[1]) - except ValueError: - raise IpactlError("Expected order as integer in: %s:%s" % ( - name, p)) - svc_list.append([order, name]) - - ordered_list = [] - for (order, svc) in sorted(svc_list): - if svc in service.SERVICE_LIST: - ordered_list.append(service.SERVICE_LIST[svc][0]) - return ordered_list - -def get_config_from_file(): - - svc_list = [] - - try: - f = open(tasks.get_svc_list_file(), 'r') - svc_list = json.load(f) - except Exception as e: - raise IpactlError("Unknown error when retrieving list of services from file: " + str(e)) - - # the framework can start/stop a number of related services we are not - # authoritative for, so filter the list through SERVICES_LIST and order it - # accordingly too. - - def_svc_list = [] - for svc in service.SERVICE_LIST: - s = service.SERVICE_LIST[svc] - def_svc_list.append([s[1], s[0]]) - - ordered_list = [] - for _order, svc in sorted(def_svc_list): - if svc in svc_list: - ordered_list.append(svc) - - return ordered_list - - -def stop_services(svc_list): - for svc in svc_list: - svc_off = services.service(svc, api=api) - try: - svc_off.stop(capture_output=False) - except Exception: - pass - - -def stop_dirsrv(dirsrv): - try: - dirsrv.stop(capture_output=False) - except Exception: - pass - - -def ipa_start(options): - - if not options.skip_version_check: - version_check() - else: - print("Skipping version check") - - if os.path.isfile(tasks.get_svc_list_file()): - emit_err("Existing service file detected!") - emit_err("Assuming stale, cleaning and proceeding") - # remove file with list of started services - # This is ok as systemd will just skip services - # that are already running and just return, so that the - # stop() method of the base class will simply fill in the - # service file again - os.unlink(paths.SVC_LIST_FILE) - - dirsrv = services.knownservices.dirsrv - try: - print("Starting Directory Service") - dirsrv.start(capture_output=get_capture_output('dirsrv', options.debug)) - except Exception as e: - raise IpactlError("Failed to start Directory Service: " + str(e)) - - try: - svc_list = get_config(dirsrv) - except Exception as e: - emit_err("Failed to read data from service file: " + str(e)) - emit_err("Shutting down") - - if not options.ignore_service_failures: - stop_dirsrv(dirsrv) - - if isinstance(e, IpactlError): - # do not display any other error message - raise IpactlError(rval=e.rval) # pylint: disable=no-member - else: - raise IpactlError() - - if len(svc_list) == 0: - # no service to start - return - - svc_list = deduplicate(svc_list) - for svc in svc_list: - svchandle = services.service(svc, api=api) - try: - print("Starting %s Service" % svc) - svchandle.start(capture_output=get_capture_output(svc, options.debug)) - except Exception: - emit_err("Failed to start %s Service" % svc) - # if ignore_service_failures is specified, skip rollback and - # continue with the next service - if options.ignore_service_failures: - emit_err("Forced start, ignoring %s Service, continuing normal operation" % svc) - continue - - emit_err("Shutting down") - stop_services(svc_list) - stop_dirsrv(dirsrv) - - emit_err(MSG_HINT_IGNORE_SERVICE_FAILURE) - raise IpactlError("Aborting ipactl") - -def ipa_stop(options): - dirsrv = services.knownservices.dirsrv - try: - svc_list = get_config_from_file() - except Exception as e: - # Issue reading the file ? Let's try to get data from LDAP as a - # fallback - try: - dirsrv.start(capture_output=False) - svc_list = get_config(dirsrv) - except Exception as e: - emit_err("Failed to read data from Directory Service: " + str(e)) - emit_err("Shutting down") - try: - # just try to stop it, do not read a result - dirsrv.stop() - finally: - raise IpactlError() - - svc_list = deduplicate(svc_list) - for svc in reversed(svc_list): - svchandle = services.service(svc, api=api) - try: - print("Stopping %s Service" % svc) - svchandle.stop(capture_output=False) - except Exception: - emit_err("Failed to stop %s Service" % svc) - - try: - print("Stopping Directory Service") - dirsrv.stop(capture_output=False) - except Exception: - raise IpactlError("Failed to stop Directory Service") - - # remove file with list of started services - try: - os.unlink(paths.SVC_LIST_FILE) - except OSError: - pass - - -def ipa_restart(options): - if not options.skip_version_check: - try: - version_check() - except Exception as e: - try: - ipa_stop(options) - except Exception: - # We don't care about errors that happened while stopping. - # We need to raise the upgrade error. - pass - raise e - else: - print("Skipping version check") - - dirsrv = services.knownservices.dirsrv - new_svc_list = [] - dirsrv_restart = True - if not dirsrv.is_running(): - try: - print("Starting Directory Service") - dirsrv.start(capture_output=get_capture_output('dirsrv', options.debug)) - dirsrv_restart = False - except Exception as e: - raise IpactlError("Failed to start Directory Service: " + str(e)) - - try: - new_svc_list = get_config(dirsrv) - except Exception as e: - emit_err("Failed to read data from Directory Service: " + str(e)) - emit_err("Shutting down") - try: - dirsrv.stop(capture_output=False) - except Exception: - pass - if isinstance(e, IpactlError): - # do not display any other error message - raise IpactlError(rval=e.rval) # pylint: disable=no-member - else: - raise IpactlError() - - old_svc_list = [] - try: - old_svc_list = get_config_from_file() - except Exception as e: - emit_err("Failed to get service list from file: " + str(e)) - # fallback to what's in LDAP - old_svc_list = new_svc_list - - # match service to start/stop - svc_list = [] - for s in new_svc_list: - if s in old_svc_list: - svc_list.append(s) - - #remove commons - for s in svc_list: - if s in old_svc_list: - old_svc_list.remove(s) - for s in svc_list: - if s in new_svc_list: - new_svc_list.remove(s) - - if len(old_svc_list) != 0: - # we need to definitely stop some services - old_svc_list = deduplicate(old_svc_list) - for svc in reversed(old_svc_list): - svchandle = services.service(svc, api=api) - try: - print("Stopping %s Service" % svc) - svchandle.stop(capture_output=False) - except Exception: - emit_err("Failed to stop %s Service" % svc) - - try: - if dirsrv_restart: - print("Restarting Directory Service") - dirsrv.restart(capture_output=get_capture_output('dirsrv', options.debug)) - except Exception as e: - emit_err("Failed to restart Directory Service: " + str(e)) - emit_err("Shutting down") - - if not options.ignore_service_failures: - stop_services(reversed(svc_list)) - stop_dirsrv(dirsrv) - - raise IpactlError("Aborting ipactl") - - if len(svc_list) != 0: - # there are services to restart - svc_list = deduplicate(svc_list) - for svc in svc_list: - svchandle = services.service(svc, api=api) - try: - print("Restarting %s Service" % svc) - svchandle.restart(capture_output=get_capture_output(svc, options.debug)) - except Exception: - emit_err("Failed to restart %s Service" % svc) - # if ignore_service_failures is specified, - # skip rollback and continue with the next service - if options.ignore_service_failures: - emit_err("Forced restart, ignoring %s Service, continuing normal operation" % svc) - continue - - emit_err("Shutting down") - stop_services(svc_list) - stop_dirsrv(dirsrv) - - emit_err(MSG_HINT_IGNORE_SERVICE_FAILURE) - raise IpactlError("Aborting ipactl") - - if len(new_svc_list) != 0: - # we still need to start some services - new_svc_list = deduplicate(new_svc_list) - for svc in new_svc_list: - svchandle = services.service(svc, api=api) - try: - print("Starting %s Service" % svc) - svchandle.start(capture_output=get_capture_output(svc, options.debug)) - except Exception: - emit_err("Failed to start %s Service" % svc) - # if ignore_service_failures is specified, skip rollback and - # continue with the next service - if options.ignore_service_failures: - emit_err("Forced start, ignoring %s Service, continuing normal operation" % svc) - continue - - emit_err("Shutting down") - stop_services(svc_list) - stop_dirsrv(dirsrv) - - emit_err(MSG_HINT_IGNORE_SERVICE_FAILURE) - raise IpactlError("Aborting ipactl") - -def ipa_status(options): - - try: - dirsrv = services.knownservices.dirsrv - if dirsrv.is_running(): - svc_list = get_config(dirsrv) - else: - svc_list = get_config_from_file() - except IpactlError as e: - if os.path.exists(tasks.get_svc_list_file()): - raise e - else: - svc_list = [] - except Exception as e: - raise IpactlError("Failed to get list of services to probe status: " + str(e)) - - dirsrv = services.knownservices.dirsrv - try: - if dirsrv.is_running(): - print("Directory Service: RUNNING") - else: - print("Directory Service: STOPPED") - if len(svc_list) == 0: - print(("Directory Service must be running in order to " + - "obtain status of other services")) - except: - raise IpactlError("Failed to get Directory Service status") - - if len(svc_list) == 0: - return - - svc_list = deduplicate(svc_list) - for svc in svc_list: - svchandle = services.service(svc, api=api) - try: - if svchandle.is_running(): - print("%s Service: RUNNING" % svc) - else: - print("%s Service: STOPPED" % svc) - except Exception: - emit_err("Failed to get %s Service status" % svc) - -def main(): - if not os.getegid() == 0: - # LSB status code 4: user had insufficient privilege - raise IpactlError("You must be root to run ipactl.", 4) - - _safe_options, options, args = parse_options() - - if len(args) != 1: - # LSB status code 2: invalid or excess argument(s) - raise IpactlError("You must specify one action", 2) - elif args[0] != "start" and args[0] != "stop" and args[0] != "restart" and args[0] != "status": - raise IpactlError("Unrecognized action [" + args[0] + "]", 2) - - # check if IPA is configured at all - try: - check_IPA_configuration() - except IpactlError as e: - if args[0].lower() == "status": - # Different LSB return code for status command: - # 4 - program or service status is unknown - # This should differentiate uninstalled IPA from status - # code 3 - program is not running - e.rval = 4 - raise e - else: - raise e - - api.bootstrap(in_server=True, - context='ipactl', - confdir=paths.ETC_IPA, - debug=options.debug) - api.finalize() - - if '.' not in api.env.host: - raise IpactlError("Invalid hostname '%s' in IPA configuration!\n" - "The hostname must be fully-qualified" % api.env.host) - - if args[0].lower() == "start": - ipa_start(options) - elif args[0].lower() == "stop": - ipa_stop(options) - elif args[0].lower() == "restart": - ipa_restart(options) - elif args[0].lower() == "status": - ipa_status(options) - - -if __name__ == '__main__': - installutils.run_script(main, operation_name='ipactl') diff --git a/install/tools/ipactl.in b/install/tools/ipactl.in new file mode 100644 index 0000000..e345dec --- /dev/null +++ b/install/tools/ipactl.in @@ -0,0 +1,604 @@ +@PYTHONSHEBANG@ +# Authors: Simo Sorce +# +# Copyright (C) 2008-2010 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from __future__ import print_function + +import sys +import os +import json + +import ldapurl + +from ipaserver.install import service, installutils +from ipaserver.install.dsinstance import config_dirname +from ipaserver.install.installutils import is_ipa_configured, ScriptError +from ipalib import api, errors +from ipapython.ipaldap import LDAPClient +from ipapython.ipautil import wait_for_open_ports, wait_for_open_socket +from ipapython.ipautil import run +from ipapython import config +from ipaplatform.tasks import tasks +from ipapython.dn import DN +from ipaplatform import services +from ipaplatform.paths import paths + +MSG_HINT_IGNORE_SERVICE_FAILURE = ( + "Hint: You can use --ignore-service-failure option for forced start in " + "case that a non-critical service failed" +) + + +class IpactlError(ScriptError): + pass + +def check_IPA_configuration(): + if not is_ipa_configured(): + # LSB status code 6: program is not configured + raise IpactlError("IPA is not configured " + + "(see man pages of ipa-server-install for help)", 6) + +def deduplicate(lst): + """Remove duplicates and preserve order. + Returns copy of list with preserved order and removed duplicates. + """ + new_lst = [] + s = set(lst) + for i in lst: + if i in s: + s.remove(i) + new_lst.append(i) + + return new_lst + +def is_dirsrv_debugging_enabled(): + """ + Check the 389-ds instance to see if debugging is enabled. + If so we suppress that in our output. + + returns True or False + """ + debugging = False + serverid = installutils.realm_to_serverid(api.env.realm) + dselist = [config_dirname(serverid)] + for dse in dselist: + try: + fd = open(dse + 'dse.ldif', 'r') + except IOError: + continue + lines = fd.readlines() + fd.close() + for line in lines: + if line.lower().startswith('nsslapd-errorlog-level'): + _option, value = line.split(':') + if int(value) > 0: + debugging = True + + return debugging + +def get_capture_output(service, debug): + """ + We want to display any output of a start/stop command with the + exception of 389-ds when debugging is enabled because it outputs + tons and tons of information. + """ + if service == 'dirsrv' and not debug and is_dirsrv_debugging_enabled(): + print(' debugging enabled, suppressing output.') + return True + else: + return False + +def parse_options(): + usage = "%prog start|stop|restart|status\n" + parser = config.IPAOptionParser(usage=usage, + formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information") + parser.add_option("-f", "--force", action="store_true", dest="force", + help="Force IPA to start. Combine options " + "--skip-version-check and --ignore-service-failures") + parser.add_option("--ignore-service-failures", action="store_true", + dest="ignore_service_failures", + help="If any service start fails, do not rollback the " + "services, continue with the operation") + parser.add_option("--skip-version-check", action="store_true", + dest="skip_version_check", default=False, + help="skip version check") + + options, args = parser.parse_args() + safe_options = parser.get_safe_opts(options) + + if options.force: + options.ignore_service_failures = True + options.skip_version_check = True + + return safe_options, options, args + +def emit_err(err): + sys.stderr.write(err + '\n') + + +def version_check(): + try: + installutils.check_version() + except (installutils.UpgradeMissingVersionError, + installutils.UpgradeDataOlderVersionError) as exc: + emit_err("IPA version error: %s" % exc) + except installutils.UpgradeVersionError as e: + emit_err("IPA version error: %s" % e) + else: + return + + emit_err("Automatically running upgrade, for details see {}".format( + paths.IPAUPGRADE_LOG)) + emit_err("Be patient, this may take a few minutes.") + + # Fork out to call ipa-server-upgrade so that logging is sane. + result = run([paths.IPA_SERVER_UPGRADE], raiseonerr=False, + capture_error=True) + if result.returncode != 0: + emit_err("Automatic upgrade failed: %s" % result.error_output) + emit_err("See the upgrade log for more details and/or run {} again". + format(paths.IPA_SERVER_UPGRADE)) + raise IpactlError("Aborting ipactl") + + +def get_config(dirsrv): + base = DN(('cn', api.env.host), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) + srcfilter = '(ipaConfigString=enabledService)' + attrs = ['cn', 'ipaConfigString'] + if not dirsrv.is_running(): + raise IpactlError("Failed to get list of services to probe status:\n" + + "Directory Server is stopped", 3) + + try: + # The start/restart functions already wait for the server to be + # started. What we are doing with this wait is really checking to see + # if the server is listening at all. + lurl = ldapurl.LDAPUrl(api.env.ldap_uri) + if lurl.urlscheme == 'ldapi': + wait_for_open_socket(lurl.hostport, timeout=api.env.startup_timeout) + else: + (host, port) = lurl.hostport.split(':') + wait_for_open_ports(host, [int(port)], timeout=api.env.startup_timeout) + con = LDAPClient(api.env.ldap_uri) + con.external_bind() + res = con.get_entries( + base, + filter=srcfilter, + attrs_list=attrs, + scope=con.SCOPE_SUBTREE, + time_limit=10) + except errors.NetworkError: + # LSB status code 3: program is not running + raise IpactlError("Failed to get list of services to probe status:\n" + + "Directory Server is stopped", 3) + except errors.NotFound: + masters_list = [] + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) + attrs = ['cn'] + try: + entries = con.get_entries(dn, con.SCOPE_ONELEVEL, attrs_list=attrs) + except Exception as e: + masters_list.append("No master found because of error: %s" % str(e)) + else: + for master_entry in entries: + masters_list.append(master_entry.single_value['cn']) + + masters = "\n".join(masters_list) + + raise IpactlError("Failed to get list of services to probe status!\n" + "Configured hostname '%s' does not match any master server in LDAP:\n%s" + % (api.env.host, masters)) + except Exception as e: + raise IpactlError("Unknown error when retrieving list of services from LDAP: " + str(e)) + + svc_list = [] + + for entry in res: + name = entry.single_value['cn'] + for p in entry['ipaConfigString']: + if p.startswith('startOrder '): + try: + order = int(p.split()[1]) + except ValueError: + raise IpactlError("Expected order as integer in: %s:%s" % ( + name, p)) + svc_list.append([order, name]) + + ordered_list = [] + for (order, svc) in sorted(svc_list): + if svc in service.SERVICE_LIST: + ordered_list.append(service.SERVICE_LIST[svc][0]) + return ordered_list + +def get_config_from_file(): + + svc_list = [] + + try: + f = open(tasks.get_svc_list_file(), 'r') + svc_list = json.load(f) + except Exception as e: + raise IpactlError("Unknown error when retrieving list of services from file: " + str(e)) + + # the framework can start/stop a number of related services we are not + # authoritative for, so filter the list through SERVICES_LIST and order it + # accordingly too. + + def_svc_list = [] + for svc in service.SERVICE_LIST: + s = service.SERVICE_LIST[svc] + def_svc_list.append([s[1], s[0]]) + + ordered_list = [] + for _order, svc in sorted(def_svc_list): + if svc in svc_list: + ordered_list.append(svc) + + return ordered_list + + +def stop_services(svc_list): + for svc in svc_list: + svc_off = services.service(svc, api=api) + try: + svc_off.stop(capture_output=False) + except Exception: + pass + + +def stop_dirsrv(dirsrv): + try: + dirsrv.stop(capture_output=False) + except Exception: + pass + + +def ipa_start(options): + + if not options.skip_version_check: + version_check() + else: + print("Skipping version check") + + if os.path.isfile(tasks.get_svc_list_file()): + emit_err("Existing service file detected!") + emit_err("Assuming stale, cleaning and proceeding") + # remove file with list of started services + # This is ok as systemd will just skip services + # that are already running and just return, so that the + # stop() method of the base class will simply fill in the + # service file again + os.unlink(paths.SVC_LIST_FILE) + + dirsrv = services.knownservices.dirsrv + try: + print("Starting Directory Service") + dirsrv.start(capture_output=get_capture_output('dirsrv', options.debug)) + except Exception as e: + raise IpactlError("Failed to start Directory Service: " + str(e)) + + try: + svc_list = get_config(dirsrv) + except Exception as e: + emit_err("Failed to read data from service file: " + str(e)) + emit_err("Shutting down") + + if not options.ignore_service_failures: + stop_dirsrv(dirsrv) + + if isinstance(e, IpactlError): + # do not display any other error message + raise IpactlError(rval=e.rval) # pylint: disable=no-member + else: + raise IpactlError() + + if len(svc_list) == 0: + # no service to start + return + + svc_list = deduplicate(svc_list) + for svc in svc_list: + svchandle = services.service(svc, api=api) + try: + print("Starting %s Service" % svc) + svchandle.start(capture_output=get_capture_output(svc, options.debug)) + except Exception: + emit_err("Failed to start %s Service" % svc) + # if ignore_service_failures is specified, skip rollback and + # continue with the next service + if options.ignore_service_failures: + emit_err("Forced start, ignoring %s Service, continuing normal operation" % svc) + continue + + emit_err("Shutting down") + stop_services(svc_list) + stop_dirsrv(dirsrv) + + emit_err(MSG_HINT_IGNORE_SERVICE_FAILURE) + raise IpactlError("Aborting ipactl") + +def ipa_stop(options): + dirsrv = services.knownservices.dirsrv + try: + svc_list = get_config_from_file() + except Exception as e: + # Issue reading the file ? Let's try to get data from LDAP as a + # fallback + try: + dirsrv.start(capture_output=False) + svc_list = get_config(dirsrv) + except Exception as e: + emit_err("Failed to read data from Directory Service: " + str(e)) + emit_err("Shutting down") + try: + # just try to stop it, do not read a result + dirsrv.stop() + finally: + raise IpactlError() + + svc_list = deduplicate(svc_list) + for svc in reversed(svc_list): + svchandle = services.service(svc, api=api) + try: + print("Stopping %s Service" % svc) + svchandle.stop(capture_output=False) + except Exception: + emit_err("Failed to stop %s Service" % svc) + + try: + print("Stopping Directory Service") + dirsrv.stop(capture_output=False) + except Exception: + raise IpactlError("Failed to stop Directory Service") + + # remove file with list of started services + try: + os.unlink(paths.SVC_LIST_FILE) + except OSError: + pass + + +def ipa_restart(options): + if not options.skip_version_check: + try: + version_check() + except Exception as e: + try: + ipa_stop(options) + except Exception: + # We don't care about errors that happened while stopping. + # We need to raise the upgrade error. + pass + raise e + else: + print("Skipping version check") + + dirsrv = services.knownservices.dirsrv + new_svc_list = [] + dirsrv_restart = True + if not dirsrv.is_running(): + try: + print("Starting Directory Service") + dirsrv.start(capture_output=get_capture_output('dirsrv', options.debug)) + dirsrv_restart = False + except Exception as e: + raise IpactlError("Failed to start Directory Service: " + str(e)) + + try: + new_svc_list = get_config(dirsrv) + except Exception as e: + emit_err("Failed to read data from Directory Service: " + str(e)) + emit_err("Shutting down") + try: + dirsrv.stop(capture_output=False) + except Exception: + pass + if isinstance(e, IpactlError): + # do not display any other error message + raise IpactlError(rval=e.rval) # pylint: disable=no-member + else: + raise IpactlError() + + old_svc_list = [] + try: + old_svc_list = get_config_from_file() + except Exception as e: + emit_err("Failed to get service list from file: " + str(e)) + # fallback to what's in LDAP + old_svc_list = new_svc_list + + # match service to start/stop + svc_list = [] + for s in new_svc_list: + if s in old_svc_list: + svc_list.append(s) + + #remove commons + for s in svc_list: + if s in old_svc_list: + old_svc_list.remove(s) + for s in svc_list: + if s in new_svc_list: + new_svc_list.remove(s) + + if len(old_svc_list) != 0: + # we need to definitely stop some services + old_svc_list = deduplicate(old_svc_list) + for svc in reversed(old_svc_list): + svchandle = services.service(svc, api=api) + try: + print("Stopping %s Service" % svc) + svchandle.stop(capture_output=False) + except Exception: + emit_err("Failed to stop %s Service" % svc) + + try: + if dirsrv_restart: + print("Restarting Directory Service") + dirsrv.restart(capture_output=get_capture_output('dirsrv', options.debug)) + except Exception as e: + emit_err("Failed to restart Directory Service: " + str(e)) + emit_err("Shutting down") + + if not options.ignore_service_failures: + stop_services(reversed(svc_list)) + stop_dirsrv(dirsrv) + + raise IpactlError("Aborting ipactl") + + if len(svc_list) != 0: + # there are services to restart + svc_list = deduplicate(svc_list) + for svc in svc_list: + svchandle = services.service(svc, api=api) + try: + print("Restarting %s Service" % svc) + svchandle.restart(capture_output=get_capture_output(svc, options.debug)) + except Exception: + emit_err("Failed to restart %s Service" % svc) + # if ignore_service_failures is specified, + # skip rollback and continue with the next service + if options.ignore_service_failures: + emit_err("Forced restart, ignoring %s Service, continuing normal operation" % svc) + continue + + emit_err("Shutting down") + stop_services(svc_list) + stop_dirsrv(dirsrv) + + emit_err(MSG_HINT_IGNORE_SERVICE_FAILURE) + raise IpactlError("Aborting ipactl") + + if len(new_svc_list) != 0: + # we still need to start some services + new_svc_list = deduplicate(new_svc_list) + for svc in new_svc_list: + svchandle = services.service(svc, api=api) + try: + print("Starting %s Service" % svc) + svchandle.start(capture_output=get_capture_output(svc, options.debug)) + except Exception: + emit_err("Failed to start %s Service" % svc) + # if ignore_service_failures is specified, skip rollback and + # continue with the next service + if options.ignore_service_failures: + emit_err("Forced start, ignoring %s Service, continuing normal operation" % svc) + continue + + emit_err("Shutting down") + stop_services(svc_list) + stop_dirsrv(dirsrv) + + emit_err(MSG_HINT_IGNORE_SERVICE_FAILURE) + raise IpactlError("Aborting ipactl") + +def ipa_status(options): + + try: + dirsrv = services.knownservices.dirsrv + if dirsrv.is_running(): + svc_list = get_config(dirsrv) + else: + svc_list = get_config_from_file() + except IpactlError as e: + if os.path.exists(tasks.get_svc_list_file()): + raise e + else: + svc_list = [] + except Exception as e: + raise IpactlError("Failed to get list of services to probe status: " + str(e)) + + dirsrv = services.knownservices.dirsrv + try: + if dirsrv.is_running(): + print("Directory Service: RUNNING") + else: + print("Directory Service: STOPPED") + if len(svc_list) == 0: + print(("Directory Service must be running in order to " + + "obtain status of other services")) + except: + raise IpactlError("Failed to get Directory Service status") + + if len(svc_list) == 0: + return + + svc_list = deduplicate(svc_list) + for svc in svc_list: + svchandle = services.service(svc, api=api) + try: + if svchandle.is_running(): + print("%s Service: RUNNING" % svc) + else: + print("%s Service: STOPPED" % svc) + except Exception: + emit_err("Failed to get %s Service status" % svc) + +def main(): + if not os.getegid() == 0: + # LSB status code 4: user had insufficient privilege + raise IpactlError("You must be root to run ipactl.", 4) + + _safe_options, options, args = parse_options() + + if len(args) != 1: + # LSB status code 2: invalid or excess argument(s) + raise IpactlError("You must specify one action", 2) + elif args[0] != "start" and args[0] != "stop" and args[0] != "restart" and args[0] != "status": + raise IpactlError("Unrecognized action [" + args[0] + "]", 2) + + # check if IPA is configured at all + try: + check_IPA_configuration() + except IpactlError as e: + if args[0].lower() == "status": + # Different LSB return code for status command: + # 4 - program or service status is unknown + # This should differentiate uninstalled IPA from status + # code 3 - program is not running + e.rval = 4 + raise e + else: + raise e + + api.bootstrap(in_server=True, + context='ipactl', + confdir=paths.ETC_IPA, + debug=options.debug) + api.finalize() + + if '.' not in api.env.host: + raise IpactlError("Invalid hostname '%s' in IPA configuration!\n" + "The hostname must be fully-qualified" % api.env.host) + + if args[0].lower() == "start": + ipa_start(options) + elif args[0].lower() == "stop": + ipa_stop(options) + elif args[0].lower() == "restart": + ipa_restart(options) + elif args[0].lower() == "status": + ipa_status(options) + + +if __name__ == '__main__': + installutils.run_script(main, operation_name='ipactl') diff --git a/ipa b/ipa deleted file mode 100755 index 4d9498a..0000000 --- a/ipa +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/python3 - -# Authors: -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Command Line Interface for IPA administration. - -The CLI functionality is implemented in ipalib/cli.py -""" -from ipaclient.__main__ import main - -if __name__ == '__main__': - main() diff --git a/ipa.in b/ipa.in new file mode 100644 index 0000000..4cd1793 --- /dev/null +++ b/ipa.in @@ -0,0 +1,30 @@ +@PYTHONSHEBANG@ + +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Command Line Interface for IPA administration. + +The CLI functionality is implemented in ipalib/cli.py +""" +from ipaclient.__main__ import main + +if __name__ == '__main__': + main() diff --git a/ipatests/i18n.py b/ipatests/i18n.py old mode 100755 new mode 100644 index 976232f..d45655c --- a/ipatests/i18n.py +++ b/ipatests/i18n.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # Authors: # John Dennis # diff --git a/makeaci b/makeaci deleted file mode 100755 index 4e92e2d..0000000 --- a/makeaci +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/python3 -# Authors: -# Petr Viktorin -# John Dennis -# Martin Kosek -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Test the managed permission ACIs against a known-good ACI list -# to ensure that changes aren't made lightly. -# Can either regenerate ACI.txt, or validate against it. - -from __future__ import print_function - -import sys -import difflib -from argparse import ArgumentParser - -from ipalib import api -from ipapython.dn import DN -from ipapython.ipaldap import LDAPClient - - -def parse_options(): - parser = ArgumentParser() - parser.add_argument('--validate', dest='validate', action='store_true', - default=False, help='Validate the API vs the stored API') - parser.add_argument('filename', nargs='?', default='ACI.txt', - help='File to create or validate, default: ACI.txt') - - options = parser.parse_args() - return options - - -def generate_aci_lines(api): - """Yields ACI lines as they appear in ACI.txt, with trailing newline""" - update_plugin = api.Updater['update_managed_permissions'] - perm_plugin = api.Object['permission'] - fake_ldap = LDAPClient('', force_schema_updates=False, no_schema=True) - for name, template, obj in update_plugin.get_templates(): - dn = perm_plugin.get_dn(name) - entry = fake_ldap.make_entry(dn) - update_plugin.update_entry( - obj=obj, - entry=entry, - template=template, - anonymous_read_aci=None, - is_new=True, - ) - aci = perm_plugin.make_aci(entry) - yield 'dn: %s\n' % entry.single_value['ipapermlocation'] - yield 'aci: %s\n' % aci - - check_member_attrs(name, template) - - -def check_member_attrs(name, template): - """Check that member* attrs are always present together for read - - ldap2._process_memberofindirect reads all these attributes together; - if the user doesn't have rights to one of them, the entire entry is - left out and memberofindirect processing returns wrong a result. - So we need all of them be readable. - """ - checked_attrs = ['member', 'memberuser', 'memberhost'] - perm_attrs = template.get('ipapermdefaultattr', ()) - flags = [(a in perm_attrs) for a in checked_attrs] - if 'read' in template['ipapermright'] and any(flags) and not all(flags): - raise AssertionError("'%s' includes some but not all of %s" % - (name, checked_attrs)) - - -def main(options): - api.bootstrap( - in_server=True, - in_tree=True, - debug=False, - verbose=0, - validate_api=True, - enable_ra=True, - mode='developer', - plugins_on_demand=False, - basedn=DN('dc=ipa,dc=example'), - realm='IPA.EXAMPLE', - domain="example.com", - ) - - from ipaserver.install.plugins import update_managed_permissions - api.add_module(update_managed_permissions) - - api.finalize() - - output_lines = list(generate_aci_lines(api)) - - if options.validate: - with open(options.filename) as file: - file_lines = file.readlines() - if file_lines != output_lines: - diff = list(difflib.unified_diff( - file_lines, - output_lines, - fromfile='existing %s' % options.filename, - tofile='new result', - )) - for line in diff: - print(line, end=' ') - print(file=sys.stderr) - print('Managed permission ACI validation failed.', file=sys.stderr) - print('Re-check permission changes and run `makeaci`.', file=sys.stderr) - sys.exit('%s validation failed' % options.filename) - else: - with open(options.filename, 'w') as file: - file.writelines(output_lines) - - -if __name__ == '__main__': - options = parse_options() - main(options) diff --git a/makeaci.in b/makeaci.in new file mode 100644 index 0000000..416b8e1 --- /dev/null +++ b/makeaci.in @@ -0,0 +1,132 @@ +@PYTHONSHEBANG@ +# Authors: +# Petr Viktorin +# John Dennis +# Martin Kosek +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Test the managed permission ACIs against a known-good ACI list +# to ensure that changes aren't made lightly. +# Can either regenerate ACI.txt, or validate against it. + +from __future__ import print_function + +import sys +import difflib +from argparse import ArgumentParser + +from ipalib import api +from ipapython.dn import DN +from ipapython.ipaldap import LDAPClient + + +def parse_options(): + parser = ArgumentParser() + parser.add_argument('--validate', dest='validate', action='store_true', + default=False, help='Validate the API vs the stored API') + parser.add_argument('filename', nargs='?', default='ACI.txt', + help='File to create or validate, default: ACI.txt') + + options = parser.parse_args() + return options + + +def generate_aci_lines(api): + """Yields ACI lines as they appear in ACI.txt, with trailing newline""" + update_plugin = api.Updater['update_managed_permissions'] + perm_plugin = api.Object['permission'] + fake_ldap = LDAPClient('', force_schema_updates=False, no_schema=True) + for name, template, obj in update_plugin.get_templates(): + dn = perm_plugin.get_dn(name) + entry = fake_ldap.make_entry(dn) + update_plugin.update_entry( + obj=obj, + entry=entry, + template=template, + anonymous_read_aci=None, + is_new=True, + ) + aci = perm_plugin.make_aci(entry) + yield 'dn: %s\n' % entry.single_value['ipapermlocation'] + yield 'aci: %s\n' % aci + + check_member_attrs(name, template) + + +def check_member_attrs(name, template): + """Check that member* attrs are always present together for read + + ldap2._process_memberofindirect reads all these attributes together; + if the user doesn't have rights to one of them, the entire entry is + left out and memberofindirect processing returns wrong a result. + So we need all of them be readable. + """ + checked_attrs = ['member', 'memberuser', 'memberhost'] + perm_attrs = template.get('ipapermdefaultattr', ()) + flags = [(a in perm_attrs) for a in checked_attrs] + if 'read' in template['ipapermright'] and any(flags) and not all(flags): + raise AssertionError("'%s' includes some but not all of %s" % + (name, checked_attrs)) + + +def main(options): + api.bootstrap( + in_server=True, + in_tree=True, + debug=False, + verbose=0, + validate_api=True, + enable_ra=True, + mode='developer', + plugins_on_demand=False, + basedn=DN('dc=ipa,dc=example'), + realm='IPA.EXAMPLE', + domain="example.com", + ) + + from ipaserver.install.plugins import update_managed_permissions + api.add_module(update_managed_permissions) + + api.finalize() + + output_lines = list(generate_aci_lines(api)) + + if options.validate: + with open(options.filename) as file: + file_lines = file.readlines() + if file_lines != output_lines: + diff = list(difflib.unified_diff( + file_lines, + output_lines, + fromfile='existing %s' % options.filename, + tofile='new result', + )) + for line in diff: + print(line, end=' ') + print(file=sys.stderr) + print('Managed permission ACI validation failed.', file=sys.stderr) + print('Re-check permission changes and run `makeaci`.', file=sys.stderr) + sys.exit('%s validation failed' % options.filename) + else: + with open(options.filename, 'w') as file: + file.writelines(output_lines) + + +if __name__ == '__main__': + options = parse_options() + main(options) diff --git a/makeapi b/makeapi deleted file mode 100755 index abdf6d6..0000000 --- a/makeapi +++ /dev/null @@ -1,544 +0,0 @@ -#!/usr/bin/python3 -# Authors: -# Rob Crittenden -# John Dennis -# Martin Kosek -# -# Copyright (C) 2011 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Test the API against a known-good API to ensure that changes aren't made -# lightly. - -from __future__ import print_function - -import importlib -import itertools -import sys -import os -import re -import inspect -import operator - -from ipalib import api -from ipalib.parameters import Param -from ipalib.output import Output -from ipalib.text import Gettext, NGettext, ConcatenatedLazyText -from ipalib.capabilities import capabilities - -API_FILE='API.txt' - -API_FILE_DIFFERENCE = 1 -API_NEW_COMMAND = 2 -API_NO_FILE = 4 -API_DOC_ERROR = 8 - -# attributes removed from Param.__kw dictionary -PARAM_IGNORED_KW_ATTRIBUTES = ( - 'attribute', - 'cli_metavar', - 'default_from', - 'doc', - 'exclude', - 'exponential', - 'flags', - 'hint', - 'include', - 'label', - 'length', - 'maxlength', - 'maxvalue', - 'minlength', - 'minvalue', - 'noextrawhitespace', - 'normalizer', - 'numberclass', - 'only_absolute', - 'only_relative', - 'pattern', - 'pattern_errmsg', - 'precision', - 'primary_key', - 'require_service', - 'query', - 'sortorder', -) - -# attributes removed from Output object -OUTPUT_IGNORED_ATTRIBUTES = ( - 'doc', - 'flags', -) - -def parse_options(): - from optparse import OptionParser # pylint: disable=deprecated-module - - parser = OptionParser() - parser.add_option("--validate", dest="validate", action="store_true", - default=False, help="Validate the API vs the stored API") - - parser.add_option("--no-validate-doc", dest="validate_doc", action="store_false", - default=True, help="Do not validate documentation") - - options, args = parser.parse_args() - return options, args - -def param_repr(p): - """ - Return parameter repr() for API.txt purposes. - - Some Param attributes do not cause API incompatibility (e.g. doc, - label or callables) and should not be added to API.txt. These attributes - are removed from the parameter before repr() is called. - - NOTE: since the parameter is not not deepcopy()'ed before attributes are - removed, the original parameter is changed in the process. This is OK - for ./makeapi since we don't need this attributes anyway (except for - validate_doc() which is, however, called before any param_repr() call). - """ - if isinstance(p, Output): - for attr in OUTPUT_IGNORED_ATTRIBUTES: - try: - object.__delattr__(p, attr) - except AttributeError: - pass - return repr(p) - elif isinstance(p, Param): - param_kw = p.__dict__['_Param__kw'] - for attr in PARAM_IGNORED_KW_ATTRIBUTES: - try: - del param_kw[attr] - except KeyError: - pass - object.__setattr__(p, 'rules', {}) - return repr(p) - else: - raise ValueError('Unsupported parameter type!') - -def validate_doc(): - """ - Iterate over all API commands and perform the following validation: - - * Every command must have documentation - and it must be marked for international translation - - * Every module hosting a command must have documentation - and it must be marked for international translation - - * Every module topic must be marked for international translation - - For every error found emit a diagnostic. - Emit a summary of total errors found. - - Return error flag if errors found, zero otherwise. - """ - - def is_i18n(obj): - 'Helper utility to determine if object has been internationalized' - return isinstance(obj, (Gettext, NGettext, ConcatenatedLazyText)) - - # The return value - rval = 0 - - # Used to track if we've processed a module already - topics = {} - - # Initialize error counters - n_missing_cmd_doc = 0 - n_missing_cmd_i18n = 0 - n_missing_mod_doc = 0 - n_missing_mod_i18n = 0 - - # Iterate over every command - for cmd in api.Command(): - cmd_class = cmd.__class__ - - # Have we processed this module yet? - topic = cmd.topic - while topic is not None: - if not topics.setdefault(topic, 0): - # First time seeing this module, validate the module contents - doc = None - next_topic = None - - for package in api.packages: - module = '%s.%s' % (package.__name__, topic) - try: - mod = sys.modules[module] - except KeyError: - try: - mod = importlib.import_module(module) - except ImportError: - continue - - if mod.__doc__ is not None: - doc = mod.__doc__ - - # See if there is a module topic, if so validate it - try: - next_topic = mod.topic - except AttributeError: - pass - - # Does the module have documentation? - if doc is None: - src_file = inspect.getsourcefile(mod) - n_missing_mod_doc += 1 - print("%s: module \"%s\" has no doc" % - (src_file, module)) - # Yes the module has doc, but is it internationalized? - elif not is_i18n(doc): - src_file = inspect.getsourcefile(cmd_class) - n_missing_mod_i18n += 1 - print("%s: module \"%s\" doc is not internationalized" % - (src_file, module)) - else: - next_topic = None - - # Increment the count of how many commands in this module - topics[topic] = topics[topic] + 1 - - topic = next_topic - - # Does the command have documentation? - if cmd.doc is None: - src_file = inspect.getsourcefile(cmd_class) - line_num = inspect.getsourcelines(cmd_class)[1] - n_missing_cmd_doc += 1 - print("%s:%d command \"%s\" has no doc" % (src_file, line_num, cmd.name)) - # Yes the command has doc, but is it internationalized? - elif not is_i18n(cmd.doc): - src_file = inspect.getsourcefile(cmd_class) - line_num = inspect.getsourcelines(cmd_class)[1] - n_missing_cmd_i18n += 1 - print("%s:%d command \"%s\" doc is not internationalized" % (src_file, line_num, cmd.name)) - - # If any errors, emit summary information and adjust return value - if n_missing_cmd_doc > 0 or n_missing_cmd_i18n > 0: - rval = API_DOC_ERROR - print("%d commands without doc, %d commands whose doc is not i18n" % \ - (n_missing_cmd_doc, n_missing_cmd_i18n)) - - if n_missing_mod_doc > 0 or n_missing_mod_i18n > 0: - rval = API_DOC_ERROR - print("%d modules without doc, %d modules whose doc is not i18n" % \ - (n_missing_mod_doc, n_missing_mod_i18n)) - - return rval - -def make_api(): - """ - Write a new API file from the current tree. - """ - fd = open(API_FILE, 'w') - for cmd in api.Command(): - fd.write('command: %s\n' % cmd.full_name) - fd.write('args: %d,%d,%d\n' % (len(cmd.args), len(cmd.options), len(cmd.output))) - for a in cmd.args(): - fd.write('arg: %s\n' % param_repr(a)) - for o in sorted(cmd.options(), key=operator.attrgetter('name')): - fd.write('option: %s\n' % param_repr(o)) - for o in sorted(cmd.output(), key=operator.attrgetter('name')): - fd.write('output: %s\n' % param_repr(o)) - for plugin in sorted(itertools.chain(api.Command(), api.Object()), - key=operator.attrgetter('full_name')): - try: - default_plugin = api.Command[plugin.name] - except KeyError: - default_plugin = api.Object[plugin.name] - if plugin is default_plugin: - fd.write('default: %s\n' % plugin.full_name) - for name, version in sorted( - capabilities.items(), key=operator.itemgetter(1, 0)): - fd.write('capability: %s %s\n' % (name, version)) - fd.close() - - return 0 - -def find_name(line): - """ - Break apart a Param line and pull out the name. It would be nice if we - could just eval() the line but we wouldn't have defined any validators - or normalizers it may be using. - """ - m = re.match('^[a-zA-Z0-9]+\(\'([a-z][_a-z0-9?\*\+]*)\'.*', line) - if m: - name = m.group(1) - else: - print("Couldn't find name in: %s" % line) - name = '' - return name - -def _finalize_command_validation(cmd, found_args, expected_args, - found_options, expected_options, - found_output, expected_output): - passed = True - # Check the args of the previous command. - if len(found_args) != expected_args: - print('Argument count in %s of %d doesn\'t match expected: %d' % ( - cmd.name, len(found_args), expected_args)) - passed = False - if len(found_options) != expected_options: - print('Options count in %s of %d doesn\'t match expected: %d' % ( - cmd.name, len(found_options), expected_options)) - passed = False - if len(found_output) != expected_output: - print('Output count in %s of %d doesn\'t match expected: %d' % ( - cmd.name, len(found_output), expected_output)) - passed = False - - # Check if there is not a new arg/opt/output in previous command - for a in cmd.args(): - if a.param_spec not in found_args: - print('Argument %s of command %s in ipalib, not in API file:\n%s' % ( - a.param_spec, cmd.name, param_repr(a))) - passed = False - for o in cmd.options(): - if o.param_spec not in found_options: - print('Option %s of command %s in ipalib, not in API file:\n%s' % ( - o.param_spec, cmd.name, param_repr(o))) - passed = False - for o in cmd.output(): - if o.name not in found_output: - print('Output %s of command %s in ipalib, not in API file:\n%s' % ( - o.name, cmd.name, param_repr(o))) - passed = False - - return passed - -def validate_api(): - """ - Compare the API in the file to the one in ipalib. - - Return a bitwise return code to identify the types of errors found, if - any. - """ - fd = open(API_FILE, 'r') - lines = fd.readlines() - fd.close() - - rval = 0 - - expected_args = 0 - expected_options = 0 - expected_output = 0 - found_args = [] - found_options = [] - found_output = [] - - # First run through the file and compare it to the API - existing_cmds = [] - existing_capabilities = set() - existing_defaults = set() - cmd = None - for line in lines: - line = line.strip() - if line.startswith('command:'): - if cmd: - if not _finalize_command_validation(cmd, found_args, expected_args, - found_options, expected_options, - found_output, expected_output): - rval |= API_FILE_DIFFERENCE - - (arg, name) = line.split(': ', 1) - if name not in api.Command: - print("Command %s in API file, not in ipalib" % name) - rval |= API_FILE_DIFFERENCE - cmd = None - else: - existing_cmds.append(name) - cmd = api.Command[name] - found_args = [] - found_options = [] - found_output = [] - if line.startswith('args:') and cmd: - line = line.replace('args: ', '') - (expected_args, expected_options, expected_output) = line.split(',') - expected_args = int(expected_args) - expected_options = int(expected_options) - expected_output = int(expected_output) - if line.startswith('arg:') and cmd: - line = line.replace('arg: ', '') - found = False - arg = find_name(line) - for a in cmd.args(): - if param_repr(a) == line: - found = True - else: - if a.name == arg: - found = True - print('Arg in %s doesn\'t match.\nGot %s\nExpected %s' % ( - name, param_repr(a), line)) - rval |= API_FILE_DIFFERENCE - if found: - found_args.append(arg) - else: - arg = find_name(line) - print("Argument '%s' in command '%s' in API file not found" % (arg, name)) - rval |= API_FILE_DIFFERENCE - if line.startswith('option:') and cmd: - line = line.replace('option: ', '') - found = False - option = find_name(line) - for o in cmd.options(): - if param_repr(o) == line: - found = True - else: - if o.name == option: - found = True - print('Option in %s doesn\'t match. Got %s Expected %s' % (name, o, line)) - rval |= API_FILE_DIFFERENCE - if found: - found_options.append(option) - else: - option = find_name(line) - print("Option '%s' in command '%s' in API file not found" % (option, name)) - rval |= API_FILE_DIFFERENCE - if line.startswith('output:') and cmd: - line = line.replace('output: ', '') - found = False - output = find_name(line) - for o in cmd.output(): - if param_repr(o) == line: - found = True - else: - if o.name == output: - found = True - print('Output in %s doesn\'t match. Got %s Expected %s' % (name, o, line)) - rval |= API_FILE_DIFFERENCE - if found: - found_output.append(output) - else: - output = find_name(line) - print("Option '%s' in command '%s' in API file not found" % (output, name)) - rval |= API_FILE_DIFFERENCE - if line.startswith('default:'): - default = line.replace('default: ', '') - existing_defaults.add(default) - default_name = None - for namespace in (api.Command, api.Object): - try: - default_name = namespace[default].name - except KeyError: - pass - else: - break - else: - print("Plugin %s in API file, not in ipalib" % default) - rval |= API_FILE_DIFFERENCE - if default_name is not None: - try: - expected_default = namespace[default_name].full_name - except KeyError: - print("Default version of plugin %s in API file not " - "found" % default_name) - rval |= API_FILE_DIFFERENCE - else: - if default != expected_default: - print("Default version of plugin %s in API file " - "doesn't match. Got %s, expected %s." % - (default_name, default, expected_default)) - rval |= API_FILE_DIFFERENCE - if line.startswith('capability:'): - cap, version = line.replace('capability: ', '').split(' ', 1) - existing_capabilities.add(cap) - try: - expected_version = str(capabilities[cap]) - except KeyError: - print("Capability '%s' in API file not found" % cap) - rval |= API_FILE_DIFFERENCE - else: - if version != expected_version: - print(( - "Capability '%s' in API file doesn't match. Got %s, " - "expected %s.") % (cap, version, expected_version)) - rval |= API_FILE_DIFFERENCE - - if cmd: - if not _finalize_command_validation(cmd, found_args, expected_args, - found_options, expected_options, - found_output, expected_output): - rval |= API_FILE_DIFFERENCE - - # Now look for new commands not in the current API - for cmd in api.Command(): - if cmd.full_name not in existing_cmds: - print("Command %s in ipalib, not in API" % cmd.full_name) - rval |= API_NEW_COMMAND - - for namespace in (api.Command, api.Object): - for plugin in namespace(): - if plugin.name in namespace and namespace[plugin.name] is cmd: - if plugin.full_name not in existing_defaults: - print("Default version of command %s in ipalib, not in " - "API" % plugin.name) - rval |= API_FILE_DIFFERENCE - - for cap in capabilities: - if cap not in existing_capabilities: - print("Capability %s in ipalib, not in API" % cap) - rval |= API_FILE_DIFFERENCE - - return rval - -def main(): - rval = 0 - options, _args = parse_options() - - cfg = dict( - in_server=True, - in_tree=True, - debug=False, - verbose=0, - validate_api=True, - enable_ra=True, - mode='developer', - plugins_on_demand=False, - realm="EXAMPLE.COM", - domain="example.com", - ) - - api.bootstrap(**cfg) - api.finalize() - - if options.validate_doc: - rval |= validate_doc() - - if options.validate: - if not os.path.exists(API_FILE): - print('No %s to validate' % API_FILE) - rval |= API_NO_FILE - else: - rval |= validate_api() - else: - print("Writing API to API.txt") - rval |= make_api() - - if rval & API_FILE_DIFFERENCE: - print('') - print('There are one or more changes to the API.\nEither undo the API changes or update API.txt and increment the major version in VERSION.') - - if rval & API_NEW_COMMAND: - print('') - print('There are one or more new commands defined.\nUpdate API.txt and increment the minor version in VERSION.') - - if rval & API_DOC_ERROR: - print('') - print('There are one or more documentation problems.\nYou must fix these before preceeding') - - return rval - -sys.exit(main()) diff --git a/makeapi.in b/makeapi.in new file mode 100644 index 0000000..b37c8c3 --- /dev/null +++ b/makeapi.in @@ -0,0 +1,544 @@ +@PYTHONSHEBANG@ +# Authors: +# Rob Crittenden +# John Dennis +# Martin Kosek +# +# Copyright (C) 2011 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Test the API against a known-good API to ensure that changes aren't made +# lightly. + +from __future__ import print_function + +import importlib +import itertools +import sys +import os +import re +import inspect +import operator + +from ipalib import api +from ipalib.parameters import Param +from ipalib.output import Output +from ipalib.text import Gettext, NGettext, ConcatenatedLazyText +from ipalib.capabilities import capabilities + +API_FILE='API.txt' + +API_FILE_DIFFERENCE = 1 +API_NEW_COMMAND = 2 +API_NO_FILE = 4 +API_DOC_ERROR = 8 + +# attributes removed from Param.__kw dictionary +PARAM_IGNORED_KW_ATTRIBUTES = ( + 'attribute', + 'cli_metavar', + 'default_from', + 'doc', + 'exclude', + 'exponential', + 'flags', + 'hint', + 'include', + 'label', + 'length', + 'maxlength', + 'maxvalue', + 'minlength', + 'minvalue', + 'noextrawhitespace', + 'normalizer', + 'numberclass', + 'only_absolute', + 'only_relative', + 'pattern', + 'pattern_errmsg', + 'precision', + 'primary_key', + 'require_service', + 'query', + 'sortorder', +) + +# attributes removed from Output object +OUTPUT_IGNORED_ATTRIBUTES = ( + 'doc', + 'flags', +) + +def parse_options(): + from optparse import OptionParser # pylint: disable=deprecated-module + + parser = OptionParser() + parser.add_option("--validate", dest="validate", action="store_true", + default=False, help="Validate the API vs the stored API") + + parser.add_option("--no-validate-doc", dest="validate_doc", action="store_false", + default=True, help="Do not validate documentation") + + options, args = parser.parse_args() + return options, args + +def param_repr(p): + """ + Return parameter repr() for API.txt purposes. + + Some Param attributes do not cause API incompatibility (e.g. doc, + label or callables) and should not be added to API.txt. These attributes + are removed from the parameter before repr() is called. + + NOTE: since the parameter is not not deepcopy()'ed before attributes are + removed, the original parameter is changed in the process. This is OK + for ./makeapi since we don't need this attributes anyway (except for + validate_doc() which is, however, called before any param_repr() call). + """ + if isinstance(p, Output): + for attr in OUTPUT_IGNORED_ATTRIBUTES: + try: + object.__delattr__(p, attr) + except AttributeError: + pass + return repr(p) + elif isinstance(p, Param): + param_kw = p.__dict__['_Param__kw'] + for attr in PARAM_IGNORED_KW_ATTRIBUTES: + try: + del param_kw[attr] + except KeyError: + pass + object.__setattr__(p, 'rules', {}) + return repr(p) + else: + raise ValueError('Unsupported parameter type!') + +def validate_doc(): + """ + Iterate over all API commands and perform the following validation: + + * Every command must have documentation + and it must be marked for international translation + + * Every module hosting a command must have documentation + and it must be marked for international translation + + * Every module topic must be marked for international translation + + For every error found emit a diagnostic. + Emit a summary of total errors found. + + Return error flag if errors found, zero otherwise. + """ + + def is_i18n(obj): + 'Helper utility to determine if object has been internationalized' + return isinstance(obj, (Gettext, NGettext, ConcatenatedLazyText)) + + # The return value + rval = 0 + + # Used to track if we've processed a module already + topics = {} + + # Initialize error counters + n_missing_cmd_doc = 0 + n_missing_cmd_i18n = 0 + n_missing_mod_doc = 0 + n_missing_mod_i18n = 0 + + # Iterate over every command + for cmd in api.Command(): + cmd_class = cmd.__class__ + + # Have we processed this module yet? + topic = cmd.topic + while topic is not None: + if not topics.setdefault(topic, 0): + # First time seeing this module, validate the module contents + doc = None + next_topic = None + + for package in api.packages: + module = '%s.%s' % (package.__name__, topic) + try: + mod = sys.modules[module] + except KeyError: + try: + mod = importlib.import_module(module) + except ImportError: + continue + + if mod.__doc__ is not None: + doc = mod.__doc__ + + # See if there is a module topic, if so validate it + try: + next_topic = mod.topic + except AttributeError: + pass + + # Does the module have documentation? + if doc is None: + src_file = inspect.getsourcefile(mod) + n_missing_mod_doc += 1 + print("%s: module \"%s\" has no doc" % + (src_file, module)) + # Yes the module has doc, but is it internationalized? + elif not is_i18n(doc): + src_file = inspect.getsourcefile(cmd_class) + n_missing_mod_i18n += 1 + print("%s: module \"%s\" doc is not internationalized" % + (src_file, module)) + else: + next_topic = None + + # Increment the count of how many commands in this module + topics[topic] = topics[topic] + 1 + + topic = next_topic + + # Does the command have documentation? + if cmd.doc is None: + src_file = inspect.getsourcefile(cmd_class) + line_num = inspect.getsourcelines(cmd_class)[1] + n_missing_cmd_doc += 1 + print("%s:%d command \"%s\" has no doc" % (src_file, line_num, cmd.name)) + # Yes the command has doc, but is it internationalized? + elif not is_i18n(cmd.doc): + src_file = inspect.getsourcefile(cmd_class) + line_num = inspect.getsourcelines(cmd_class)[1] + n_missing_cmd_i18n += 1 + print("%s:%d command \"%s\" doc is not internationalized" % (src_file, line_num, cmd.name)) + + # If any errors, emit summary information and adjust return value + if n_missing_cmd_doc > 0 or n_missing_cmd_i18n > 0: + rval = API_DOC_ERROR + print("%d commands without doc, %d commands whose doc is not i18n" % \ + (n_missing_cmd_doc, n_missing_cmd_i18n)) + + if n_missing_mod_doc > 0 or n_missing_mod_i18n > 0: + rval = API_DOC_ERROR + print("%d modules without doc, %d modules whose doc is not i18n" % \ + (n_missing_mod_doc, n_missing_mod_i18n)) + + return rval + +def make_api(): + """ + Write a new API file from the current tree. + """ + fd = open(API_FILE, 'w') + for cmd in api.Command(): + fd.write('command: %s\n' % cmd.full_name) + fd.write('args: %d,%d,%d\n' % (len(cmd.args), len(cmd.options), len(cmd.output))) + for a in cmd.args(): + fd.write('arg: %s\n' % param_repr(a)) + for o in sorted(cmd.options(), key=operator.attrgetter('name')): + fd.write('option: %s\n' % param_repr(o)) + for o in sorted(cmd.output(), key=operator.attrgetter('name')): + fd.write('output: %s\n' % param_repr(o)) + for plugin in sorted(itertools.chain(api.Command(), api.Object()), + key=operator.attrgetter('full_name')): + try: + default_plugin = api.Command[plugin.name] + except KeyError: + default_plugin = api.Object[plugin.name] + if plugin is default_plugin: + fd.write('default: %s\n' % plugin.full_name) + for name, version in sorted( + capabilities.items(), key=operator.itemgetter(1, 0)): + fd.write('capability: %s %s\n' % (name, version)) + fd.close() + + return 0 + +def find_name(line): + """ + Break apart a Param line and pull out the name. It would be nice if we + could just eval() the line but we wouldn't have defined any validators + or normalizers it may be using. + """ + m = re.match('^[a-zA-Z0-9]+\(\'([a-z][_a-z0-9?\*\+]*)\'.*', line) + if m: + name = m.group(1) + else: + print("Couldn't find name in: %s" % line) + name = '' + return name + +def _finalize_command_validation(cmd, found_args, expected_args, + found_options, expected_options, + found_output, expected_output): + passed = True + # Check the args of the previous command. + if len(found_args) != expected_args: + print('Argument count in %s of %d doesn\'t match expected: %d' % ( + cmd.name, len(found_args), expected_args)) + passed = False + if len(found_options) != expected_options: + print('Options count in %s of %d doesn\'t match expected: %d' % ( + cmd.name, len(found_options), expected_options)) + passed = False + if len(found_output) != expected_output: + print('Output count in %s of %d doesn\'t match expected: %d' % ( + cmd.name, len(found_output), expected_output)) + passed = False + + # Check if there is not a new arg/opt/output in previous command + for a in cmd.args(): + if a.param_spec not in found_args: + print('Argument %s of command %s in ipalib, not in API file:\n%s' % ( + a.param_spec, cmd.name, param_repr(a))) + passed = False + for o in cmd.options(): + if o.param_spec not in found_options: + print('Option %s of command %s in ipalib, not in API file:\n%s' % ( + o.param_spec, cmd.name, param_repr(o))) + passed = False + for o in cmd.output(): + if o.name not in found_output: + print('Output %s of command %s in ipalib, not in API file:\n%s' % ( + o.name, cmd.name, param_repr(o))) + passed = False + + return passed + +def validate_api(): + """ + Compare the API in the file to the one in ipalib. + + Return a bitwise return code to identify the types of errors found, if + any. + """ + fd = open(API_FILE, 'r') + lines = fd.readlines() + fd.close() + + rval = 0 + + expected_args = 0 + expected_options = 0 + expected_output = 0 + found_args = [] + found_options = [] + found_output = [] + + # First run through the file and compare it to the API + existing_cmds = [] + existing_capabilities = set() + existing_defaults = set() + cmd = None + for line in lines: + line = line.strip() + if line.startswith('command:'): + if cmd: + if not _finalize_command_validation(cmd, found_args, expected_args, + found_options, expected_options, + found_output, expected_output): + rval |= API_FILE_DIFFERENCE + + (arg, name) = line.split(': ', 1) + if name not in api.Command: + print("Command %s in API file, not in ipalib" % name) + rval |= API_FILE_DIFFERENCE + cmd = None + else: + existing_cmds.append(name) + cmd = api.Command[name] + found_args = [] + found_options = [] + found_output = [] + if line.startswith('args:') and cmd: + line = line.replace('args: ', '') + (expected_args, expected_options, expected_output) = line.split(',') + expected_args = int(expected_args) + expected_options = int(expected_options) + expected_output = int(expected_output) + if line.startswith('arg:') and cmd: + line = line.replace('arg: ', '') + found = False + arg = find_name(line) + for a in cmd.args(): + if param_repr(a) == line: + found = True + else: + if a.name == arg: + found = True + print('Arg in %s doesn\'t match.\nGot %s\nExpected %s' % ( + name, param_repr(a), line)) + rval |= API_FILE_DIFFERENCE + if found: + found_args.append(arg) + else: + arg = find_name(line) + print("Argument '%s' in command '%s' in API file not found" % (arg, name)) + rval |= API_FILE_DIFFERENCE + if line.startswith('option:') and cmd: + line = line.replace('option: ', '') + found = False + option = find_name(line) + for o in cmd.options(): + if param_repr(o) == line: + found = True + else: + if o.name == option: + found = True + print('Option in %s doesn\'t match. Got %s Expected %s' % (name, o, line)) + rval |= API_FILE_DIFFERENCE + if found: + found_options.append(option) + else: + option = find_name(line) + print("Option '%s' in command '%s' in API file not found" % (option, name)) + rval |= API_FILE_DIFFERENCE + if line.startswith('output:') and cmd: + line = line.replace('output: ', '') + found = False + output = find_name(line) + for o in cmd.output(): + if param_repr(o) == line: + found = True + else: + if o.name == output: + found = True + print('Output in %s doesn\'t match. Got %s Expected %s' % (name, o, line)) + rval |= API_FILE_DIFFERENCE + if found: + found_output.append(output) + else: + output = find_name(line) + print("Option '%s' in command '%s' in API file not found" % (output, name)) + rval |= API_FILE_DIFFERENCE + if line.startswith('default:'): + default = line.replace('default: ', '') + existing_defaults.add(default) + default_name = None + for namespace in (api.Command, api.Object): + try: + default_name = namespace[default].name + except KeyError: + pass + else: + break + else: + print("Plugin %s in API file, not in ipalib" % default) + rval |= API_FILE_DIFFERENCE + if default_name is not None: + try: + expected_default = namespace[default_name].full_name + except KeyError: + print("Default version of plugin %s in API file not " + "found" % default_name) + rval |= API_FILE_DIFFERENCE + else: + if default != expected_default: + print("Default version of plugin %s in API file " + "doesn't match. Got %s, expected %s." % + (default_name, default, expected_default)) + rval |= API_FILE_DIFFERENCE + if line.startswith('capability:'): + cap, version = line.replace('capability: ', '').split(' ', 1) + existing_capabilities.add(cap) + try: + expected_version = str(capabilities[cap]) + except KeyError: + print("Capability '%s' in API file not found" % cap) + rval |= API_FILE_DIFFERENCE + else: + if version != expected_version: + print(( + "Capability '%s' in API file doesn't match. Got %s, " + "expected %s.") % (cap, version, expected_version)) + rval |= API_FILE_DIFFERENCE + + if cmd: + if not _finalize_command_validation(cmd, found_args, expected_args, + found_options, expected_options, + found_output, expected_output): + rval |= API_FILE_DIFFERENCE + + # Now look for new commands not in the current API + for cmd in api.Command(): + if cmd.full_name not in existing_cmds: + print("Command %s in ipalib, not in API" % cmd.full_name) + rval |= API_NEW_COMMAND + + for namespace in (api.Command, api.Object): + for plugin in namespace(): + if plugin.name in namespace and namespace[plugin.name] is cmd: + if plugin.full_name not in existing_defaults: + print("Default version of command %s in ipalib, not in " + "API" % plugin.name) + rval |= API_FILE_DIFFERENCE + + for cap in capabilities: + if cap not in existing_capabilities: + print("Capability %s in ipalib, not in API" % cap) + rval |= API_FILE_DIFFERENCE + + return rval + +def main(): + rval = 0 + options, _args = parse_options() + + cfg = dict( + in_server=True, + in_tree=True, + debug=False, + verbose=0, + validate_api=True, + enable_ra=True, + mode='developer', + plugins_on_demand=False, + realm="EXAMPLE.COM", + domain="example.com", + ) + + api.bootstrap(**cfg) + api.finalize() + + if options.validate_doc: + rval |= validate_doc() + + if options.validate: + if not os.path.exists(API_FILE): + print('No %s to validate' % API_FILE) + rval |= API_NO_FILE + else: + rval |= validate_api() + else: + print("Writing API to API.txt") + rval |= make_api() + + if rval & API_FILE_DIFFERENCE: + print('') + print('There are one or more changes to the API.\nEither undo the API changes or update API.txt and increment the major version in VERSION.') + + if rval & API_NEW_COMMAND: + print('') + print('There are one or more new commands defined.\nUpdate API.txt and increment the minor version in VERSION.') + + if rval & API_DOC_ERROR: + print('') + print('There are one or more documentation problems.\nYou must fix these before preceeding') + + return rval + +sys.exit(main())