summaryrefslogtreecommitdiffstats
path: root/ipatests
diff options
context:
space:
mode:
authorPetr Viktorin <pviktori@redhat.com>2014-11-13 16:23:56 +0100
committerTomas Babej <tbabej@redhat.com>2014-12-11 07:04:58 +0100
commit74f7d67fd5484137b77e54ab50d7869daa6a7db5 (patch)
tree976d9c22b7f0587135acdbb4418fec0155160c04 /ipatests
parent8822be36d342c2bc499937c3f144e11ae98d8e58 (diff)
downloadfreeipa-74f7d67fd5484137b77e54ab50d7869daa6a7db5.tar.gz
freeipa-74f7d67fd5484137b77e54ab50d7869daa6a7db5.tar.xz
freeipa-74f7d67fd5484137b77e54ab50d7869daa6a7db5.zip
test_integration: Use python-pytest-multihost
The core integration testing functionality was split into a separate project. Use this project, and configure it for FreeIPA. The "mh" (multihost) fixture is made available for integration tests. Configuration based on environment variables is moved into a separate module, to ease eventual deprecation. Reviewed-By: Tomas Babej <tbabej@redhat.com>
Diffstat (limited to 'ipatests')
-rwxr-xr-xipatests/ipa-test-config5
-rwxr-xr-xipatests/ipa-test-task2
-rw-r--r--ipatests/pytest_plugins/integration.py119
-rw-r--r--ipatests/test_integration/base.py11
-rw-r--r--ipatests/test_integration/config.py422
-rw-r--r--ipatests/test_integration/env_config.py356
-rw-r--r--ipatests/test_integration/host.py238
-rw-r--r--ipatests/test_integration/tasks.py2
-rw-r--r--ipatests/test_integration/test_caless.py35
-rw-r--r--ipatests/test_integration/test_forced_client_reenrollment.py6
-rw-r--r--ipatests/test_integration/test_testconfig.py6
-rw-r--r--ipatests/test_integration/test_trust.py2
-rw-r--r--ipatests/test_integration/transport.py443
-rw-r--r--ipatests/test_integration/util.py10
14 files changed, 526 insertions, 1131 deletions
diff --git a/ipatests/ipa-test-config b/ipatests/ipa-test-config
index dc94b8afb..6a3101f63 100755
--- a/ipatests/ipa-test-config
+++ b/ipatests/ipa-test-config
@@ -25,7 +25,7 @@ import argparse
import json
from ipalib.constants import FQDN
-from ipatests.test_integration import config
+from ipatests.test_integration import config, env_config
def main(argv):
@@ -92,7 +92,8 @@ def main(argv):
import yaml
return yaml.safe_dump(conf.to_dict(), default_flow_style=False)
else:
- return config.env_to_script(get_object(conf, args).to_env(**kwargs))
+ env = get_object(conf, args).to_env(**kwargs)
+ return env_config.env_to_script(env)
def get_object(conf, args):
diff --git a/ipatests/ipa-test-task b/ipatests/ipa-test-task
index 612974549..d89af841d 100755
--- a/ipatests/ipa-test-task
+++ b/ipatests/ipa-test-task
@@ -248,6 +248,8 @@ class TaskRunner(object):
args = self.get_parser().parse_args(argv)
self.config = config.Config.from_env(os.environ)
+ if not self.config:
+ raise EnvironmentError('Multihost environment not configured')
logs_to_collect = {}
diff --git a/ipatests/pytest_plugins/integration.py b/ipatests/pytest_plugins/integration.py
index 5329e5190..a6c09518f 100644
--- a/ipatests/pytest_plugins/integration.py
+++ b/ipatests/pytest_plugins/integration.py
@@ -24,10 +24,13 @@ import tempfile
import shutil
import pytest
+from pytest_multihost import make_multihost_fixture
from ipapython import ipautil
from ipapython.ipa_log_manager import log_mgr
-from ipatests.test_integration.config import get_global_config
+from ipatests.test_integration import tasks
+from ipatests.test_integration.config import Config
+from ipatests.test_integration.env_config import get_global_config
log = log_mgr.get_logger(__name__)
@@ -147,74 +150,86 @@ def integration_logs(class_integration_logs, request):
@pytest.yield_fixture(scope='class')
-def integration_config(request, class_integration_logs):
- """Integration test Config object
+def mh(request, class_integration_logs):
+ """IPA's multihost fixture object
"""
cls = request.cls
- def get_resources(resource_container, resource_str, num_needed):
- if len(resource_container) < num_needed:
- raise pytest.skip(
- 'Not enough %s available (have %s, need %s)' %
- (resource_str, len(resource_container), num_needed))
- return resource_container[:num_needed]
-
- config = get_global_config()
- if not config.domains:
- raise pytest.skip('Integration testing not configured')
+ domain_description = {
+ 'type': 'IPA',
+ 'hosts': {
+ 'master': 1,
+ 'replica': cls.num_replicas,
+ 'client': cls.num_replicas,
+ },
+ }
+ domain_description['hosts'].update(
+ {role: 1 for role in cls.required_extra_roles})
+
+ domain_descriptions = [domain_description]
+ for i in range(cls.num_ad_domains):
+ domain_descriptions.append({
+ 'type': 'AD',
+ 'hosts': {'ad': 1, 'ad_subdomain': 1},
+ })
+
+ mh = make_multihost_fixture(
+ request,
+ domain_descriptions,
+ config_class=Config,
+ _config=get_global_config(),
+ )
+ config = mh.config
+ mh.domain = mh.config.domains[0]
+ [mh.master] = mh.domain.hosts_by_role('master')
+ mh.replicas = mh.domain.hosts_by_role('replica')
+ mh.clients = mh.domain.hosts_by_role('client')
cls.logs_to_collect = class_integration_logs
- cls.domain = config.domains[0]
-
- # Check that we have enough resources available
- cls.master = cls.domain.master
- cls.replicas = get_resources(cls.domain.replicas, 'replicas',
- cls.num_replicas)
- cls.clients = get_resources(cls.domain.clients, 'clients',
- cls.num_clients)
- cls.ad_domains = get_resources(config.ad_domains, 'AD domains',
- cls.num_ad_domains)
-
- # Check that we have all required extra hosts at our disposal
- available_extra_roles = [role for domain in cls.get_domains()
- for role in domain.extra_roles]
- missing_extra_roles = list(set(cls.required_extra_roles) -
- set(available_extra_roles))
-
- if missing_extra_roles:
- raise pytest.skip("Not all required extra hosts available, "
- "missing: %s, available: %s"
- % (missing_extra_roles,
- available_extra_roles))
-
def collect_log(host, filename):
log.info('Adding %s:%s to list of logs to collect' %
(host.external_hostname, filename))
class_integration_logs.setdefault(host, []).append(filename)
- for host in cls.get_all_hosts():
+ print config
+ for host in config.get_all_hosts():
host.add_log_collector(collect_log)
- cls.prepare_host(host)
+ cls.log.info('Preparing host %s', host.hostname)
+ tasks.prepare_host(host)
- try:
- cls.install()
- except:
- cls.uninstall()
- raise
+ setup_class(cls, config)
+ mh._pytestmh_request.addfinalizer(lambda: teardown_class(cls))
- yield config
+ yield mh.install()
for host in cls.get_all_hosts():
host.remove_log_collector(collect_log)
collect_test_logs(request.node, class_integration_logs, request.config)
- try:
- cls.uninstall()
- finally:
- del cls.master
- del cls.replicas
- del cls.clients
- del cls.ad_domains
- del cls.domain
+
+def setup_class(cls, config):
+ """Add convenience addributes to the test class
+
+ This is deprecated in favor of the mh fixture.
+ To be removed when no more tests using this.
+ """
+ cls.domain = config.domains[0]
+ cls.master = cls.domain.master
+ cls.replicas = cls.domain.replicas
+ cls.clients = cls.domain.clients
+ cls.ad_domains = config.ad_domains
+
+
+def teardown_class(cls):
+ """Add convenience addributes to the test class
+
+ This is deprecated in favor of the mh fixture.
+ To be removed when no more tests using this.
+ """
+ del cls.master
+ del cls.replicas
+ del cls.clients
+ del cls.ad_domains
+ del cls.domain
diff --git a/ipatests/test_integration/base.py b/ipatests/test_integration/base.py
index c8b98126e..d291c36c2 100644
--- a/ipatests/test_integration/base.py
+++ b/ipatests/test_integration/base.py
@@ -29,7 +29,7 @@ log = log_mgr.get_logger(__name__)
@ordered
-@pytest.mark.usefixtures('integration_config')
+@pytest.mark.usefixtures('mh')
@pytest.mark.usefixtures('integration_logs')
class IntegrationTest(object):
num_replicas = 0
@@ -61,12 +61,7 @@ class IntegrationTest(object):
return [cls.domain] + cls.ad_domains
@classmethod
- def prepare_host(cls, host):
- cls.log.info('Preparing host %s', host.hostname)
- tasks.prepare_host(host)
-
- @classmethod
- def install(cls):
+ def install(cls, mh):
if cls.topology is None:
return
else:
@@ -77,7 +72,7 @@ class IntegrationTest(object):
pass
@classmethod
- def uninstall(cls):
+ def uninstall(cls, mh):
tasks.uninstall_master(cls.master)
for replica in cls.replicas:
tasks.uninstall_master(replica)
diff --git a/ipatests/test_integration/config.py b/ipatests/test_integration/config.py
index 832d5d257..aaee72652 100644
--- a/ipatests/test_integration/config.py
+++ b/ipatests/test_integration/config.py
@@ -20,412 +20,110 @@
"""Utilities for configuration of multi-master tests"""
-import os
-import collections
import random
-import json
-from ipapython import ipautil
+import pytest_multihost.config
+
from ipapython.dn import DN
from ipapython.ipa_log_manager import log_mgr
-from ipatests.test_integration.util import check_config_dict_empty
-from ipatests.test_integration.util import TESTHOST_PREFIX
-
-
-_SettingInfo = collections.namedtuple('Setting', 'name var_name default')
-_setting_infos = (
- # Directory on which test-specific files will be stored,
- _SettingInfo('test_dir', 'IPATEST_DIR', '/root/ipatests'),
-
- # File with root's private RSA key for SSH (default: ~/.ssh/id_rsa)
- _SettingInfo('root_ssh_key_filename', 'IPA_ROOT_SSH_KEY', None),
-
- # SSH password for root (used if root_ssh_key_filename is not set)
- _SettingInfo('root_password', 'IPA_ROOT_SSH_PASSWORD', None),
-
- _SettingInfo('admin_name', 'ADMINID', 'admin'),
- _SettingInfo('admin_password', 'ADMINPW', 'Secret123'),
- _SettingInfo('dirman_dn', 'ROOTDN', 'cn=Directory Manager'),
- _SettingInfo('dirman_password', 'ROOTDNPWD', None),
-
- # 8.8.8.8 is probably the best-known public DNS
- _SettingInfo('dns_forwarder', 'DNSFORWARD', '8.8.8.8'),
- _SettingInfo('nis_domain', 'NISDOMAIN', 'ipatest'),
- _SettingInfo('ntp_server', 'NTPSERVER', None),
- _SettingInfo('ad_admin_name', 'ADADMINID', 'Administrator'),
- _SettingInfo('ad_admin_password', 'ADADMINPW', 'Secret123'),
- _SettingInfo('ipv6', 'IPv6SETUP', False),
- _SettingInfo('debug', 'IPADEBUG', False),
-)
+class Config(pytest_multihost.config.Config):
+ extra_init_args = {
+ 'admin_name',
+ 'admin_password',
+ 'dirman_dn',
+ 'dirman_password',
+ 'nis_domain',
+ 'ntp_server',
+ 'ad_admin_name',
+ 'ad_admin_password',
+ 'dns_forwarder',
+ }
-class Config(object):
def __init__(self, **kwargs):
- self.log = log_mgr.get_logger(self)
+ kwargs.setdefault('test_dir', '/root/ipatests')
+ super(Config, self).__init__(**kwargs)
admin_password = kwargs.get('admin_password') or 'Secret123'
- # This unfortunately duplicates information in _setting_infos,
- # but is left here for the sake of static analysis.
- self.test_dir = kwargs.get('test_dir', '/root/ipatests')
- self.root_ssh_key_filename = kwargs.get('root_ssh_key_filename')
- self.root_password = kwargs.get('root_password')
self.admin_name = kwargs.get('admin_name') or 'admin'
self.admin_password = admin_password
self.dirman_dn = DN(kwargs.get('dirman_dn') or 'cn=Directory Manager')
self.dirman_password = kwargs.get('dirman_password') or admin_password
- self.dns_forwarder = kwargs.get('dns_forwarder') or '8.8.8.8'
self.nis_domain = kwargs.get('nis_domain') or 'ipatest'
self.ntp_server = str(kwargs.get('ntp_server') or (
'%s.pool.ntp.org' % random.randint(0, 3)))
self.ad_admin_name = kwargs.get('ad_admin_name') or 'Administrator'
self.ad_admin_password = kwargs.get('ad_admin_password') or 'Secret123'
- self.ipv6 = bool(kwargs.get('ipv6', False))
- self.debug = bool(kwargs.get('debug', False))
- if not self.root_password and not self.root_ssh_key_filename:
- self.root_ssh_key_filename = '~/.ssh/id_rsa'
+ # 8.8.8.8 is probably the best-known public DNS
+ self.dns_forwarder = kwargs.get('dns_forwarder') or '8.8.8.8'
+ self.debug = False
+
+ def get_domain_class(self):
+ return Domain
- self.domains = []
+ def get_logger(self, name):
+ return log_mgr.get_logger(name)
@property
def ad_domains(self):
return filter(lambda d: d.type == 'AD', self.domains)
- @classmethod
- def from_dict(cls, dct):
- kwargs = {s.name: dct.pop(s.name, s.default) for s in _setting_infos}
- self = cls(**kwargs)
-
- for domain_dict in dct.pop('domains'):
- self.domains.append(Domain.from_dict(domain_dict, self))
-
- check_config_dict_empty(dct, 'config')
-
- return self
+ def get_all_hosts(self):
+ for domain in self.domains:
+ for host in domain.hosts:
+ yield host
def to_dict(self):
- dct = {'domains': [d.to_dict() for d in self.domains]}
- for setting in _setting_infos:
- value = getattr(self, setting.name)
- if isinstance(value, DN):
- value = str(value)
- dct[setting.name] = value
- return dct
+ extra_args = self.extra_init_args - {'dirman_dn'}
+ result = super(Config, self).to_dict(extra_args)
+ result['dirman_dn'] = str(self.dirman_dn)
+ return result
@classmethod
def from_env(cls, env):
- """Create a test config from environment variables
-
- If IPATEST_YAML_CONFIG or IPATEST_JSON_CONFIG is set,
- configuration is read from the named file.
- For YAML, the PyYAML (python-yaml) library needs to be installed.
-
- Otherwise, configuration is read from various curiously
- named environment variables:
-
- See _setting_infos for test-wide settings
-
- MASTER_env1: FQDN of the master
- REPLICA_env1: space-separated FQDNs of the replicas
- CLIENT_env1: space-separated FQDNs of the clients
- AD_env1: space-separated FQDNs of the Active Directories
- OTHER_env1: space-separated FQDNs of other hosts
- (same for _env2, _env3, etc)
- BEAKERREPLICA1_IP_env1: IP address of replica 1 in env 1
- (same for MASTER, CLIENT, or any extra defined ROLE)
-
- For each machine that should be accessible to tests via extra roles,
- the following environment variable is necessary:
-
- TESTHOST_<role>_env1: FQDN of the machine with the extra role <role>
-
- You can also optionally specify the IP address of the host:
- BEAKER<role>_IP_env1: IP address of the machine of the extra role
-
- The framework will try to resolve the hostname to its IP address
- if not passed via this environment variable.
-
- Also see env_normalize() for alternate variable names
- """
- if 'IPATEST_YAML_CONFIG' in env:
- import yaml
- with open(env['IPATEST_YAML_CONFIG']) as file:
- data = yaml.safe_load(file)
- return cls.from_dict(data)
-
- if 'IPATEST_JSON_CONFIG' in env:
- with open(env['IPATEST_JSON_CONFIG']) as file:
- data = json.load(file)
- return cls.from_dict(data)
-
- env_normalize(env)
-
- kwargs = {s.name: env.get(s.var_name, s.default)
- for s in _setting_infos}
-
- # $IPv6SETUP needs to be 'TRUE' to enable ipv6
- if isinstance(kwargs['ipv6'], basestring):
- kwargs['ipv6'] = (kwargs['ipv6'].upper() == 'TRUE')
-
- self = cls(**kwargs)
-
- # Either IPA master or AD can define a domain
-
- domain_index = 1
- while (env.get('MASTER_env%s' % domain_index) or
- env.get('AD_env%s' % domain_index)):
-
- if env.get('MASTER_env%s' % domain_index):
- # IPA domain takes precedence to AD domain in case of conflict
- self.domains.append(Domain.from_env(env, self, domain_index,
- domain_type='IPA'))
- else:
- self.domains.append(Domain.from_env(env, self, domain_index,
- domain_type='AD'))
- domain_index += 1
-
- return self
-
- def to_env(self, simple=True):
- """Convert this test config into environment variables"""
- try:
- env = collections.OrderedDict()
- except AttributeError:
- # Older Python versions
- env = {}
-
- for setting in _setting_infos:
- value = getattr(self, setting.name)
- if value in (None, False):
- env[setting.var_name] = ''
- elif value is True:
- env[setting.var_name] = 'TRUE'
- else:
- env[setting.var_name] = str(value)
+ from ipatests.test_integration.env_config import config_from_env
+ return config_from_env(env)
- for domain in self.domains:
- env_suffix = '_env%s' % (self.domains.index(domain) + 1)
- env['DOMAIN%s' % env_suffix] = domain.name
- env['RELM%s' % env_suffix] = domain.realm
- env['BASEDN%s' % env_suffix] = str(domain.basedn)
-
- for role in domain.roles:
- hosts = domain.hosts_by_role(role)
-
- prefix = ('' if role in domain.static_roles
- else TESTHOST_PREFIX)
-
- hostnames = ' '.join(h.hostname for h in hosts)
- env['%s%s%s' % (prefix, role.upper(), env_suffix)] = hostnames
-
- ext_hostnames = ' '.join(h.external_hostname for h in hosts)
- env['BEAKER%s%s' % (role.upper(), env_suffix)] = ext_hostnames
-
- ips = ' '.join(h.ip for h in hosts)
- env['BEAKER%s_IP%s' % (role.upper(), env_suffix)] = ips
-
- for i, host in enumerate(hosts, start=1):
- suffix = '%s%s' % (role.upper(), i)
- prefix = ('' if role in domain.static_roles
- else TESTHOST_PREFIX)
-
- ext_hostname = host.external_hostname
- env['%s%s%s' % (prefix, suffix,
- env_suffix)] = host.hostname
- env['BEAKER%s%s' % (suffix, env_suffix)] = ext_hostname
- env['BEAKER%s_IP%s' % (suffix, env_suffix)] = host.ip
-
- if simple:
- # Simple Vars for simplicity and backwards compatibility with older
- # tests. This means no _env<NUM> suffix.
- if self.domains:
- default_domain = self.domains[0]
- if default_domain.master:
- env['MASTER'] = default_domain.master.hostname
- env['BEAKERMASTER'] = default_domain.master.external_hostname
- env['MASTERIP'] = default_domain.master.ip
- if default_domain.replicas:
- env['SLAVE'] = env['REPLICA'] = env['REPLICA_env1']
- env['BEAKERSLAVE'] = env['BEAKERREPLICA_env1']
- env['SLAVEIP'] = env['BEAKERREPLICA_IP_env1']
- if default_domain.clients:
- client = default_domain.clients[0]
- env['CLIENT'] = client.hostname
- env['BEAKERCLIENT'] = client.external_hostname
- if len(default_domain.clients) >= 2:
- client = default_domain.clients[1]
- env['CLIENT2'] = client.hostname
- env['BEAKERCLIENT2'] = client.external_hostname
-
- return env
-
- def host_by_name(self, name):
- for domain in self.domains:
- try:
- return domain.host_by_name(name)
- except LookupError:
- pass
- raise LookupError(name)
-
-
-def env_normalize(env):
- """Fill env variables from alternate variable names
-
- MASTER_env1 <- MASTER
- REPLICA_env1 <- REPLICA, SLAVE
- CLIENT_env1 <- CLIENT
- similarly for BEAKER* variants: BEAKERMASTER1_env1 <- BEAKERMASTER, etc.
-
- CLIENT_env1 gets extended with CLIENT2 or CLIENT2_env1
- """
- def coalesce(name, *other_names):
- """If name is not set, set it to first existing env[other_name]"""
- if name not in env:
- for other_name in other_names:
- try:
- env[name] = env[other_name]
- except KeyError:
- pass
- else:
- return
- else:
- env[name] = ''
- coalesce('MASTER_env1', 'MASTER')
- coalesce('REPLICA_env1', 'REPLICA', 'SLAVE')
- coalesce('CLIENT_env1', 'CLIENT')
-
- coalesce('BEAKERMASTER1_env1', 'BEAKERMASTER')
- coalesce('BEAKERREPLICA1_env1', 'BEAKERREPLICA', 'BEAKERSLAVE')
- coalesce('BEAKERCLIENT1_env1', 'BEAKERCLIENT')
-
- def extend(name, name2):
- value = env.get(name2)
- if value and value not in env[name].split(' '):
- env[name] += ' ' + value
- extend('CLIENT_env1', 'CLIENT2')
- extend('CLIENT_env1', 'CLIENT2_env1')
-
-
-class Domain(object):
+ def to_env(self, **kwargs):
+ from ipatests.test_integration.env_config import config_to_env
+ return config_to_env(self, **kwargs)
+
+
+class Domain(pytest_multihost.config.Domain):
"""Configuration for an IPA / AD domain"""
def __init__(self, config, name, domain_type):
- self.log = log_mgr.get_logger(self)
self.type = str(domain_type)
self.config = config
self.name = str(name)
self.hosts = []
+ assert domain_type in ('IPA', 'AD')
self.realm = self.name.upper()
self.basedn = DN(*(('dc', p) for p in name.split('.')))
@property
- def roles(self):
- return sorted(set(host.role for host in self.hosts))
-
- @property
def static_roles(self):
# Specific roles for each domain type are hardcoded
if self.type == 'IPA':
return ('master', 'replica', 'client', 'other')
- else:
+ elif self.type == 'AD':
return ('ad',)
-
- @property
- def extra_roles(self):
- return [role for role in self.roles if role not in self.static_roles]
-
- def _roles_from_env(self, env, env_suffix):
- for role in self.static_roles:
- yield role
-
- # Extra roles are defined via env variables of form TESTHOST_key_envX
- roles = set()
- for var in sorted(env):
- if var.startswith(TESTHOST_PREFIX) and var.endswith(env_suffix):
- variable_split = var.split('_')
- role_name = '_'.join(variable_split[1:-1])
- if (role_name and not role_name[-1].isdigit()):
- roles.add(role_name.lower())
- for role in sorted(roles):
- yield role
-
- @classmethod
- def from_dict(cls, dct, config):
- from ipatests.test_integration.host import BaseHost
-
- domain_type = dct.pop('type')
- assert domain_type in ('IPA', 'AD')
- domain_name = dct.pop('name')
- self = cls(config, domain_name, domain_type)
-
- for host_dict in dct.pop('hosts'):
- host = BaseHost.from_dict(host_dict, self)
- self.hosts.append(host)
-
- check_config_dict_empty(dct, 'domain %s' % domain_name)
-
- return self
-
- def to_dict(self):
- return {
- 'type': self.type,
- 'name': self.name,
- 'hosts': [h.to_dict() for h in self.hosts],
- }
-
- @classmethod
- def from_env(cls, env, config, index, domain_type):
- from ipatests.test_integration.host import BaseHost
-
- # Roles available in the domain depend on the type of the domain
- # Unix machines are added only to the IPA domains, Windows machines
- # only to the AD domains
- if domain_type == 'IPA':
- master_role = 'MASTER'
else:
- master_role = 'AD'
-
- env_suffix = '_env%s' % index
-
- master_env = '%s%s' % (master_role, env_suffix)
- hostname, dot, domain_name = env[master_env].partition('.')
- self = cls(config, domain_name, domain_type)
-
- for role in self._roles_from_env(env, env_suffix):
- prefix = '' if role in self.static_roles else TESTHOST_PREFIX
- value = env.get('%s%s%s' % (prefix, role.upper(), env_suffix), '')
+ raise LookupError(self.type)
- for host_index, hostname in enumerate(value.split(), start=1):
- host = BaseHost.from_env(env, self, hostname, role,
- host_index, index)
- self.hosts.append(host)
+ def get_host_class(self, host_dict):
+ from ipatests.test_integration.host import Host, WinHost
- if not self.hosts:
- raise ValueError('No hosts defined for %s' % env_suffix)
-
- return self
-
- def to_env(self, **kwargs):
- """Return environment variables specific to this domain"""
- env = self.config.to_env(**kwargs)
-
- env['DOMAIN'] = self.name
- env['RELM'] = self.realm
- env['BASEDN'] = str(self.basedn)
-
- return env
-
- def host_by_role(self, role):
- if self.hosts_by_role(role):
- return self.hosts_by_role(role)[0]
+ if self.type == 'IPA':
+ return Host
+ elif self.type == 'AD':
+ return WinHost
else:
- raise LookupError(role)
-
- def hosts_by_role(self, role):
- return [h for h in self.hosts if h.role == role]
+ raise LookupError(self.type)
@property
def master(self):
@@ -451,17 +149,11 @@ class Domain(object):
def other_hosts(self):
return self.hosts_by_role('other')
- def host_by_name(self, name):
- for host in self.hosts:
- if name in (host.hostname, host.external_hostname, host.shortname):
- return host
- raise LookupError(name)
-
-
-def env_to_script(env):
- return ''.join(['export %s=%s\n' % (key, ipautil.shell_quote(value))
- for key, value in env.items()])
-
+ @classmethod
+ def from_env(cls, env, config, index, domain_type):
+ from ipatests.test_integration.env_config import domain_from_env
+ return domain_from_env(env, config, index, domain_type)
-def get_global_config():
- return Config.from_env(os.environ)
+ def to_env(self, **kwargs):
+ from ipatests.test_integration.env_config import domain_to_env
+ return domain_to_env(self, **kwargs)
diff --git a/ipatests/test_integration/env_config.py b/ipatests/test_integration/env_config.py
new file mode 100644
index 000000000..c06334d9c
--- /dev/null
+++ b/ipatests/test_integration/env_config.py
@@ -0,0 +1,356 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+# Tomas Babej <tbabej@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Support for configuring multihost testing via environment variables
+
+This is here to support tests configured for Beaker,
+such as the ones at https://github.com/freeipa/tests/
+"""
+
+import os
+import json
+import collections
+
+from ipapython import ipautil
+from ipatests.test_integration.config import Config, Domain
+
+TESTHOST_PREFIX = 'TESTHOST_'
+
+
+_SettingInfo = collections.namedtuple('Setting', 'name var_name default')
+_setting_infos = (
+ # Directory on which test-specific files will be stored,
+ _SettingInfo('test_dir', 'IPATEST_DIR', '/root/ipatests'),
+
+ # File with root's private RSA key for SSH (default: ~/.ssh/id_rsa)
+ _SettingInfo('ssh_key_filename', 'IPA_ROOT_SSH_KEY', None),
+
+ # SSH password for root (used if root_ssh_key_filename is not set)
+ _SettingInfo('ssh_password', 'IPA_ROOT_SSH_PASSWORD', None),
+
+ _SettingInfo('admin_name', 'ADMINID', 'admin'),
+ _SettingInfo('admin_password', 'ADMINPW', 'Secret123'),
+ _SettingInfo('dirman_dn', 'ROOTDN', 'cn=Directory Manager'),
+ _SettingInfo('dirman_password', 'ROOTDNPWD', None),
+
+ # 8.8.8.8 is probably the best-known public DNS
+ _SettingInfo('dns_forwarder', 'DNSFORWARD', '8.8.8.8'),
+ _SettingInfo('nis_domain', 'NISDOMAIN', 'ipatest'),
+ _SettingInfo('ntp_server', 'NTPSERVER', None),
+ _SettingInfo('ad_admin_name', 'ADADMINID', 'Administrator'),
+ _SettingInfo('ad_admin_password', 'ADADMINPW', 'Secret123'),
+
+ _SettingInfo('ipv6', 'IPv6SETUP', False),
+ _SettingInfo('debug', 'IPADEBUG', False),
+)
+
+
+def get_global_config(env=None):
+ """Create a test config from environment variables
+
+ If env is None, uses os.environ; otherwise env is an environment dict.
+
+ If IPATEST_YAML_CONFIG or IPATEST_JSON_CONFIG is set,
+ configuration is read from the named file.
+ For YAML, the PyYAML (python-yaml) library needs to be installed.
+
+ Otherwise, configuration is read from various curiously
+ named environment variables:
+
+ See _setting_infos for test-wide settings
+
+ MASTER_env1: FQDN of the master
+ REPLICA_env1: space-separated FQDNs of the replicas
+ CLIENT_env1: space-separated FQDNs of the clients
+ AD_env1: space-separated FQDNs of the Active Directories
+ OTHER_env1: space-separated FQDNs of other hosts
+ (same for _env2, _env3, etc)
+ BEAKERREPLICA1_IP_env1: IP address of replica 1 in env 1
+ (same for MASTER, CLIENT, or any extra defined ROLE)
+
+ For each machine that should be accessible to tests via extra roles,
+ the following environment variable is necessary:
+
+ TESTHOST_<role>_env1: FQDN of the machine with the extra role <role>
+
+ You can also optionally specify the IP address of the host:
+ BEAKER<role>_IP_env1: IP address of the machine of the extra role
+
+ The framework will try to resolve the hostname to its IP address
+ if not passed via this environment variable.
+
+ Also see env_normalize() for alternate variable names
+ """
+ if env is None:
+ env = os.environ
+ env = dict(env)
+
+ return config_from_env(env)
+
+
+def config_from_env(env):
+ if 'IPATEST_YAML_CONFIG' in env:
+ import yaml
+ with open(env['IPATEST_YAML_CONFIG']) as file:
+ confdict = yaml.safe_load(file)
+ return Config.from_dict(confdict)
+
+ if 'IPATEST_JSON_CONFIG' in env:
+ with open(env['IPATEST_JSON_CONFIG']) as file:
+ confdict = json.load(file)
+ return Config.from_dict(confdict)
+
+ env_normalize(env)
+
+ kwargs = {s.name: env.get(s.var_name, s.default)
+ for s in _setting_infos}
+ kwargs['domains'] = []
+
+ # $IPv6SETUP needs to be 'TRUE' to enable ipv6
+ if isinstance(kwargs['ipv6'], basestring):
+ kwargs['ipv6'] = (kwargs['ipv6'].upper() == 'TRUE')
+
+ config = Config(**kwargs)
+
+ # Either IPA master or AD can define a domain
+
+ domain_index = 1
+ while (env.get('MASTER_env%s' % domain_index) or
+ env.get('AD_env%s' % domain_index)):
+
+ if env.get('MASTER_env%s' % domain_index):
+ # IPA domain takes precedence to AD domain in case of conflict
+ config.domains.append(domain_from_env(env, config, domain_index,
+ domain_type='IPA'))
+ else:
+ config.domains.append(domain_from_env(env, config, domain_index,
+ domain_type='AD'))
+ domain_index += 1
+
+ return config
+
+
+def config_to_env(config, simple=True):
+ """Convert this test config into environment variables"""
+ try:
+ env = collections.OrderedDict()
+ except AttributeError:
+ # Older Python versions
+ env = {}
+
+ for setting in _setting_infos:
+ value = getattr(config, setting.name)
+ if value in (None, False):
+ env[setting.var_name] = ''
+ elif value is True:
+ env[setting.var_name] = 'TRUE'
+ else:
+ env[setting.var_name] = str(value)
+
+ for domain in config.domains:
+ env_suffix = '_env%s' % (config.domains.index(domain) + 1)
+ env['DOMAIN%s' % env_suffix] = domain.name
+ env['RELM%s' % env_suffix] = domain.realm
+ env['BASEDN%s' % env_suffix] = str(domain.basedn)
+
+ for role in domain.roles:
+ hosts = domain.hosts_by_role(role)
+
+ prefix = ('' if role in domain.static_roles
+ else TESTHOST_PREFIX)
+
+ hostnames = ' '.join(h.hostname for h in hosts)
+ env['%s%s%s' % (prefix, role.upper(), env_suffix)] = hostnames
+
+ ext_hostnames = ' '.join(h.external_hostname for h in hosts)
+ env['BEAKER%s%s' % (role.upper(), env_suffix)] = ext_hostnames
+
+ ips = ' '.join(h.ip for h in hosts)
+ env['BEAKER%s_IP%s' % (role.upper(), env_suffix)] = ips
+
+ for i, host in enumerate(hosts, start=1):
+ suffix = '%s%s' % (role.upper(), i)
+ prefix = ('' if role in domain.static_roles
+ else TESTHOST_PREFIX)
+
+ ext_hostname = host.external_hostname
+ env['%s%s%s' % (prefix, suffix,
+ env_suffix)] = host.hostname
+ env['BEAKER%s%s' % (suffix, env_suffix)] = ext_hostname
+ env['BEAKER%s_IP%s' % (suffix, env_suffix)] = host.ip
+
+ if simple:
+ # Simple Vars for simplicity and backwards compatibility with older
+ # tests. This means no _env<NUM> suffix.
+ if config.domains:
+ default_domain = config.domains[0]
+ if default_domain.master:
+ env['MASTER'] = default_domain.master.hostname
+ env['BEAKERMASTER'] = default_domain.master.external_hostname
+ env['MASTERIP'] = default_domain.master.ip
+ if default_domain.replicas:
+ env['SLAVE'] = env['REPLICA'] = env['REPLICA_env1']
+ env['BEAKERSLAVE'] = env['BEAKERREPLICA_env1']
+ env['SLAVEIP'] = env['BEAKERREPLICA_IP_env1']
+ if default_domain.clients:
+ client = default_domain.clients[0]
+ env['CLIENT'] = client.hostname
+ env['BEAKERCLIENT'] = client.external_hostname
+ if len(default_domain.clients) >= 2:
+ client = default_domain.clients[1]
+ env['CLIENT2'] = client.hostname
+ env['BEAKERCLIENT2'] = client.external_hostname
+
+ return env
+
+
+def env_normalize(env):
+ """Fill env variables from alternate variable names
+
+ MASTER_env1 <- MASTER
+ REPLICA_env1 <- REPLICA, SLAVE
+ CLIENT_env1 <- CLIENT
+ similarly for BEAKER* variants: BEAKERMASTER1_env1 <- BEAKERMASTER, etc.
+
+ CLIENT_env1 gets extended with CLIENT2 or CLIENT2_env1
+ """
+ def coalesce(name, *other_names):
+ """If name is not set, set it to first existing env[other_name]"""
+ if name not in env:
+ for other_name in other_names:
+ try:
+ env[name] = env[other_name]
+ except KeyError:
+ pass
+ else:
+ return
+ else:
+ env[name] = ''
+ coalesce('MASTER_env1', 'MASTER')
+ coalesce('REPLICA_env1', 'REPLICA', 'SLAVE')
+ coalesce('CLIENT_env1', 'CLIENT')
+
+ coalesce('BEAKERMASTER1_env1', 'BEAKERMASTER')
+ coalesce('BEAKERREPLICA1_env1', 'BEAKERREPLICA', 'BEAKERSLAVE')
+ coalesce('BEAKERCLIENT1_env1', 'BEAKERCLIENT')
+
+ def extend(name, name2):
+ value = env.get(name2)
+ if value and value not in env[name].split(' '):
+ env[name] += ' ' + value
+ extend('CLIENT_env1', 'CLIENT2')
+ extend('CLIENT_env1', 'CLIENT2_env1')
+
+
+def domain_from_env(env, config, index, domain_type):
+ # Roles available in the domain depend on the type of the domain
+ # Unix machines are added only to the IPA domains, Windows machines
+ # only to the AD domains
+ if domain_type == 'IPA':
+ master_role = 'MASTER'
+ else:
+ master_role = 'AD'
+
+ env_suffix = '_env%s' % index
+
+ master_env = '%s%s' % (master_role, env_suffix)
+ hostname, dot, domain_name = env[master_env].partition('.')
+ domain = Domain(config, domain_name, domain_type)
+
+ for role in _roles_from_env(domain, env, env_suffix):
+ prefix = '' if role in domain.static_roles else TESTHOST_PREFIX
+ value = env.get('%s%s%s' % (prefix, role.upper(), env_suffix), '')
+
+ for host_index, hostname in enumerate(value.split(), start=1):
+ host = host_from_env(env, domain, hostname, role,
+ host_index, index)
+ domain.hosts.append(host)
+
+ if not domain.hosts:
+ raise ValueError('No hosts defined for %s' % env_suffix)
+
+ return domain
+
+
+def _roles_from_env(domain, env, env_suffix):
+ for role in domain.static_roles:
+ yield role
+
+ # Extra roles are defined via env variables of form TESTHOST_key_envX
+ roles = set()
+ for var in sorted(env):
+ if var.startswith(TESTHOST_PREFIX) and var.endswith(env_suffix):
+ variable_split = var.split('_')
+ role_name = '_'.join(variable_split[1:-1])
+ if (role_name and not role_name[-1].isdigit()):
+ roles.add(role_name.lower())
+ for role in sorted(roles):
+ yield role
+
+
+def domain_to_env(domain, **kwargs):
+ """Return environment variables specific to this domain"""
+ env = domain.config.to_env(**kwargs)
+
+ env['DOMAIN'] = domain.name
+ env['RELM'] = domain.realm
+ env['BASEDN'] = str(domain.basedn)
+
+ return env
+
+
+def host_from_env(env, domain, hostname, role, index, domain_index):
+ ip = env.get('BEAKER%s%s_IP_env%s' %
+ (role.upper(), index, domain_index), None)
+ external_hostname = env.get(
+ 'BEAKER%s%s_env%s' % (role.upper(), index, domain_index), None)
+
+ cls = domain.get_host_class({})
+
+ return cls(domain, hostname, role, ip, external_hostname)
+
+
+def host_to_env(host, **kwargs):
+ """Return environment variables specific to this host"""
+ env = host.domain.to_env(**kwargs)
+
+ index = host.domain.hosts.index(host) + 1
+ domain_index = host.config.domains.index(host.domain) + 1
+
+ role = host.role.upper()
+ if host.role != 'master':
+ role += str(index)
+
+ env['MYHOSTNAME'] = host.hostname
+ env['MYBEAKERHOSTNAME'] = host.external_hostname
+ env['MYIP'] = host.ip
+
+ prefix = ('' if host.role in host.domain.static_roles
+ else TESTHOST_PREFIX)
+ env_suffix = '_env%s' % domain_index
+ env['MYROLE'] = '%s%s%s' % (prefix, role, env_suffix)
+ env['MYENV'] = str(domain_index)
+
+ return env
+
+
+def env_to_script(env):
+ return ''.join(['export %s=%s\n' % (key, ipautil.shell_quote(value))
+ for key, value in env.items()])
diff --git a/ipatests/test_integration/host.py b/ipatests/test_integration/host.py
index 7a3a6ac77..399884fdb 100644
--- a/ipatests/test_integration/host.py
+++ b/ipatests/test_integration/host.py
@@ -19,93 +19,13 @@
"""Host class for integration testing"""
-import os
-import socket
+import pytest_multihost.host
from ipapython.ipaldap import IPAdmin
-from ipapython import ipautil
-from ipapython.ipa_log_manager import log_mgr
-from ipatests.test_integration import transport
-from ipatests.test_integration.util import check_config_dict_empty
-from ipatests.test_integration.util import TESTHOST_PREFIX
-class BaseHost(object):
+class Host(pytest_multihost.host.Host):
"""Representation of a remote IPA host"""
- transport_class = None
-
- def __init__(self, domain, hostname, role, ip=None,
- external_hostname=None):
- self.domain = domain
- self.role = str(role)
-
- shortname, dot, ext_domain = hostname.partition('.')
- self.shortname = shortname
-
- self.hostname = (hostname[:-1]
- if hostname.endswith('.')
- else shortname + '.' + self.domain.name)
-
- self.external_hostname = str(external_hostname or hostname)
-
- self.netbios = self.domain.name.split('.')[0].upper()
-
- self.logger_name = '%s.%s.%s' % (
- self.__module__, type(self).__name__, shortname)
- self.log = log_mgr.get_logger(self.logger_name)
-
- if ip:
- self.ip = str(ip)
- else:
- if self.config.ipv6:
- # $(dig +short $M $rrtype|tail -1)
- stdout, stderr, returncode = ipautil.run(
- ['dig', '+short', self.external_hostname, 'AAAA'])
- self.ip = stdout.splitlines()[-1].strip()
- else:
- try:
- self.ip = socket.gethostbyname(self.external_hostname)
- except socket.gaierror:
- self.ip = None
-
- if not self.ip:
- raise RuntimeError('Could not determine IP address of %s' %
- self.external_hostname)
-
- self.root_password = self.config.root_password
- self.root_ssh_key_filename = self.config.root_ssh_key_filename
- self.host_key = None
- self.ssh_port = 22
-
- self.env_sh_path = os.path.join(domain.config.test_dir, 'env.sh')
-
- self.log_collectors = []
-
- def __str__(self):
- template = ('<{s.__class__.__name__} {s.hostname} ({s.role})>')
- return template.format(s=self)
-
- def __repr__(self):
- template = ('<{s.__module__}.{s.__class__.__name__} '
- '{s.hostname} ({s.role})>')
- return template.format(s=self)
-
- def add_log_collector(self, collector):
- """Register a log collector for this host"""
- self.log_collectors.append(collector)
-
- def remove_log_collector(self, collector):
- """Unregister a log collector"""
- self.log_collectors.remove(collector)
-
- @classmethod
- def from_env(cls, env, domain, hostname, role, index, domain_index):
- ip = env.get('BEAKER%s%s_IP_env%s' %
- (role.upper(), index, domain_index), None)
- external_hostname = env.get(
- 'BEAKER%s%s_env%s' % (role.upper(), index, domain_index), None)
-
- return cls._make_host(domain, hostname, role, ip, external_hostname)
@staticmethod
def _make_host(domain, hostname, role, ip, external_hostname):
@@ -120,84 +40,6 @@ class BaseHost(object):
return cls(domain, hostname, role, ip, external_hostname)
- @classmethod
- def from_dict(cls, dct, domain):
- if isinstance(dct, basestring):
- dct = {'name': dct}
- try:
- role = dct.pop('role').lower()
- except KeyError:
- role = domain.static_roles[0]
-
- hostname = dct.pop('name')
- if '.' not in hostname:
- hostname = '.'.join((hostname, domain.name))
-
- ip = dct.pop('ip', None)
- external_hostname = dct.pop('external_hostname', None)
-
- check_config_dict_empty(dct, 'host %s' % hostname)
-
- return cls._make_host(domain, hostname, role, ip, external_hostname)
-
- def to_dict(self):
- return {
- 'name': str(self.hostname),
- 'ip': self.ip,
- 'role': self.role,
- 'external_hostname': self.external_hostname,
- }
-
- @property
- def config(self):
- return self.domain.config
-
- def to_env(self, **kwargs):
- """Return environment variables specific to this host"""
- env = self.domain.to_env(**kwargs)
-
- index = self.domain.hosts.index(self) + 1
- domain_index = self.config.domains.index(self.domain) + 1
-
- role = self.role.upper()
- if self.role != 'master':
- role += str(index)
-
- env['MYHOSTNAME'] = self.hostname
- env['MYBEAKERHOSTNAME'] = self.external_hostname
- env['MYIP'] = self.ip
-
- prefix = ('' if self.role in self.domain.static_roles
- else TESTHOST_PREFIX)
- env_suffix = '_env%s' % domain_index
- env['MYROLE'] = '%s%s%s' % (prefix, role, env_suffix)
- env['MYENV'] = str(domain_index)
-
- return env
-
- @property
- def transport(self):
- try:
- return self._transport
- except AttributeError:
- cls = self.transport_class
- if cls:
- # transport_class is None in the base class and must be
- # set in subclasses.
- # Pylint reports that calling None will fail
- self._transport = cls(self) # pylint: disable=E1102
- else:
- raise NotImplementedError('transport class not available')
- return self._transport
-
- def get_file_contents(self, filename):
- """Shortcut for transport.get_file_contents"""
- return self.transport.get_file_contents(filename)
-
- def put_file_contents(self, filename, contents):
- """Shortcut for transport.put_file_contents"""
- self.transport.put_file_contents(filename, contents)
-
def ldap_connect(self):
"""Return an LDAPClient authenticated to this host as directory manager
"""
@@ -208,80 +50,20 @@ class BaseHost(object):
ldap.do_simple_bind(binddn, self.config.dirman_password)
return ldap
- def collect_log(self, filename):
- for collector in self.log_collectors:
- collector(self, filename)
-
- def run_command(self, argv, set_env=True, stdin_text=None,
- log_stdout=True, raiseonerr=True,
- cwd=None):
- """Run the given command on this host
-
- Returns a Shell instance. The command will have already run in the
- shell when this method returns, so its stdout_text, stderr_text, and
- returncode attributes will be available.
-
- :param argv: Command to run, as either a Popen-style list, or a string
- containing a shell script
- :param set_env: If true, env.sh exporting configuration variables will
- be sourced before running the command.
- :param stdin_text: If given, will be written to the command's stdin
- :param log_stdout: If false, standard output will not be logged
- (but will still be available as cmd.stdout_text)
- :param raiseonerr: If true, an exception will be raised if the command
- does not exit with return code 0
- :param cwd: The working directory for the command
- """
- raise NotImplementedError()
-
-
-class Host(BaseHost):
- """A Unix host"""
- transport_class = transport.SSHTransport
-
- def run_command(self, argv, set_env=True, stdin_text=None,
- log_stdout=True, raiseonerr=True,
- cwd=None):
- # This will give us a Bash shell
- command = self.transport.start_shell(argv, log_stdout=log_stdout)
-
- # Set working directory
- if cwd is None:
- cwd = self.config.test_dir
- command.stdin.write('cd %s\n' % ipautil.shell_quote(cwd))
-
- # Set the environment
- if set_env:
- command.stdin.write('. %s\n' %
- ipautil.shell_quote(self.env_sh_path))
- command.stdin.write('set -e\n')
-
- if isinstance(argv, basestring):
- # Run a shell command given as a string
- command.stdin.write('(')
- command.stdin.write(argv)
- command.stdin.write(')')
- else:
- # Run a command given as a popen-style list (no shell expansion)
- for arg in argv:
- command.stdin.write(ipautil.shell_quote(arg))
- command.stdin.write(' ')
-
- command.stdin.write(';exit\n')
- if stdin_text:
- command.stdin.write(stdin_text)
- command.stdin.flush()
+ @classmethod
+ def from_env(cls, env, domain, hostname, role, index, domain_index):
+ from ipatests.test_integration.env_config import host_from_env
+ return host_from_env(env, domain, hostname, role, index, domain_index)
- command.wait(raiseonerr=raiseonerr)
- return command
+ def to_env(self, **kwargs):
+ from ipatests.test_integration.env_config import host_to_env
+ return host_to_env(self, **kwargs)
-class WinHost(BaseHost):
+class WinHost(pytest_multihost.host.WinHost):
"""
Representation of a remote Windows host.
This serves as a sketch class once we move from manual preparation of
Active Directory to the automated setup.
"""
-
- pass
diff --git a/ipatests/test_integration/tasks.py b/ipatests/test_integration/tasks.py
index 1458d7f93..271d726ca 100644
--- a/ipatests/test_integration/tasks.py
+++ b/ipatests/test_integration/tasks.py
@@ -34,7 +34,7 @@ from ipaplatform.paths import paths
from ipapython.dn import DN
from ipapython.ipa_log_manager import log_mgr
from ipatests.test_integration import util
-from ipatests.test_integration.config import env_to_script
+from ipatests.test_integration.env_config import env_to_script
from ipatests.test_integration.host import Host
log = log_mgr.get_logger(__name__)
diff --git a/ipatests/test_integration/test_caless.py b/ipatests/test_integration/test_caless.py
index ef76036c2..caf90dd54 100644
--- a/ipatests/test_integration/test_caless.py
+++ b/ipatests/test_integration/test_caless.py
@@ -24,7 +24,6 @@ import base64
import glob
import contextlib
import nose
-import pytest
from ipalib import x509
from ipapython import ipautil
@@ -67,7 +66,7 @@ def assert_error(result, stderr_text, returncode=None):
class CALessBase(IntegrationTest):
@classmethod
- def install(cls):
+ def install(cls, mh):
super(CALessBase, cls).install()
cls.cert_dir = tempfile.mkdtemp(prefix="ipatest-")
cls.pem_filename = os.path.join(cls.cert_dir, 'root.pem')
@@ -108,7 +107,7 @@ class CALessBase(IntegrationTest):
host.transport.put_file(source, dest)
@classmethod
- def uninstall(cls):
+ def uninstall(cls, mh):
# Remove the NSS database
shutil.rmtree(cls.cert_dir)
@@ -340,7 +339,7 @@ class CALessBase(IntegrationTest):
class TestServerInstall(CALessBase):
num_replicas = 0
- def teardown(self):
+ def tearDown(self):
self.uninstall_server()
# Remove CA cert in /etc/pki/nssdb, in case of failed (un)install
@@ -750,7 +749,7 @@ class TestServerInstall(CALessBase):
class TestReplicaInstall(CALessBase):
num_replicas = 1
- def setup(self):
+ def setUp(self):
# Install the master for every test
self.export_pkcs12('ca1/server')
with open(self.pem_filename, 'w') as f:
@@ -759,7 +758,7 @@ class TestReplicaInstall(CALessBase):
result = self.install_server()
assert result.returncode == 0
- def teardown(self):
+ def tearDown(self):
# Uninstall both master and replica
replica = self.replicas[0]
tasks.kinit_admin(self.master)
@@ -1162,19 +1161,25 @@ class TestIPACommands(CALessBase):
cls.test_hostname = 'testhost.%s' % cls.master.domain.name
cls.test_service = 'test/%s' % cls.test_hostname
- @pytest.mark.parametrize('cmd', (
- 'cert-status',
- 'cert-show',
- 'cert-find',
- 'cert-revoke',
- 'cert-remove-hold',
- 'cert-status'))
- def test_cert_commands_unavailable(self, cmd):
+ def check_ipa_command_not_available(self, command):
"Verify that the given IPA subcommand is not available"
result = self.master.run_command(['ipa', command], raiseonerr=False)
assert_error(result, "ipa: ERROR: unknown command '%s'" % command)
+ def test_cert_commands_unavailable(self):
+ for cmd in (
+ 'cert-status',
+ 'cert-show',
+ 'cert-find',
+ 'cert-revoke',
+ 'cert-remove-hold',
+ 'cert-status'):
+ func = lambda: self.check_ipa_command_not_available(cmd)
+ func.description = 'Verify that %s command is not available' % cmd
+ func.test_argument = cmd
+ yield (func, )
+
def test_cert_help_unavailable(self):
"Verify that cert plugin help is not available"
result = self.master.run_command(['ipa', 'help', 'cert'],
@@ -1241,7 +1246,7 @@ class TestIPACommands(CALessBase):
class TestCertinstall(CALessBase):
@classmethod
- def install(cls):
+ def install(cls, mh):
super(TestCertinstall, cls).install()
cls.export_pkcs12('ca1/server')
diff --git a/ipatests/test_integration/test_forced_client_reenrollment.py b/ipatests/test_integration/test_forced_client_reenrollment.py
index 826e70d24..709bc72c9 100644
--- a/ipatests/test_integration/test_forced_client_reenrollment.py
+++ b/ipatests/test_integration/test_forced_client_reenrollment.py
@@ -35,7 +35,7 @@ class TestForcedClientReenrollment(IntegrationTest):
num_clients = 1
@classmethod
- def install(cls):
+ def install(cls, mh):
super(TestForcedClientReenrollment, cls).install()
tasks.install_master(cls.master)
tasks.install_replica(cls.master, cls.replicas[0], setup_ca=False)
@@ -44,11 +44,11 @@ class TestForcedClientReenrollment(IntegrationTest):
'krb5.keytab'
)
- def setup(self):
+ def setUp(self):
tasks.prepare_host(self.clients[0])
tasks.install_client(self.master, self.clients[0])
- def teardown(self):
+ def tearDown(self):
tasks.uninstall_client(self.clients[0])
self.delete_client_host_entry()
diff --git a/ipatests/test_integration/test_testconfig.py b/ipatests/test_integration/test_testconfig.py
index 974263669..219600344 100644
--- a/ipatests/test_integration/test_testconfig.py
+++ b/ipatests/test_integration/test_testconfig.py
@@ -27,13 +27,13 @@ from ipatests.util import assert_deepequal
DEFAULT_OUTPUT_DICT = {
"nis_domain": "ipatest",
"test_dir": "/root/ipatests",
- "debug": False,
"ad_admin_name": "Administrator",
"ipv6": False,
- "root_ssh_key_filename": "~/.ssh/id_rsa",
+ "ssh_key_filename": "~/.ssh/id_rsa",
+ "ssh_username": "root",
"admin_name": "admin",
"ad_admin_password": "Secret123",
- "root_password": None,
+ "ssh_password": None,
"dns_forwarder": "8.8.8.8",
"domains": [],
"dirman_dn": "cn=Directory Manager",
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
index 597f4ced7..9a7ab2948 100644
--- a/ipatests/test_integration/test_trust.py
+++ b/ipatests/test_integration/test_trust.py
@@ -33,7 +33,7 @@ class ADTrustBase(IntegrationTest):
optional_extra_roles = ['ad_subdomain']
@classmethod
- def install(cls):
+ def install(cls, mh):
super(ADTrustBase, cls).install()
cls.ad = cls.ad_domains[0].ads[0]
cls.install_adtrust()
diff --git a/ipatests/test_integration/transport.py b/ipatests/test_integration/transport.py
deleted file mode 100644
index 066feaef6..000000000
--- a/ipatests/test_integration/transport.py
+++ /dev/null
@@ -1,443 +0,0 @@
-# Authors:
-# Petr Viktorin <pviktori@redhat.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-"""Objects for communicating with remote hosts
-
-This class defines "SSHTransport" as ParamikoTransport (by default), or as
-OpenSSHTransport (if Paramiko is not importable, or the IPA_TEST_SSH_TRANSPORT
-environment variable is set to "openssh").
-"""
-
-import os
-import socket
-import threading
-import subprocess
-from contextlib import contextmanager
-import errno
-
-from ipapython.ipa_log_manager import log_mgr
-from ipatests import util
-
-try:
- import paramiko
- have_paramiko = True
-except ImportError:
- have_paramiko = False
-
-
-class Transport(object):
- """Mechanism for communicating with remote hosts
-
- The Transport can manipulate files on a remote host, and open a Command.
-
- The base class defines an interface that specific subclasses implement.
- """
- def __init__(self, host):
- self.host = host
- self.logger_name = '%s.%s' % (host.logger_name, type(self).__name__)
- self.log = log_mgr.get_logger(self.logger_name)
- self._command_index = 0
-
- def get_file_contents(self, filename):
- """Read the named remote file and return the contents as a string"""
- raise NotImplementedError('Transport.get_file_contents')
-
- def put_file_contents(self, filename, contents):
- """Write the given string to the named remote file"""
- raise NotImplementedError('Transport.put_file_contents')
-
- def file_exists(self, filename):
- """Return true if the named remote file exists"""
- raise NotImplementedError('Transport.file_exists')
-
- def mkdir(self, path):
- """Make the named directory"""
- raise NotImplementedError('Transport.mkdir')
-
- def start_shell(self, argv, log_stdout=True):
- """Start a Shell
-
- :param argv: The command this shell is intended to run (used for
- logging only)
- :param log_stdout: If false, the stdout will not be logged (useful when
- binary output is expected)
-
- Given a `shell` from this method, the caller can then use
- ``shell.stdin.write()`` to input any command(s), call ``shell.wait()``
- to let the command run, and then inspect ``returncode``,
- ``stdout_text`` or ``stderr_text``.
- """
- raise NotImplementedError('Transport.start_shell')
-
- def mkdir_recursive(self, path):
- """`mkdir -p` on the remote host"""
- if not self.file_exists(path):
- parent_path = os.path.dirname(path)
- if path != parent_path:
- self.mkdir_recursive(parent_path)
- self.mkdir(path)
-
- def get_file(self, remotepath, localpath):
- """Copy a file from the remote host to a local file"""
- contents = self.get_file_contents(remotepath)
- with open(localpath, 'wb') as local_file:
- local_file.write(contents)
-
- def put_file(self, localpath, remotepath):
- """Copy a local file to the remote host"""
- with open(localpath, 'rb') as local_file:
- contents = local_file.read()
- self.put_file_contents(remotepath, contents)
-
- def get_next_command_logger_name(self):
- self._command_index += 1
- return '%s.cmd%s' % (self.host.logger_name, self._command_index)
-
-
-class Command(object):
- """A Popen-style object representing a remote command
-
- Instances of this class should only be created via method of a concrete
- Transport, such as start_shell.
-
- The standard error and output are handled by this class. They're not
- available for file-like reading, and are logged by default.
- To make sure reading doesn't stall after one buffer fills up, they are read
- in parallel using threads.
-
- After calling wait(), ``stdout_text`` and ``stderr_text`` attributes will
- be strings containing the output, and ``returncode`` will contain the
- exit code.
- """
- def __init__(self, argv, logger_name=None, log_stdout=True):
- self.returncode = None
- self.argv = argv
- self._done = False
-
- if logger_name:
- self.logger_name = logger_name
- else:
- self.logger_name = '%s.%s' % (self.__module__, type(self).__name__)
- self.log = log_mgr.get_logger(self.logger_name)
-
- def wait(self, raiseonerr=True):
- """Wait for the remote process to exit
-
- Raises an excption if the exit code is not 0, unless raiseonerr is
- true.
- """
- if self._done:
- return self.returncode
-
- self._end_process()
-
- self._done = True
-
- if raiseonerr and self.returncode:
- self.log.error('Exit code: %s', self.returncode)
- raise subprocess.CalledProcessError(self.returncode, self.argv)
- else:
- self.log.debug('Exit code: %s', self.returncode)
- return self.returncode
-
- def _end_process(self):
- """Wait until the process exits and output is received, close channel
-
- Called from wait()
- """
- raise NotImplementedError()
-
-
-class ParamikoTransport(Transport):
- """Transport that uses the Paramiko SSH2 library"""
- def __init__(self, host):
- super(ParamikoTransport, self).__init__(host)
- sock = socket.create_connection((host.external_hostname,
- host.ssh_port))
- self._transport = transport = paramiko.Transport(sock)
- transport.connect(hostkey=host.host_key)
- if host.root_ssh_key_filename:
- self.log.debug('Authenticating with private RSA key')
- filename = os.path.expanduser(host.root_ssh_key_filename)
- key = paramiko.RSAKey.from_private_key_file(filename)
- transport.auth_publickey(username='root', key=key)
- elif host.root_password:
- self.log.debug('Authenticating with password')
- transport.auth_password(username='root',
- password=host.root_password)
- else:
- self.log.critical('No SSH credentials configured')
- raise RuntimeError('No SSH credentials configured')
-
- @contextmanager
- def sftp_open(self, filename, mode='r'):
- """Context manager that provides a file-like object over a SFTP channel
-
- This provides compatibility with older Paramiko versions.
- (In Paramiko 1.10+, file objects from `sftp.open` are directly usable
- as context managers).
- """
- file = self.sftp.open(filename, mode)
- try:
- yield file
- finally:
- file.close()
-
- @property
- def sftp(self):
- """Paramiko SFTPClient connected to this host"""
- try:
- return self._sftp
- except AttributeError:
- transport = self._transport
- self._sftp = paramiko.SFTPClient.from_transport(transport)
- return self._sftp
-
- def get_file_contents(self, filename):
- """Read the named remote file and return the contents as a string"""
- self.log.debug('READ %s', filename)
- with self.sftp_open(filename) as f:
- return f.read()
-
- def put_file_contents(self, filename, contents):
- """Write the given string to the named remote file"""
- self.log.info('WRITE %s', filename)
- with self.sftp_open(filename, 'w') as f:
- f.write(contents)
-
- def file_exists(self, filename):
- """Return true if the named remote file exists"""
- self.log.debug('STAT %s', filename)
- try:
- self.sftp.stat(filename)
- except IOError, e:
- if e.errno == errno.ENOENT:
- return False
- else:
- raise
- return True
-
- def mkdir(self, path):
- self.log.info('MKDIR %s', path)
- self.sftp.mkdir(path)
-
- def start_shell(self, argv, log_stdout=True):
- logger_name = self.get_next_command_logger_name()
- ssh = self._transport.open_channel('session')
- self.log.info('RUN %s', argv)
- return SSHCommand(ssh, argv, logger_name=logger_name,
- log_stdout=log_stdout)
-
- def get_file(self, remotepath, localpath):
- self.log.debug('GET %s', remotepath)
- self.sftp.get(remotepath, localpath)
-
- def put_file(self, localpath, remotepath):
- self.log.info('PUT %s', remotepath)
- self.sftp.put(localpath, remotepath)
-
-
-class OpenSSHTransport(Transport):
- """Transport that uses the `ssh` binary"""
- def __init__(self, host):
- super(OpenSSHTransport, self).__init__(host)
- self.control_dir = util.TempDir()
-
- self.ssh_argv = self._get_ssh_argv()
-
- # Run a "control master" process. This serves two purposes:
- # - Establishes a control socket; other SSHs will connect to it
- # and reuse the same connection. This way the slow handshake
- # only needs to be done once
- # - Writes the host to known_hosts so stderr of "real" connections
- # doesn't contain the "unknown host" warning
- # Popen closes the stdin pipe when it's garbage-collected, so
- # this process will exit when it's no longer needed
- command = ['-o', 'ControlMaster=yes', '/usr/bin/cat']
- self.control_master = self._run(command, collect_output=False)
-
- def _get_ssh_argv(self):
- """Return the path to SSH and options needed for every call"""
- control_file = os.path.join(self.control_dir.path, 'control')
- known_hosts_file = os.path.join(self.control_dir.path, 'known_hosts')
-
- argv = ['ssh',
- '-l', 'root',
- '-o', 'ControlPath=%s' % control_file,
- '-o', 'StrictHostKeyChecking=no',
- '-o', 'UserKnownHostsFile=%s' % known_hosts_file]
-
- if self.host.root_ssh_key_filename:
- key_filename = os.path.expanduser(self.host.root_ssh_key_filename)
- argv.extend(['-i', key_filename])
- elif self.host.root_password:
- self.log.critical('Password authentication not supported')
- raise RuntimeError('Password authentication not supported')
- else:
- self.log.critical('No SSH credentials configured')
- raise RuntimeError('No SSH credentials configured')
-
- argv.append(self.host.external_hostname)
- self.log.debug('SSH invocation: %s', argv)
-
- return argv
-
- def start_shell(self, argv, log_stdout=True):
- self.log.info('RUN %s', argv)
- command = self._run(['bash'], argv=argv, log_stdout=log_stdout)
- return command
-
- def _run(self, command, log_stdout=True, argv=None, collect_output=True):
- """Run the given command on the remote host
-
- :param command: Command to run (appended to the common SSH invocation)
- :param log_stdout: If false, stdout will not be logged
- :param argv: Command to log (if different from ``command``
- :param collect_output: If false, no output will be collected
- """
- if argv is None:
- argv = command
- logger_name = self.get_next_command_logger_name()
- ssh = SSHCallWrapper(self.ssh_argv + list(command))
- return SSHCommand(ssh, argv, logger_name, log_stdout=log_stdout,
- collect_output=collect_output)
-
- def file_exists(self, path):
- self.log.info('STAT %s', path)
- cmd = self._run(['ls', path], log_stdout=False)
- cmd.wait(raiseonerr=False)
-
- return cmd.returncode == 0
-
- def mkdir(self, path):
- self.log.info('MKDIR %s', path)
- cmd = self._run(['mkdir', path])
- cmd.wait()
-
- def put_file_contents(self, filename, contents):
- self.log.info('PUT %s', filename)
- cmd = self._run(['tee', filename], log_stdout=False)
- cmd.stdin.write(contents)
- cmd.wait()
- assert cmd.stdout_text == contents
-
- def get_file_contents(self, filename):
- self.log.info('GET %s', filename)
- cmd = self._run(['cat', filename], log_stdout=False)
- cmd.wait(raiseonerr=False)
- if cmd.returncode == 0:
- return cmd.stdout_text
- else:
- raise IOError('File %r could not be read' % filename)
-
-
-class SSHCallWrapper(object):
- """Adapts a /usr/bin/ssh call to the paramiko.Channel interface
-
- This only wraps what SSHCommand needs.
- """
- def __init__(self, command):
- self.command = command
-
- def invoke_shell(self):
- self.command = subprocess.Popen(
- self.command,
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
-
- def makefile(self, mode):
- return {
- 'wb': self.command.stdin,
- 'rb': self.command.stdout,
- }[mode]
-
- def makefile_stderr(self, mode):
- assert mode == 'rb'
- return self.command.stderr
-
- def shutdown_write(self):
- self.command.stdin.close()
-
- def recv_exit_status(self):
- return self.command.wait()
-
- def close(self):
- return self.command.wait()
-
-
-class SSHCommand(Command):
- """Command implementation for ParamikoTransport and OpenSSHTranspport"""
- def __init__(self, ssh, argv, logger_name, log_stdout=True,
- collect_output=True):
- super(SSHCommand, self).__init__(argv, logger_name,
- log_stdout=log_stdout)
- self._stdout_lines = []
- self._stderr_lines = []
- self.running_threads = set()
-
- self._ssh = ssh
-
- self.log.debug('RUN %s', argv)
-
- self._ssh.invoke_shell()
- stdin = self.stdin = self._ssh.makefile('wb')
- stdout = self._ssh.makefile('rb')
- stderr = self._ssh.makefile_stderr('rb')
-
- if collect_output:
- self._start_pipe_thread(self._stdout_lines, stdout, 'out',
- log_stdout)
- self._start_pipe_thread(self._stderr_lines, stderr, 'err', True)
-
- def _end_process(self, raiseonerr=True):
- self._ssh.shutdown_write()
-
- while self.running_threads:
- self.running_threads.pop().join()
-
- self.stdout_text = ''.join(self._stdout_lines)
- self.stderr_text = ''.join(self._stderr_lines)
- self.returncode = self._ssh.recv_exit_status()
- self._ssh.close()
-
- def _start_pipe_thread(self, result_list, stream, name, do_log=True):
- """Start a thread that copies lines from ``stream`` to ``result_list``
-
- If do_log is true, also logs the lines under ``name``
-
- The thread is added to ``self.running_threads``.
- """
- log = log_mgr.get_logger('%s.%s' % (self.logger_name, name))
-
- def read_stream():
- for line in stream:
- if do_log:
- log.debug(line.rstrip('\n'))
- result_list.append(line)
-
- thread = threading.Thread(target=read_stream)
- self.running_threads.add(thread)
- thread.start()
- return thread
-
-
-if not have_paramiko or os.environ.get('IPA_TEST_SSH_TRANSPORT') == 'openssh':
- SSHTransport = OpenSSHTransport
-else:
- SSHTransport = ParamikoTransport
diff --git a/ipatests/test_integration/util.py b/ipatests/test_integration/util.py
index b2b433519..1a1bb3fcc 100644
--- a/ipatests/test_integration/util.py
+++ b/ipatests/test_integration/util.py
@@ -20,16 +20,6 @@
import time
-TESTHOST_PREFIX = 'TESTHOST_'
-
-
-def check_config_dict_empty(dct, name):
- """Ensure that no keys are left in a configuration dict"""
- if dct:
- raise ValueError('Extra keys in confuguration for %s: %s' %
- (name, ', '.join(dct)))
-
-
def run_repeatedly(host, command, assert_zero_rc=True, test=None,
timeout=30, **kwargs):
"""