diff --git a/roles/ipareplica/module_utils/ansible_ipa_replica.py b/roles/ipareplica/module_utils/ansible_ipa_replica.py index 27ee13d6548981da770f6f243d21d20d9c50beb3..b56ae86136948f05c462baab720be397824bd235 100644 --- a/roles/ipareplica/module_utils/ansible_ipa_replica.py +++ b/roles/ipareplica/module_utils/ansible_ipa_replica.py @@ -5,7 +5,7 @@ # # Based on ipa-replica-install code # -# Copyright (C) 2018 Red Hat +# Copyright (C) 2018-2022 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify @@ -47,41 +47,38 @@ __all__ = ["contextlib", "dnsexception", "dnsresolver", "dnsreversename", "check_domain_level_is_supported", "promotion_check_ipa_domain", "SSSDConfig", "CalledProcessError", "timeconf", "ntpinstance", "dnsname", "kernel_keyring", "krbinstance", "getargspec", - "adtrustinstance"] + "adtrustinstance", "paths", "api", "dsinstance", "ipaldap", "Env", + "ipautil", "installutils", "IPA_PYTHON_VERSION", "NUM_VERSION", + "ReplicaConfig", "create_api"] import sys - -# HACK: workaround for Ansible 2.9 -# https://github.com/ansible/ansible/issues/68361 -if 'ansible.executor' in sys.modules: - for attr in __all__: - setattr(sys.modules[__name__], attr, None) -else: - import logging +import logging + +# Import getargspec from inspect or provide own getargspec for +# Python 2 compatibility with Python 3.11+. +try: + from inspect import getargspec +except ImportError: + from collections import namedtuple + from inspect import getfullargspec + + # The code is copied from Python 3.10 inspect.py + # Authors: Ka-Ping Yee <ping@lfw.org> + # Yury Selivanov <yselivanov@sprymix.com> + ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults') + + def getargspec(func): + args, varargs, varkw, defaults, kwonlyargs, _kwonlydefaults, \ + ann = getfullargspec(func) + if kwonlyargs or ann: + raise ValueError( + "Function has keyword-only parameters or annotations" + ", use inspect.signature() API which can support them") + return ArgSpec(args, varargs, varkw, defaults) + + +try: from contextlib import contextmanager as contextlib_contextmanager - - # Import getargspec from inspect or provide own getargspec for - # Python 2 compatibility with Python 3.11+. - try: - from inspect import getargspec - except ImportError: - from collections import namedtuple - from inspect import getfullargspec - - # The code is copied from Python 3.10 inspect.py - # Authors: Ka-Ping Yee <ping@lfw.org> - # Yury Selivanov <yselivanov@sprymix.com> - ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults') - - def getargspec(func): - args, varargs, varkw, defaults, kwonlyargs, _kwonlydefaults, \ - ann = getfullargspec(func) - if kwonlyargs or ann: - raise ValueError( - "Function has keyword-only parameters or annotations" - ", use inspect.signature() API which can support them") - return ArgSpec(args, varargs, varkw, defaults) - from ipapython.version import NUM_VERSION, VERSION if NUM_VERSION < 30201: @@ -177,296 +174,323 @@ else: raise Exception("freeipa version '%s' is too old" % VERSION) - logger = logging.getLogger("ipa-server-install") +except ImportError as _err: + ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR = str(_err) - def setup_logging(): - # logger.setLevel(logging.DEBUG) - standard_logging_setup( - paths.IPAREPLICA_INSTALL_LOG, verbose=False, debug=False, - filemode='a', console_format='%(message)s') + for attr in __all__: + setattr(sys.modules[__name__], attr, None) + +else: + ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR = None + + +logger = logging.getLogger("ipa-server-install") - @contextlib_contextmanager - def redirect_stdout(stream): - sys.stdout = stream - try: - yield stream - finally: - sys.stdout = sys.__stdout__ - - class AnsibleModuleLog(): - def __init__(self, module): - self.module = module - _ansible_module_log = self - - class AnsibleLoggingHandler(logging.Handler): - def emit(self, record): - _ansible_module_log.write(self.format(record)) - - self.logging_handler = AnsibleLoggingHandler() - logger.setLevel(logging.DEBUG) - logger.root.addHandler(self.logging_handler) - - def close(self): - self.flush() - - def flush(self): - pass - - def log(self, msg): - # self.write(msg+"\n") - self.write(msg) - - def debug(self, msg): - self.module.debug(msg) - - def info(self, msg): - self.module.debug(msg) - - @staticmethod - def isatty(): - return False - - def write(self, msg): - self.module.debug(msg) - # self.module.warn(msg) - - # pylint: disable=too-many-instance-attributes, useless-object-inheritance - class installer_obj(object): # pylint: disable=invalid-name - def __init__(self): - # CompatServerReplicaInstall - self.ca_cert_files = None - self.all_ip_addresses = False - self.no_wait_for_dns = True - self.nisdomain = None - self.no_nisdomain = False - self.no_sudo = False - self.request_cert = False - self.ca_file = None - self.zonemgr = None - self.replica_file = None - # ServerReplicaInstall - self.subject_base = None - self.ca_subject = None - # others - self._ccache = None - self.password = None - self.reverse_zones = [] - # def _is_promote(self): - # return self.replica_file is None - # self.skip_conncheck = False - self._replica_install = False - # self.dnssec_master = False # future unknown - # self.disable_dnssec_master = False # future unknown - # self.domainlevel = MAX_DOMAIN_LEVEL # deprecated - # self.domain_level = self.domainlevel # deprecated - self.interactive = False - self.unattended = not self.interactive - # self.promote = self.replica_file is None - self.promote = True - self.skip_schema_check = None + +def setup_logging(): + # logger.setLevel(logging.DEBUG) + standard_logging_setup( + paths.IPAREPLICA_INSTALL_LOG, verbose=False, debug=False, + filemode='a', console_format='%(message)s') + + +@contextlib_contextmanager +def redirect_stdout(stream): + sys.stdout = stream + try: + yield stream + finally: + sys.stdout = sys.__stdout__ + + +class AnsibleModuleLog(): + def __init__(self, module): + self.module = module + _ansible_module_log = self + + class AnsibleLoggingHandler(logging.Handler): + def emit(self, record): + _ansible_module_log.write(self.format(record)) + + self.logging_handler = AnsibleLoggingHandler() + logger.setLevel(logging.DEBUG) + logger.root.addHandler(self.logging_handler) + + def close(self): + self.flush() + + def flush(self): + pass + + def log(self, msg): + # self.write(msg+"\n") + self.write(msg) + + def debug(self, msg): + self.module.debug(msg) + + def info(self, msg): + self.module.debug(msg) + + @staticmethod + def isatty(): + return False + + def write(self, msg): + self.module.debug(msg) + # self.module.warn(msg) + + +# pylint: disable=too-many-instance-attributes, useless-object-inheritance +class installer_obj(object): # pylint: disable=invalid-name + def __init__(self): + # CompatServerReplicaInstall + self.ca_cert_files = None + self.all_ip_addresses = False + self.no_wait_for_dns = True + self.nisdomain = None + self.no_nisdomain = False + self.no_sudo = False + self.request_cert = False + self.ca_file = None + self.zonemgr = None + self.replica_file = None + # ServerReplicaInstall + self.subject_base = None + self.ca_subject = None + # others + self._ccache = None + self.password = None + self.reverse_zones = [] + # def _is_promote(self): + # return self.replica_file is None + # self.skip_conncheck = False + self._replica_install = False + # self.dnssec_master = False # future unknown + # self.disable_dnssec_master = False # future unknown + # self.domainlevel = MAX_DOMAIN_LEVEL # deprecated + # self.domain_level = self.domainlevel # deprecated + self.interactive = False + self.unattended = not self.interactive + # self.promote = self.replica_file is None + self.promote = True + self.skip_schema_check = None + + # def __getattribute__(self, attr): + # value = super(installer_obj, self).__getattribute__(attr) + # if not attr.startswith("--") and not attr.endswith("--"): + # logger.debug( + # " <-- Accessing installer.%s (%s)" % + # (attr, repr(value))) + # return value + + def __getattr__(self, attrname): + logger.info(" --> ADDING missing installer.%s", attrname) + setattr(self, attrname, None) + return getattr(self, attrname) + + # def __setattr__(self, attr, value): + # logger.debug(" --> Setting installer.%s to %s" % + # (attr, repr(value))) + # return super(installer_obj, self).__setattr__(attr, value) + + def knobs(self): + for name in self.__dict__: + yield self, name + + +# pylint: enable=too-many-instance-attributes, useless-object-inheritance + + +# pylint: disable=attribute-defined-outside-init +installer = installer_obj() +options = installer + +# DNSInstallInterface +options.dnssec_master = False +options.disable_dnssec_master = False +options.kasp_db_file = None +options.force = False + +# ServerMasterInstall +options.add_sids = False +options.add_agents = False + +# ServerReplicaInstall +options.subject_base = None +options.ca_subject = None +# pylint: enable=attribute-defined-outside-init + + +def gen_env_boostrap_finalize_core(etc_ipa, default_config): + env = Env() + # env._bootstrap(context='installer', confdir=paths.ETC_IPA, log=None) + # env._finalize_core(**dict(constants.DEFAULT_CONFIG)) + env._bootstrap(context='installer', confdir=etc_ipa, log=None) + env._finalize_core(**dict(default_config)) + return env + + +def api_bootstrap_finalize(env): + # pylint: disable=no-member + xmlrpc_uri = \ + 'https://{}/ipa/xml'.format(ipautil.format_netloc(env.host)) + api.bootstrap(in_server=True, + context='installer', + confdir=paths.ETC_IPA, + ldap_uri=installutils.realm_to_ldapi_uri(env.realm), + xmlrpc_uri=xmlrpc_uri) + # pylint: enable=no-member + api.finalize() + + +def gen_ReplicaConfig(): # pylint: disable=invalid-name + # pylint: disable=too-many-instance-attributes + class ExtendedReplicaConfig(ReplicaConfig): + # pylint: disable=useless-super-delegation + def __init__(self, top_dir=None): + # pylint: disable=super-with-arguments + super(ExtendedReplicaConfig, self).__init__(top_dir) # def __getattribute__(self, attr): - # value = super(installer_obj, self).__getattribute__(attr) - # if not attr.startswith("--") and not attr.endswith("--"): - # logger.debug( - # " <-- Accessing installer.%s (%s)" % - # (attr, repr(value))) - # return value + # value = super(ExtendedReplicaConfig, self).__getattribute__( + # attr) + # if attr not in ["__dict__", "knobs"]: + # logger.debug(" <== Accessing config.%s (%s)" % + # (attr, repr(value))) + # return value\ + # pylint: enable=useless-super-delegation def __getattr__(self, attrname): - logger.info(" --> ADDING missing installer.%s", attrname) + logger.info(" ==> ADDING missing config.%s", attrname) setattr(self, attrname, None) return getattr(self, attrname) # def __setattr__(self, attr, value): - # logger.debug(" --> Setting installer.%s to %s" % - # (attr, repr(value))) - # return super(installer_obj, self).__setattr__(attr, value) + # logger.debug(" ==> Setting config.%s to %s" % + # (attr, repr(value))) + # return super(ExtendedReplicaConfig, self).__setattr__(attr, + # value) def knobs(self): for name in self.__dict__: yield self, name - - # pylint: enable=too-many-instance-attributes, useless-object-inheritance + # pylint: enable=too-many-instance-attributes # pylint: disable=attribute-defined-outside-init - installer = installer_obj() - options = installer - - # DNSInstallInterface - options.dnssec_master = False - options.disable_dnssec_master = False - options.kasp_db_file = None - options.force = False - - # ServerMasterInstall - options.add_sids = False - options.add_agents = False - - # ServerReplicaInstall - options.subject_base = None - options.ca_subject = None + # config = ReplicaConfig() + config = ExtendedReplicaConfig() + config.realm_name = api.env.realm + config.host_name = api.env.host + config.domain_name = api.env.domain + config.master_host_name = api.env.server + config.ca_host_name = api.env.ca_host + config.kra_host_name = config.ca_host_name + config.ca_ds_port = 389 + config.setup_ca = options.setup_ca + config.setup_kra = options.setup_kra + config.dir = options._top_dir + config.basedn = api.env.basedn + # config.subject_base = options.subject_base + # pylint: enable=attribute-defined-outside-init - def gen_env_boostrap_finalize_core(etc_ipa, default_config): - env = Env() - # env._bootstrap(context='installer', confdir=paths.ETC_IPA, log=None) - # env._finalize_core(**dict(constants.DEFAULT_CONFIG)) - env._bootstrap(context='installer', confdir=etc_ipa, log=None) - env._finalize_core(**dict(default_config)) - return env - - def api_bootstrap_finalize(env): - # pylint: disable=no-member - xmlrpc_uri = \ - 'https://{}/ipa/xml'.format(ipautil.format_netloc(env.host)) - api.bootstrap(in_server=True, - context='installer', - confdir=paths.ETC_IPA, - ldap_uri=installutils.realm_to_ldapi_uri(env.realm), - xmlrpc_uri=xmlrpc_uri) - # pylint: enable=no-member - api.finalize() - - def gen_ReplicaConfig(): # pylint: disable=invalid-name - # pylint: disable=too-many-instance-attributes - class ExtendedReplicaConfig(ReplicaConfig): - # pylint: disable=useless-super-delegation - def __init__(self, top_dir=None): - # pylint: disable=super-with-arguments - super(ExtendedReplicaConfig, self).__init__(top_dir) - - # def __getattribute__(self, attr): - # value = super(ExtendedReplicaConfig, self).__getattribute__( - # attr) - # if attr not in ["__dict__", "knobs"]: - # logger.debug(" <== Accessing config.%s (%s)" % - # (attr, repr(value))) - # return value\ - # pylint: enable=useless-super-delegation - - def __getattr__(self, attrname): - logger.info(" ==> ADDING missing config.%s", attrname) - setattr(self, attrname, None) - return getattr(self, attrname) - - # def __setattr__(self, attr, value): - # logger.debug(" ==> Setting config.%s to %s" % - # (attr, repr(value))) - # return super(ExtendedReplicaConfig, self).__setattr__(attr, - # value) - - def knobs(self): - for name in self.__dict__: - yield self, name - # pylint: enable=too-many-instance-attributes - - # pylint: disable=attribute-defined-outside-init - # config = ReplicaConfig() - config = ExtendedReplicaConfig() - config.realm_name = api.env.realm - config.host_name = api.env.host - config.domain_name = api.env.domain - config.master_host_name = api.env.server - config.ca_host_name = api.env.ca_host - config.kra_host_name = config.ca_host_name - config.ca_ds_port = 389 - config.setup_ca = options.setup_ca - config.setup_kra = options.setup_kra - config.dir = options._top_dir - config.basedn = api.env.basedn - # config.subject_base = options.subject_base - - # pylint: enable=attribute-defined-outside-init - - return config - - def replica_ds_init_info(ansible_log, - config, options_, ca_is_configured, remote_api, - ds_ca_subject, ca_file, - promote=False, pkcs12_info=None): - - dsinstance.check_ports() - - # if we have a pkcs12 file, create the cert db from - # that. Otherwise the ds setup will create the CA - # cert - if pkcs12_info is None: - pkcs12_info = make_pkcs12_info(config.dir, "dscert.p12", - "dirsrv_pin.txt") - - # during replica install, this gets invoked before local DS is - # available, so use the remote api. - # if ca_is_configured: - # ca_subject = ca.lookup_ca_subject(_api, config.subject_base) - # else: - # ca_subject = installutils.default_ca_subject_dn( - # config.subject_base) - ca_subject = ds_ca_subject - - ds = dsinstance.DsInstance( - config_ldif=options_.dirsrv_config_file) - ds.set_output(ansible_log) - - # Source: ipaserver/install/dsinstance.py - - # idstart and idmax are configured so that the range is seen as - # depleted by the DNA plugin and the replica will go and get a - # new range from the master. - # This way all servers use the initially defined range by default. - idstart = 1101 - idmax = 1100 - - with redirect_stdout(ansible_log): - ds.init_info( - realm_name=config.realm_name, - fqdn=config.host_name, - domain_name=config.domain_name, - dm_password=config.dirman_password, - subject_base=config.subject_base, - ca_subject=ca_subject, - idstart=idstart, - idmax=idmax, - pkcs12_info=pkcs12_info, - ca_file=ca_file, - setup_pkinit=not options.no_pkinit, - ) - ds.master_fqdn = config.master_host_name - if ca_is_configured is not None: - ds.ca_is_configured = ca_is_configured - ds.promote = promote - ds.api = remote_api - - # from __setup_replica - - # Always connect to ds over ldapi - ldap_uri = ipaldap.get_ldap_uri(protocol='ldapi', realm=ds.realm) - conn = ipaldap.LDAPClient(ldap_uri) - conn.external_bind() - - return ds - - def ansible_module_get_parsed_ip_addresses(ansible_module, - param='ip_addresses'): - ip_addrs = [] - for ip in ansible_module.params.get(param): - try: - ip_parsed = ipautil.CheckedIPAddress(ip) - except Exception as e: - ansible_module.fail_json( - msg="Invalid IP Address %s: %s" % (ip, e)) - ip_addrs.append(ip_parsed) - return ip_addrs - - def gen_remote_api(master_host_name, etc_ipa): - ldapuri = 'ldaps://%s' % ipautil.format_netloc(master_host_name) - xmlrpc_uri = 'https://{}/ipa/xml'.format( - ipautil.format_netloc(master_host_name)) - remote_api = create_api(mode=None) - remote_api.bootstrap(in_server=True, - context='installer', - confdir=etc_ipa, - ldap_uri=ldapuri, - xmlrpc_uri=xmlrpc_uri) - remote_api.finalize() - return remote_api + return config + + +def replica_ds_init_info(ansible_log, + config, options_, ca_is_configured, remote_api, + ds_ca_subject, ca_file, + promote=False, pkcs12_info=None): + + dsinstance.check_ports() + + # if we have a pkcs12 file, create the cert db from + # that. Otherwise the ds setup will create the CA + # cert + if pkcs12_info is None: + pkcs12_info = make_pkcs12_info(config.dir, "dscert.p12", + "dirsrv_pin.txt") + + # during replica install, this gets invoked before local DS is + # available, so use the remote api. + # if ca_is_configured: + # ca_subject = ca.lookup_ca_subject(_api, config.subject_base) + # else: + # ca_subject = installutils.default_ca_subject_dn( + # config.subject_base) + ca_subject = ds_ca_subject + + ds = dsinstance.DsInstance( + config_ldif=options_.dirsrv_config_file) + ds.set_output(ansible_log) + + # Source: ipaserver/install/dsinstance.py + + # idstart and idmax are configured so that the range is seen as + # depleted by the DNA plugin and the replica will go and get a + # new range from the master. + # This way all servers use the initially defined range by default. + idstart = 1101 + idmax = 1100 + + with redirect_stdout(ansible_log): + ds.init_info( + realm_name=config.realm_name, + fqdn=config.host_name, + domain_name=config.domain_name, + dm_password=config.dirman_password, + subject_base=config.subject_base, + ca_subject=ca_subject, + idstart=idstart, + idmax=idmax, + pkcs12_info=pkcs12_info, + ca_file=ca_file, + setup_pkinit=not options.no_pkinit, + ) + ds.master_fqdn = config.master_host_name + if ca_is_configured is not None: + ds.ca_is_configured = ca_is_configured + ds.promote = promote + ds.api = remote_api + + # from __setup_replica + + # Always connect to ds over ldapi + ldap_uri = ipaldap.get_ldap_uri(protocol='ldapi', realm=ds.realm) + conn = ipaldap.LDAPClient(ldap_uri) + conn.external_bind() + + return ds + + +def ansible_module_get_parsed_ip_addresses(ansible_module, + param='ip_addresses'): + ip_addrs = [] + for ip in ansible_module.params.get(param): + try: + ip_parsed = ipautil.CheckedIPAddress(ip) + except Exception as e: + ansible_module.fail_json( + msg="Invalid IP Address %s: %s" % (ip, e)) + ip_addrs.append(ip_parsed) + return ip_addrs + + +def gen_remote_api(master_host_name, etc_ipa): + ldapuri = 'ldaps://%s' % ipautil.format_netloc(master_host_name) + xmlrpc_uri = 'https://{}/ipa/xml'.format( + ipautil.format_netloc(master_host_name)) + remote_api = create_api(mode=None) + remote_api.bootstrap(in_server=True, + context='installer', + confdir=etc_ipa, + ldap_uri=ldapuri, + xmlrpc_uri=xmlrpc_uri) + remote_api.finalize() + return remote_api + + +def check_imports(module): + if ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR is not None: + module.fail_json(msg=ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR)