ansible_ipa_replica: Fix ansible-test fake execution test findings

All imports that are only available after installing IPA need to be in a
try exception clause to be able to pass the fake execution test. The old
workaround "if 'ansible.executor' in sys.modules:" is not working with
this test anymore.

If the imports can not be done, all used and needed attributes are
defines with the value None.

The new function check_imports has been added to fail with module.fail_json
if an import exception occured and ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR is
not None. This function needs to be called in all modules.

The `copyright` date is extended with `-2022`.
This commit is contained in:
Thomas Woerner
2022-11-08 12:09:24 +01:00
parent 0f0c098fa2
commit d4c34a28df

View File

@@ -5,7 +5,7 @@
# #
# Based on ipa-replica-install code # 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 # see file 'COPYING' for use and warranty information
# #
# This program is free software; you can redistribute it and/or modify # 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", "check_domain_level_is_supported", "promotion_check_ipa_domain",
"SSSDConfig", "CalledProcessError", "timeconf", "ntpinstance", "SSSDConfig", "CalledProcessError", "timeconf", "ntpinstance",
"dnsname", "kernel_keyring", "krbinstance", "getargspec", "dnsname", "kernel_keyring", "krbinstance", "getargspec",
"adtrustinstance"] "adtrustinstance", "paths", "api", "dsinstance", "ipaldap", "Env",
"ipautil", "installutils", "IPA_PYTHON_VERSION", "NUM_VERSION",
"ReplicaConfig", "create_api"]
import sys import sys
import logging
# HACK: workaround for Ansible 2.9 # Import getargspec from inspect or provide own getargspec for
# https://github.com/ansible/ansible/issues/68361 # Python 2 compatibility with Python 3.11+.
if 'ansible.executor' in sys.modules: try:
for attr in __all__: from inspect import getargspec
setattr(sys.modules[__name__], attr, None) except ImportError:
else: from collections import namedtuple
import logging 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 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 from ipapython.version import NUM_VERSION, VERSION
if NUM_VERSION < 30201: if NUM_VERSION < 30201:
@@ -177,296 +174,323 @@ else:
raise Exception("freeipa version '%s' is too old" % VERSION) 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(): for attr in __all__:
# logger.setLevel(logging.DEBUG) setattr(sys.modules[__name__], attr, None)
standard_logging_setup(
paths.IPAREPLICA_INSTALL_LOG, verbose=False, debug=False,
filemode='a', console_format='%(message)s')
@contextlib_contextmanager else:
def redirect_stdout(stream): ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR = None
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): logger = logging.getLogger("ipa-server-install")
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): def setup_logging():
self.flush() # logger.setLevel(logging.DEBUG)
standard_logging_setup(
paths.IPAREPLICA_INSTALL_LOG, verbose=False, debug=False,
filemode='a', console_format='%(message)s')
def flush(self):
pass
def log(self, msg): @contextlib_contextmanager
# self.write(msg+"\n") def redirect_stdout(stream):
self.write(msg) sys.stdout = stream
try:
yield stream
finally:
sys.stdout = sys.__stdout__
def debug(self, msg):
self.module.debug(msg)
def info(self, msg): class AnsibleModuleLog():
self.module.debug(msg) def __init__(self, module):
self.module = module
_ansible_module_log = self
@staticmethod class AnsibleLoggingHandler(logging.Handler):
def isatty(): def emit(self, record):
return False _ansible_module_log.write(self.format(record))
def write(self, msg): self.logging_handler = AnsibleLoggingHandler()
self.module.debug(msg) logger.setLevel(logging.DEBUG)
# self.module.warn(msg) logger.root.addHandler(self.logging_handler)
# pylint: disable=too-many-instance-attributes, useless-object-inheritance def close(self):
class installer_obj(object): # pylint: disable=invalid-name self.flush()
def __init__(self):
# CompatServerReplicaInstall def flush(self):
self.ca_cert_files = None pass
self.all_ip_addresses = False
self.no_wait_for_dns = True def log(self, msg):
self.nisdomain = None # self.write(msg+"\n")
self.no_nisdomain = False self.write(msg)
self.no_sudo = False
self.request_cert = False def debug(self, msg):
self.ca_file = None self.module.debug(msg)
self.zonemgr = None
self.replica_file = None def info(self, msg):
# ServerReplicaInstall self.module.debug(msg)
self.subject_base = None
self.ca_subject = None @staticmethod
# others def isatty():
self._ccache = None return False
self.password = None
self.reverse_zones = [] def write(self, msg):
# def _is_promote(self): self.module.debug(msg)
# return self.replica_file is None # self.module.warn(msg)
# self.skip_conncheck = False
self._replica_install = False
# self.dnssec_master = False # future unknown # pylint: disable=too-many-instance-attributes, useless-object-inheritance
# self.disable_dnssec_master = False # future unknown class installer_obj(object): # pylint: disable=invalid-name
# self.domainlevel = MAX_DOMAIN_LEVEL # deprecated def __init__(self):
# self.domain_level = self.domainlevel # deprecated # CompatServerReplicaInstall
self.interactive = False self.ca_cert_files = None
self.unattended = not self.interactive self.all_ip_addresses = False
# self.promote = self.replica_file is None self.no_wait_for_dns = True
self.promote = True self.nisdomain = None
self.skip_schema_check = 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): # def __getattribute__(self, attr):
# value = super(installer_obj, self).__getattribute__(attr) # value = super(ExtendedReplicaConfig, self).__getattribute__(
# if not attr.startswith("--") and not attr.endswith("--"): # attr)
# logger.debug( # if attr not in ["__dict__", "knobs"]:
# " <-- Accessing installer.%s (%s)" % # logger.debug(" <== Accessing config.%s (%s)" %
# (attr, repr(value))) # (attr, repr(value)))
# return value # return value\
# pylint: enable=useless-super-delegation
def __getattr__(self, attrname): def __getattr__(self, attrname):
logger.info(" --> ADDING missing installer.%s", attrname) logger.info(" ==> ADDING missing config.%s", attrname)
setattr(self, attrname, None) setattr(self, attrname, None)
return getattr(self, attrname) return getattr(self, attrname)
# def __setattr__(self, attr, value): # def __setattr__(self, attr, value):
# logger.debug(" --> Setting installer.%s to %s" % # logger.debug(" ==> Setting config.%s to %s" %
# (attr, repr(value))) # (attr, repr(value)))
# return super(installer_obj, self).__setattr__(attr, value) # return super(ExtendedReplicaConfig, self).__setattr__(attr,
# value)
def knobs(self): def knobs(self):
for name in self.__dict__: for name in self.__dict__:
yield self, name yield self, name
# pylint: enable=too-many-instance-attributes
# pylint: enable=too-many-instance-attributes, useless-object-inheritance
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
installer = installer_obj() # config = ReplicaConfig()
options = installer 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
# 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 # pylint: enable=attribute-defined-outside-init
def gen_env_boostrap_finalize_core(etc_ipa, default_config): return 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 def replica_ds_init_info(ansible_log,
# pylint: disable=too-many-instance-attributes config, options_, ca_is_configured, remote_api,
class ExtendedReplicaConfig(ReplicaConfig): ds_ca_subject, ca_file,
# pylint: disable=useless-super-delegation promote=False, pkcs12_info=None):
def __init__(self, top_dir=None):
# pylint: disable=super-with-arguments
super(ExtendedReplicaConfig, self).__init__(top_dir)
# def __getattribute__(self, attr): dsinstance.check_ports()
# 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): # if we have a pkcs12 file, create the cert db from
logger.info(" ==> ADDING missing config.%s", attrname) # that. Otherwise the ds setup will create the CA
setattr(self, attrname, None) # cert
return getattr(self, attrname) if pkcs12_info is None:
pkcs12_info = make_pkcs12_info(config.dir, "dscert.p12",
"dirsrv_pin.txt")
# def __setattr__(self, attr, value): # during replica install, this gets invoked before local DS is
# logger.debug(" ==> Setting config.%s to %s" % # available, so use the remote api.
# (attr, repr(value))) # if ca_is_configured:
# return super(ExtendedReplicaConfig, self).__setattr__(attr, # ca_subject = ca.lookup_ca_subject(_api, config.subject_base)
# value) # else:
# ca_subject = installutils.default_ca_subject_dn(
# config.subject_base)
ca_subject = ds_ca_subject
def knobs(self): ds = dsinstance.DsInstance(
for name in self.__dict__: config_ldif=options_.dirsrv_config_file)
yield self, name ds.set_output(ansible_log)
# pylint: enable=too-many-instance-attributes
# pylint: disable=attribute-defined-outside-init # Source: ipaserver/install/dsinstance.py
# 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 # 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
return config 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
def replica_ds_init_info(ansible_log, # from __setup_replica
config, options_, ca_is_configured, remote_api,
ds_ca_subject, ca_file,
promote=False, pkcs12_info=None):
dsinstance.check_ports() # 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()
# if we have a pkcs12 file, create the cert db from return ds
# 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( def ansible_module_get_parsed_ip_addresses(ansible_module,
config_ldif=options_.dirsrv_config_file) param='ip_addresses'):
ds.set_output(ansible_log) 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
# Source: ipaserver/install/dsinstance.py
# idstart and idmax are configured so that the range is seen as def gen_remote_api(master_host_name, etc_ipa):
# depleted by the DNA plugin and the replica will go and get a ldapuri = 'ldaps://%s' % ipautil.format_netloc(master_host_name)
# new range from the master. xmlrpc_uri = 'https://{}/ipa/xml'.format(
# This way all servers use the initially defined range by default. ipautil.format_netloc(master_host_name))
idstart = 1101 remote_api = create_api(mode=None)
idmax = 1100 remote_api.bootstrap(in_server=True,
context='installer',
confdir=etc_ipa,
ldap_uri=ldapuri,
xmlrpc_uri=xmlrpc_uri)
remote_api.finalize()
return remote_api
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 def check_imports(module):
if ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR is not None:
# Always connect to ds over ldapi module.fail_json(msg=ANSIBLE_IPA_REPLICA_MODULE_IMPORT_ERROR)
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