Imported Debian patch 4.0.5-6~numeezy
This commit is contained in:
committed by
Mario Fetka
parent
c44de33144
commit
10dfc9587b
@@ -19,29 +19,72 @@
|
||||
|
||||
"""Base class for FreeIPA integration tests"""
|
||||
|
||||
import pytest
|
||||
import nose
|
||||
|
||||
from ipapython.ipa_log_manager import log_mgr
|
||||
from ipatests.test_integration.config import get_global_config
|
||||
from ipatests.test_integration import tasks
|
||||
from pytest_sourceorder import ordered
|
||||
from ipatests.order_plugin import ordered
|
||||
|
||||
log = log_mgr.get_logger(__name__)
|
||||
|
||||
|
||||
@ordered
|
||||
@pytest.mark.usefixtures('mh')
|
||||
@pytest.mark.usefixtures('integration_logs')
|
||||
class IntegrationTest(object):
|
||||
num_replicas = 0
|
||||
num_clients = 0
|
||||
num_ad_domains = 0
|
||||
required_extra_roles = []
|
||||
topology = None
|
||||
domain_level = None
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pass
|
||||
|
||||
def get_resources(resource_container, resource_str, num_needed):
|
||||
if len(resource_container) < num_needed:
|
||||
raise nose.SkipTest(
|
||||
'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 nose.SkipTest('Integration testing not configured')
|
||||
|
||||
cls.logs_to_collect = {}
|
||||
|
||||
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 nose.SkipTest("Not all required extra hosts available, "
|
||||
"missing: %s, available: %s"
|
||||
% (missing_extra_roles,
|
||||
available_extra_roles))
|
||||
|
||||
for host in cls.get_all_hosts():
|
||||
host.add_log_collector(cls.collect_log)
|
||||
cls.prepare_host(host)
|
||||
|
||||
try:
|
||||
cls.install()
|
||||
except:
|
||||
cls.uninstall()
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def host_by_role(cls, role):
|
||||
@@ -55,35 +98,51 @@ class IntegrationTest(object):
|
||||
@classmethod
|
||||
def get_all_hosts(cls):
|
||||
return ([cls.master] + cls.replicas + cls.clients +
|
||||
[cls.host_by_role(r) for r in cls.required_extra_roles])
|
||||
map(cls.host_by_role, cls.required_extra_roles))
|
||||
|
||||
@classmethod
|
||||
def get_domains(cls):
|
||||
return [cls.domain] + cls.ad_domains
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
if cls.domain_level is not None:
|
||||
domain_level = cls.domain_level
|
||||
else:
|
||||
domain_level = cls.master.config.domain_level
|
||||
def prepare_host(cls, host):
|
||||
cls.log.info('Preparing host %s', host.hostname)
|
||||
tasks.prepare_host(host)
|
||||
|
||||
@classmethod
|
||||
def install(cls):
|
||||
if cls.topology is None:
|
||||
return
|
||||
else:
|
||||
tasks.install_topo(cls.topology,
|
||||
cls.master, cls.replicas,
|
||||
cls.clients, domain_level)
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
pass
|
||||
cls.master, cls.replicas, cls.clients)
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls, mh):
|
||||
def teardown_class(cls):
|
||||
for host in cls.get_all_hosts():
|
||||
host.remove_log_collector(cls.collect_log)
|
||||
|
||||
try:
|
||||
cls.uninstall()
|
||||
finally:
|
||||
del cls.master
|
||||
del cls.replicas
|
||||
del cls.clients
|
||||
del cls.ad_domains
|
||||
del cls.domain
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
tasks.uninstall_master(cls.master)
|
||||
for replica in cls.replicas:
|
||||
tasks.uninstall_master(replica)
|
||||
for client in cls.clients:
|
||||
tasks.uninstall_client(client)
|
||||
|
||||
@classmethod
|
||||
def collect_log(cls, host, filename):
|
||||
cls.log.info('Adding %s:%s to list of logs to collect' %
|
||||
(host.external_hostname, filename))
|
||||
cls.logs_to_collect.setdefault(host, []).append(filename)
|
||||
|
||||
IntegrationTest.log = log_mgr.get_logger(IntegrationTest())
|
||||
|
||||
@@ -20,114 +20,412 @@
|
||||
|
||||
"""Utilities for configuration of multi-master tests"""
|
||||
|
||||
import os
|
||||
import collections
|
||||
import random
|
||||
import json
|
||||
|
||||
import pytest_multihost.config
|
||||
|
||||
from ipapython import ipautil
|
||||
from ipapython.dn import DN
|
||||
from ipapython.ipa_log_manager import log_mgr
|
||||
from ipalib.constants import MAX_DOMAIN_LEVEL
|
||||
from ipatests.test_integration.util import check_config_dict_empty
|
||||
from ipatests.test_integration.util import TESTHOST_PREFIX
|
||||
|
||||
|
||||
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',
|
||||
'domain_level',
|
||||
}
|
||||
_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(object):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault('test_dir', '/root/ipatests')
|
||||
super(Config, self).__init__(**kwargs)
|
||||
self.log = log_mgr.get_logger(self)
|
||||
|
||||
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.domain_level = kwargs.get('domain_level', MAX_DOMAIN_LEVEL)
|
||||
# 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
|
||||
if self.domain_level is None:
|
||||
self.domain_level = MAX_DOMAIN_LEVEL
|
||||
self.ipv6 = bool(kwargs.get('ipv6', False))
|
||||
self.debug = bool(kwargs.get('debug', False))
|
||||
|
||||
def get_domain_class(self):
|
||||
return Domain
|
||||
if not self.root_password and not self.root_ssh_key_filename:
|
||||
self.root_ssh_key_filename = '~/.ssh/id_rsa'
|
||||
|
||||
def get_logger(self, name):
|
||||
return log_mgr.get_logger(name)
|
||||
self.domains = []
|
||||
|
||||
@property
|
||||
def ad_domains(self):
|
||||
return [d for d in self.domains if d.type == 'AD']
|
||||
return filter(lambda d: d.type == 'AD', self.domains)
|
||||
|
||||
def get_all_hosts(self):
|
||||
for domain in self.domains:
|
||||
for host in domain.hosts:
|
||||
yield host
|
||||
@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 to_dict(self):
|
||||
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
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env):
|
||||
from ipatests.test_integration.env_config import config_from_env
|
||||
return config_from_env(env)
|
||||
"""Create a test config from environment variables
|
||||
|
||||
def to_env(self, **kwargs):
|
||||
from ipatests.test_integration.env_config import config_to_env
|
||||
return config_to_env(self, **kwargs)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Domain(pytest_multihost.config.Domain):
|
||||
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):
|
||||
"""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')
|
||||
elif self.type == 'AD':
|
||||
else:
|
||||
return ('ad',)
|
||||
else:
|
||||
raise LookupError(self.type)
|
||||
|
||||
def get_host_class(self, host_dict):
|
||||
from ipatests.test_integration.host import Host, WinHost
|
||||
@property
|
||||
def extra_roles(self):
|
||||
return [role for role in self.roles if role not in self.static_roles]
|
||||
|
||||
if self.type == 'IPA':
|
||||
return Host
|
||||
elif self.type == 'AD':
|
||||
return WinHost
|
||||
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:
|
||||
raise LookupError(self.type)
|
||||
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), '')
|
||||
|
||||
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)
|
||||
|
||||
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]
|
||||
else:
|
||||
raise LookupError(role)
|
||||
|
||||
def hosts_by_role(self, role):
|
||||
return [h for h in self.hosts if h.role == role]
|
||||
|
||||
@property
|
||||
def master(self):
|
||||
@@ -153,11 +451,17 @@ class Domain(pytest_multihost.config.Domain):
|
||||
def other_hosts(self):
|
||||
return self.hosts_by_role('other')
|
||||
|
||||
@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 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 to_env(self, **kwargs):
|
||||
from ipatests.test_integration.env_config import domain_to_env
|
||||
return domain_to_env(self, **kwargs)
|
||||
|
||||
def env_to_script(env):
|
||||
return ''.join(['export %s=%s\n' % (key, ipautil.shell_quote(value))
|
||||
for key, value in env.items()])
|
||||
|
||||
|
||||
def get_global_config():
|
||||
return Config.from_env(os.environ)
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
# 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
|
||||
|
||||
import six
|
||||
|
||||
from ipapython import ipautil
|
||||
from ipatests.test_integration.config import Config, Domain
|
||||
from ipalib.constants import MAX_DOMAIN_LEVEL
|
||||
|
||||
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),
|
||||
_SettingInfo('domain_level', 'DOMAINLVL', MAX_DOMAIN_LEVEL),
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
try:
|
||||
import yaml
|
||||
except ImportError as e:
|
||||
raise ImportError("%s, please install PyYAML package to fix it" %
|
||||
e.message)
|
||||
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'], six.string_types):
|
||||
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()])
|
||||
@@ -19,13 +19,93 @@
|
||||
|
||||
"""Host class for integration testing"""
|
||||
|
||||
import pytest_multihost.host
|
||||
import os
|
||||
import socket
|
||||
|
||||
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 Host(pytest_multihost.host.Host):
|
||||
class BaseHost(object):
|
||||
"""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):
|
||||
@@ -40,6 +120,84 @@ class Host(pytest_multihost.host.Host):
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -50,20 +208,80 @@ class Host(pytest_multihost.host.Host):
|
||||
ldap.do_simple_bind(binddn, self.config.dirman_password)
|
||||
return ldap
|
||||
|
||||
@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)
|
||||
def collect_log(self, filename):
|
||||
for collector in self.log_collectors:
|
||||
collector(self, filename)
|
||||
|
||||
def to_env(self, **kwargs):
|
||||
from ipatests.test_integration.env_config import host_to_env
|
||||
return host_to_env(self, **kwargs)
|
||||
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 WinHost(pytest_multihost.host.WinHost):
|
||||
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()
|
||||
|
||||
command.wait(raiseonerr=raiseonerr)
|
||||
return command
|
||||
|
||||
|
||||
class WinHost(BaseHost):
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -24,55 +24,22 @@ import textwrap
|
||||
import re
|
||||
import collections
|
||||
import itertools
|
||||
import tempfile
|
||||
import time
|
||||
import StringIO
|
||||
|
||||
import dns
|
||||
from ldif import LDIFWriter
|
||||
from SSSDConfig import SSSDConfig
|
||||
from six import StringIO
|
||||
|
||||
from ipapython import ipautil
|
||||
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.env_config import env_to_script
|
||||
from ipatests.test_integration.config import env_to_script
|
||||
from ipatests.test_integration.host import Host
|
||||
from ipalib import errors
|
||||
from ipalib.util import get_reverse_zone_default, verify_host_resolvable
|
||||
from ipalib.constants import DOMAIN_SUFFIX_NAME
|
||||
from ipalib.constants import DOMAIN_LEVEL_0
|
||||
|
||||
log = log_mgr.get_logger(__name__)
|
||||
|
||||
|
||||
def check_arguments_are(slice, instanceof):
|
||||
"""
|
||||
:param: slice - tuple of integers denoting the beginning and the end
|
||||
of argument list to be checked
|
||||
:param: instanceof - name of the class the checked arguments should be
|
||||
instances of
|
||||
Example: @check_arguments_are((1, 3), int) will check that the second
|
||||
and third arguments are integers
|
||||
"""
|
||||
def wrapper(func):
|
||||
def wrapped(*args):
|
||||
for i in args[slice[0]:slice[1]]:
|
||||
assert isinstance(i, instanceof), "Wrong type: %s: %s" % (i, type(i))
|
||||
return func(*args)
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
def prepare_reverse_zone(host, ip):
|
||||
zone = get_reverse_zone_default(ip)
|
||||
result = host.run_command(["ipa",
|
||||
"dnszone-add",
|
||||
zone], raiseonerr=False)
|
||||
if result.returncode > 0:
|
||||
log.warning(result.stderr_text)
|
||||
return zone, result.returncode
|
||||
|
||||
def prepare_host(host):
|
||||
if isinstance(host, Host):
|
||||
env_filename = os.path.join(host.config.test_dir, 'env.sh')
|
||||
@@ -81,24 +48,14 @@ def prepare_host(host):
|
||||
host.run_command(['true'], set_env=False)
|
||||
|
||||
host.collect_log(env_filename)
|
||||
try:
|
||||
host.transport.mkdir_recursive(host.config.test_dir)
|
||||
except IOError:
|
||||
# The folder already exists
|
||||
pass
|
||||
host.transport.mkdir_recursive(host.config.test_dir)
|
||||
host.put_file_contents(env_filename, env_to_script(host.to_env()))
|
||||
|
||||
|
||||
def allow_sync_ptr(host):
|
||||
kinit_admin(host)
|
||||
host.run_command(["ipa", "dnsconfig-mod", "--allow-sync-ptr=true"],
|
||||
raiseonerr=False)
|
||||
|
||||
|
||||
def apply_common_fixes(host):
|
||||
fix_etc_hosts(host)
|
||||
fix_hostname(host)
|
||||
prepare_host(host)
|
||||
fix_resolv_conf(host)
|
||||
|
||||
|
||||
def backup_file(host, filename):
|
||||
@@ -122,14 +79,12 @@ def fix_etc_hosts(host):
|
||||
backup_file(host, paths.HOSTS)
|
||||
contents = host.get_file_contents(paths.HOSTS)
|
||||
# Remove existing mentions of the host's FQDN, short name, and IP
|
||||
# Removing of IP must be done as first, otherwise hosts file may be
|
||||
# corrupted
|
||||
contents = re.sub('^%s.*' % re.escape(host.ip), '', contents,
|
||||
flags=re.MULTILINE)
|
||||
contents = re.sub('\s%s(\s|$)' % re.escape(host.hostname), ' ', contents,
|
||||
flags=re.MULTILINE)
|
||||
contents = re.sub('\s%s(\s|$)' % re.escape(host.shortname), ' ', contents,
|
||||
flags=re.MULTILINE)
|
||||
contents = re.sub('^%s.*' % re.escape(host.ip), '', contents,
|
||||
flags=re.MULTILINE)
|
||||
# Add the host's info again
|
||||
contents += '\n%s %s %s\n' % (host.ip, host.hostname, host.shortname)
|
||||
log.debug('Writing the following to /etc/hosts:\n%s', contents)
|
||||
@@ -145,14 +100,16 @@ def fix_hostname(host):
|
||||
host.run_command('hostname > %s' % ipautil.shell_quote(backupname))
|
||||
|
||||
|
||||
def host_service_active(host, service):
|
||||
res = host.run_command(['systemctl', 'is-active', '--quiet', service],
|
||||
raiseonerr=False)
|
||||
|
||||
if res.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
def fix_resolv_conf(host):
|
||||
backup_file(host, paths.RESOLV_CONF)
|
||||
lines = host.get_file_contents(paths.RESOLV_CONF).splitlines()
|
||||
lines = ['#' + l if l.startswith('nameserver') else l for l in lines]
|
||||
for other_host in host.domain.hosts:
|
||||
if other_host.role in ('master', 'replica'):
|
||||
lines.append('nameserver %s' % other_host.ip)
|
||||
contents = '\n'.join(lines)
|
||||
log.debug('Writing the following to /etc/resolv.conf:\n%s', contents)
|
||||
host.put_file_contents(paths.RESOLV_CONF, contents)
|
||||
|
||||
|
||||
def fix_apache_semaphores(master):
|
||||
@@ -161,8 +118,7 @@ def fix_apache_semaphores(master):
|
||||
if systemd_available:
|
||||
master.run_command(['systemctl', 'stop', 'httpd'], raiseonerr=False)
|
||||
else:
|
||||
master.run_command([paths.SBIN_SERVICE, 'httpd', 'stop'],
|
||||
raiseonerr=False)
|
||||
master.run_command([paths.SBIN_SERVICE, 'httpd', 'stop'], raiseonerr=False)
|
||||
|
||||
master.run_command('for line in `ipcs -s | grep apache | cut -d " " -f 2`; '
|
||||
'do ipcrm -s $line; done', raiseonerr=False)
|
||||
@@ -228,10 +184,7 @@ def enable_replication_debugging(host):
|
||||
stdin_text=logging_ldif)
|
||||
|
||||
|
||||
def install_master(host, setup_dns=True, setup_kra=False, extra_args=(),
|
||||
domain_level=None):
|
||||
if domain_level is None:
|
||||
domain_level = host.config.domain_level
|
||||
def install_master(host):
|
||||
host.collect_log(paths.IPASERVER_INSTALL_LOG)
|
||||
host.collect_log(paths.IPACLIENT_INSTALL_LOG)
|
||||
inst = host.domain.realm.replace('.', '-')
|
||||
@@ -241,129 +194,48 @@ def install_master(host, setup_dns=True, setup_kra=False, extra_args=(),
|
||||
apply_common_fixes(host)
|
||||
fix_apache_semaphores(host)
|
||||
|
||||
args = [
|
||||
'ipa-server-install', '-U',
|
||||
'-n', host.domain.name,
|
||||
'-r', host.domain.realm,
|
||||
'-p', host.config.dirman_password,
|
||||
'-a', host.config.admin_password,
|
||||
"--domain-level=%i" % domain_level,
|
||||
]
|
||||
host.run_command(['ipa-server-install', '-U',
|
||||
'-r', host.domain.name,
|
||||
'-p', host.config.dirman_password,
|
||||
'-a', host.config.admin_password,
|
||||
'--setup-dns',
|
||||
'--forwarder', host.config.dns_forwarder])
|
||||
|
||||
if setup_dns:
|
||||
args.extend([
|
||||
'--setup-dns',
|
||||
'--forwarder', host.config.dns_forwarder,
|
||||
'--auto-reverse'
|
||||
])
|
||||
|
||||
args.extend(extra_args)
|
||||
|
||||
host.run_command(args)
|
||||
enable_replication_debugging(host)
|
||||
setup_sssd_debugging(host)
|
||||
|
||||
if setup_kra:
|
||||
args = [
|
||||
"ipa-kra-install",
|
||||
"-p", host.config.dirman_password,
|
||||
"-U",
|
||||
]
|
||||
host.run_command(args)
|
||||
|
||||
kinit_admin(host)
|
||||
|
||||
|
||||
def get_replica_filename(replica):
|
||||
return os.path.join(replica.config.test_dir, 'replica-info.gpg')
|
||||
|
||||
|
||||
def domainlevel(host):
|
||||
# Dynamically determines the domainlevel on master. Needed for scenarios
|
||||
# when domainlevel is changed during the test execution.
|
||||
result = host.run_command(['ipa', 'domainlevel-get'], raiseonerr=False)
|
||||
level = 0
|
||||
domlevel_re = re.compile('.*(\d)')
|
||||
if result.returncode == 0:
|
||||
# "domainlevel-get" command doesn't exist on ipa versions prior to 4.3
|
||||
level = int(domlevel_re.findall(result.stdout_text)[0])
|
||||
return level
|
||||
|
||||
def master_authoritative_for_client_domain(master, client):
|
||||
zone = ".".join(client.hostname.split('.')[1:])
|
||||
result = master.run_command(["ipa", "dnszone-show", zone],
|
||||
raiseonerr=False)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def replica_prepare(master, replica):
|
||||
fix_apache_semaphores(replica)
|
||||
prepare_reverse_zone(master, replica.ip)
|
||||
args = ['ipa-replica-prepare',
|
||||
'-p', replica.config.dirman_password,
|
||||
replica.hostname]
|
||||
if master_authoritative_for_client_domain(master, replica):
|
||||
args.extend(['--ip-address', replica.ip])
|
||||
master.run_command(args)
|
||||
replica_bundle = master.get_file_contents(
|
||||
paths.REPLICA_INFO_GPG_TEMPLATE % replica.hostname)
|
||||
replica_filename = get_replica_filename(replica)
|
||||
replica.put_file_contents(replica_filename, replica_bundle)
|
||||
|
||||
|
||||
def install_replica(master, replica, setup_ca=True, setup_dns=False,
|
||||
setup_kra=False, extra_args=(), domain_level=None):
|
||||
if domain_level is None:
|
||||
domain_level = domainlevel(master)
|
||||
apply_common_fixes(replica)
|
||||
def install_replica(master, replica, setup_ca=True):
|
||||
replica.collect_log(paths.IPAREPLICA_INSTALL_LOG)
|
||||
replica.collect_log(paths.IPAREPLICA_CONNCHECK_LOG)
|
||||
allow_sync_ptr(master)
|
||||
# Otherwise ipa-client-install would not create a PTR
|
||||
# and replica installation would fail
|
||||
|
||||
apply_common_fixes(replica)
|
||||
fix_apache_semaphores(replica)
|
||||
|
||||
master.run_command(['ipa-replica-prepare',
|
||||
'-p', replica.config.dirman_password,
|
||||
'--ip-address', replica.ip,
|
||||
replica.hostname])
|
||||
replica_bundle = master.get_file_contents(
|
||||
paths.REPLICA_INFO_GPG_TEMPLATE % replica.hostname)
|
||||
replica_filename = os.path.join(replica.config.test_dir,
|
||||
'replica-info.gpg')
|
||||
replica.put_file_contents(replica_filename, replica_bundle)
|
||||
args = ['ipa-replica-install', '-U',
|
||||
'--setup-ca',
|
||||
'-p', replica.config.dirman_password,
|
||||
'-w', replica.config.admin_password]
|
||||
'-w', replica.config.admin_password,
|
||||
'--ip-address', replica.ip,
|
||||
replica_filename]
|
||||
if setup_ca:
|
||||
args.append('--setup-ca')
|
||||
if setup_dns:
|
||||
args.extend([
|
||||
'--setup-dns',
|
||||
'--forwarder', replica.config.dns_forwarder
|
||||
])
|
||||
if master_authoritative_for_client_domain(master, replica):
|
||||
args.extend(['--ip-address', replica.ip])
|
||||
|
||||
args.extend(extra_args)
|
||||
|
||||
if domain_level == DOMAIN_LEVEL_0:
|
||||
# prepare the replica file on master and put it to replica, AKA "old way"
|
||||
replica_prepare(master, replica)
|
||||
replica_filename = get_replica_filename(replica)
|
||||
args.append(replica_filename)
|
||||
else:
|
||||
# install client on a replica machine and then promote it to replica
|
||||
install_client(master, replica)
|
||||
fix_apache_semaphores(replica)
|
||||
args.extend(['-r', replica.domain.realm])
|
||||
replica.run_command(args)
|
||||
|
||||
enable_replication_debugging(replica)
|
||||
setup_sssd_debugging(replica)
|
||||
|
||||
if setup_kra:
|
||||
assert setup_ca, "CA must be installed on replica with KRA"
|
||||
args = [
|
||||
"ipa-kra-install",
|
||||
"-p", replica.config.dirman_password,
|
||||
"-U",
|
||||
]
|
||||
if domainlevel(master) == DOMAIN_LEVEL_0:
|
||||
args.append(replica_filename)
|
||||
replica.run_command(args)
|
||||
|
||||
kinit_admin(replica)
|
||||
|
||||
|
||||
@@ -371,14 +243,6 @@ def install_client(master, client, extra_args=()):
|
||||
client.collect_log(paths.IPACLIENT_INSTALL_LOG)
|
||||
|
||||
apply_common_fixes(client)
|
||||
allow_sync_ptr(master)
|
||||
# Now, for the situations where a client resides in a different subnet from
|
||||
# master, we need to explicitly tell master to create a reverse zone for
|
||||
# the client and enable dynamic updates for this zone.
|
||||
zone, error = prepare_reverse_zone(master, client.ip)
|
||||
if not error:
|
||||
master.run_command(["ipa", "dnszone-mod", zone,
|
||||
"--dynamic-update=TRUE"])
|
||||
|
||||
client.run_command(['ipa-client-install', '-U',
|
||||
'--domain', client.domain.name,
|
||||
@@ -386,7 +250,7 @@ def install_client(master, client, extra_args=()):
|
||||
'-p', client.config.admin_name,
|
||||
'-w', client.config.admin_password,
|
||||
'--server', master.hostname]
|
||||
+ list(extra_args))
|
||||
+ list(extra_args))
|
||||
|
||||
setup_sssd_debugging(client)
|
||||
kinit_admin(client)
|
||||
@@ -414,18 +278,11 @@ def install_adtrust(host):
|
||||
|
||||
# Restart named because it lost connection to dirsrv
|
||||
# (Directory server restarts during the ipa-adtrust-install)
|
||||
# we use two services named and named-pkcs11,
|
||||
# if named is masked restart named-pkcs11
|
||||
result = host.run_command(['systemctl', 'is-enabled', 'named'],
|
||||
raiseonerr=False)
|
||||
if result.stdout_text.startswith("masked"):
|
||||
host.run_command(['systemctl', 'restart', 'named-pkcs11'])
|
||||
else:
|
||||
host.run_command(['systemctl', 'restart', 'named'])
|
||||
host.run_command(['systemctl', 'restart', 'named'])
|
||||
|
||||
# Check that named is running and has loaded the information from LDAP
|
||||
dig_command = ['dig', 'SRV', '+short', '@localhost',
|
||||
'_ldap._tcp.%s' % host.domain.name]
|
||||
'_ldap._tcp.%s' % host.domain.name]
|
||||
dig_output = '0 100 389 %s.' % host.hostname
|
||||
dig_test = lambda x: re.search(re.escape(dig_output), x)
|
||||
|
||||
@@ -497,9 +354,9 @@ def establish_trust_with_ad(master, ad, extra_args=()):
|
||||
master.run_command(['smbcontrol', 'all', 'debug', '100'])
|
||||
util.run_repeatedly(master,
|
||||
['ipa', 'trust-add',
|
||||
'--type', 'ad', ad.domain.name,
|
||||
'--admin', 'Administrator',
|
||||
'--password'] + list(extra_args),
|
||||
'--type', 'ad', ad.domain.name,
|
||||
'--admin', 'Administrator',
|
||||
'--password'] + list(extra_args),
|
||||
stdin_text=master.config.ad_admin_password)
|
||||
master.run_command(['smbcontrol', 'all', 'debug', '1'])
|
||||
clear_sssd_cache(master)
|
||||
@@ -552,14 +409,15 @@ def setup_sssd_debugging(host):
|
||||
# First, remove any previous occurences
|
||||
host.run_command(['sed', '-i',
|
||||
'/debug_level = 7/d',
|
||||
paths.SSSD_CONF],
|
||||
raiseonerr=False)
|
||||
paths.SSSD_CONF
|
||||
], raiseonerr=False)
|
||||
|
||||
# Add the debug directive to each section
|
||||
host.run_command(['sed', '-i',
|
||||
'/\[*\]/ a\debug_level = 7',
|
||||
paths.SSSD_CONF],
|
||||
raiseonerr=False)
|
||||
paths.SSSD_CONF
|
||||
], raiseonerr=False)
|
||||
|
||||
|
||||
host.collect_log('/var/log/sssd/*')
|
||||
|
||||
@@ -567,47 +425,6 @@ def setup_sssd_debugging(host):
|
||||
clear_sssd_cache(host)
|
||||
|
||||
|
||||
def modify_sssd_conf(host, domain, mod_dict, provider='ipa',
|
||||
provider_subtype=None):
|
||||
"""
|
||||
modify options in a single domain section of host's sssd.conf
|
||||
:param host: multihost.Host object
|
||||
:param domain: domain section name to modify
|
||||
:param mod_dict: dictionary of options which will be passed to
|
||||
SSSDDomain.set_option(). To remove an option specify its value as
|
||||
None
|
||||
:param provider: provider backend to set. Defaults to ipa
|
||||
:param provider_subtype: backend subtype (e.g. id or sudo), will be added
|
||||
to the domain config if not present
|
||||
"""
|
||||
try:
|
||||
temp_config_file = tempfile.mkstemp()[1]
|
||||
current_config = host.transport.get_file_contents(paths.SSSD_CONF)
|
||||
|
||||
with open(temp_config_file, 'wb') as f:
|
||||
f.write(current_config)
|
||||
|
||||
sssd_config = SSSDConfig()
|
||||
sssd_config.import_config(temp_config_file)
|
||||
sssd_domain = sssd_config.get_domain(domain)
|
||||
|
||||
if provider_subtype is not None:
|
||||
sssd_domain.add_provider(provider, provider_subtype)
|
||||
|
||||
for m in mod_dict:
|
||||
sssd_domain.set_option(m, mod_dict[m])
|
||||
|
||||
sssd_config.save_domain(sssd_domain)
|
||||
|
||||
new_config = sssd_config.dump(sssd_config.opts).encode('utf-8')
|
||||
host.transport.put_file_contents(paths.SSSD_CONF, new_config)
|
||||
finally:
|
||||
try:
|
||||
os.remove(temp_config_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def clear_sssd_cache(host):
|
||||
"""
|
||||
Clears SSSD cache by removing the cache files. Restarts SSSD.
|
||||
@@ -644,48 +461,26 @@ def sync_time(host, server):
|
||||
host.run_command(['ntpdate', server.hostname])
|
||||
|
||||
|
||||
def connect_replica(master, replica, domain_level=None):
|
||||
if domain_level is None:
|
||||
domain_level = master.config.domain_level
|
||||
if domain_level == DOMAIN_LEVEL_0:
|
||||
replica.run_command(['ipa-replica-manage', 'connect', master.hostname])
|
||||
else:
|
||||
kinit_admin(master)
|
||||
master.run_command(["ipa", "topologysegment-add", DOMAIN_SUFFIX_NAME,
|
||||
"%s-to-%s" % (master.hostname, replica.hostname),
|
||||
"--leftnode=%s" % master.hostname,
|
||||
"--rightnode=%s" % replica.hostname
|
||||
])
|
||||
def connect_replica(master, replica):
|
||||
kinit_admin(replica)
|
||||
replica.run_command(['ipa-replica-manage', 'connect', master.hostname])
|
||||
|
||||
|
||||
def disconnect_replica(master, replica, domain_level=None):
|
||||
if domain_level is None:
|
||||
domain_level = master.config.domain_level
|
||||
if domain_level == DOMAIN_LEVEL_0:
|
||||
replica.run_command(['ipa-replica-manage', 'disconnect', master.hostname])
|
||||
else:
|
||||
kinit_admin(master)
|
||||
master.run_command(["ipa", "topologysegment-del", DOMAIN_SUFFIX_NAME,
|
||||
"%s-to-%s" % (master.hostname, replica.hostname),
|
||||
"--continue"
|
||||
])
|
||||
def disconnect_replica(master, replica):
|
||||
kinit_admin(replica)
|
||||
replica.run_command(['ipa-replica-manage', 'disconnect', master.hostname])
|
||||
|
||||
|
||||
def kinit_admin(host):
|
||||
host.run_command(['kinit', 'admin'],
|
||||
stdin_text=host.config.admin_password)
|
||||
stdin_text=host.config.admin_password)
|
||||
|
||||
|
||||
def uninstall_master(host, ignore_topology_disconnect=True):
|
||||
def uninstall_master(host):
|
||||
host.collect_log(paths.IPASERVER_UNINSTALL_LOG)
|
||||
uninstall_cmd = ['ipa-server-install', '--uninstall', '-U']
|
||||
|
||||
host_domain_level = domainlevel(host)
|
||||
|
||||
if ignore_topology_disconnect and host_domain_level != DOMAIN_LEVEL_0:
|
||||
uninstall_cmd.append('--ignore-topology-disconnect')
|
||||
|
||||
host.run_command(uninstall_cmd, raiseonerr=False)
|
||||
host.run_command(['ipa-server-install', '--uninstall', '-U'],
|
||||
raiseonerr=False)
|
||||
host.run_command(['pkidestroy', '-s', 'CA', '-i', 'pki-tomcat'],
|
||||
raiseonerr=False)
|
||||
host.run_command(['rm', '-rf',
|
||||
@@ -693,9 +488,8 @@ def uninstall_master(host, ignore_topology_disconnect=True):
|
||||
paths.SYSCONFIG_PKI_TOMCAT,
|
||||
paths.SYSCONFIG_PKI_TOMCAT_PKI_TOMCAT_DIR,
|
||||
paths.VAR_LIB_PKI_TOMCAT_DIR,
|
||||
paths.PKI_TOMCAT,
|
||||
paths.REPLICA_INFO_GPG_TEMPLATE % host.hostname],
|
||||
raiseonerr=False)
|
||||
paths.PKI_TOMCAT],
|
||||
raiseonerr=False)
|
||||
unapply_fixes(host)
|
||||
|
||||
|
||||
@@ -707,56 +501,6 @@ def uninstall_client(host):
|
||||
unapply_fixes(host)
|
||||
|
||||
|
||||
@check_arguments_are((0, 2), Host)
|
||||
def clean_replication_agreement(master, replica):
|
||||
"""
|
||||
Performs `ipa-replica-manage del replica_hostname --force`.
|
||||
"""
|
||||
master.run_command(['ipa-replica-manage',
|
||||
'del',
|
||||
replica.hostname,
|
||||
'--force'])
|
||||
|
||||
|
||||
@check_arguments_are((0, 3), Host)
|
||||
def create_segment(master, leftnode, rightnode):
|
||||
"""
|
||||
creates a topology segment. The first argument is a node to run the command
|
||||
:returns: a hash object containing segment's name, leftnode, rightnode
|
||||
information and an error string.
|
||||
"""
|
||||
kinit_admin(master)
|
||||
lefthost = leftnode.hostname
|
||||
righthost = rightnode.hostname
|
||||
segment_name = "%s-to-%s" % (lefthost, righthost)
|
||||
result = master.run_command(["ipa", "topologysegment-add", DOMAIN_SUFFIX_NAME,
|
||||
segment_name,
|
||||
"--leftnode=%s" % lefthost,
|
||||
"--rightnode=%s" % righthost], raiseonerr=False)
|
||||
if result.returncode == 0:
|
||||
return {'leftnode': lefthost,
|
||||
'rightnode': righthost,
|
||||
'name': segment_name}, ""
|
||||
else:
|
||||
return {}, result.stderr_text
|
||||
|
||||
|
||||
def destroy_segment(master, segment_name):
|
||||
"""
|
||||
Destroys topology segment.
|
||||
:param master: reference to master object of class Host
|
||||
:param segment_name: name of the segment to be created
|
||||
"""
|
||||
assert isinstance(master, Host), "master should be an instance of Host"
|
||||
kinit_admin(master)
|
||||
command = ["ipa",
|
||||
"topologysegment-del",
|
||||
DOMAIN_SUFFIX_NAME,
|
||||
segment_name]
|
||||
result = master.run_command(command, raiseonerr=False)
|
||||
return result.returncode, result.stderr_text
|
||||
|
||||
|
||||
def get_topo(name_or_func):
|
||||
"""Get a topology function by name
|
||||
|
||||
@@ -875,123 +619,13 @@ def tree2_topo(master, replicas):
|
||||
master = replica
|
||||
|
||||
|
||||
@_topo('2-connected')
|
||||
def two_connected_topo(master, replicas):
|
||||
r"""No replica has more than 4 agreements and at least two
|
||||
replicas must fail to disconnect the topology.
|
||||
|
||||
. . . .
|
||||
. . . .
|
||||
. . . .
|
||||
... R --- R R --- R ...
|
||||
\ / \ / \ /
|
||||
\ / \ / \ /
|
||||
... R R R ...
|
||||
\ / \ /
|
||||
\ / \ /
|
||||
M0 -- R2
|
||||
| |
|
||||
| |
|
||||
R1 -- R3
|
||||
. \ / .
|
||||
. \ / .
|
||||
. R .
|
||||
. .
|
||||
. .
|
||||
. .
|
||||
"""
|
||||
grow = []
|
||||
pool = [master] + replicas
|
||||
|
||||
try:
|
||||
v0 = pool.pop(0)
|
||||
v1 = pool.pop(0)
|
||||
yield v0, v1
|
||||
|
||||
v2 = pool.pop(0)
|
||||
yield v0, v2
|
||||
grow.append((v0, v2))
|
||||
|
||||
v3 = pool.pop(0)
|
||||
yield v2, v3
|
||||
yield v1, v3
|
||||
grow.append((v1, v3))
|
||||
|
||||
for (r, s) in grow:
|
||||
t = pool.pop(0)
|
||||
|
||||
for (u, v) in [(r, t), (s, t)]:
|
||||
yield u, v
|
||||
w = pool.pop(0)
|
||||
yield u, w
|
||||
x = pool.pop(0)
|
||||
yield v, x
|
||||
yield w, x
|
||||
grow.append((w, x))
|
||||
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
|
||||
@_topo('double-circle')
|
||||
def double_circle_topo(master, replicas, site_size=6):
|
||||
"""
|
||||
R--R
|
||||
|\/|
|
||||
|/\|
|
||||
R--R
|
||||
/ \
|
||||
M -- R
|
||||
/| |\
|
||||
/ | | \
|
||||
R - R - R--|----|--R - R - R
|
||||
| X | | | | | | X |
|
||||
R - R - R -|----|--R - R - R
|
||||
\ | | /
|
||||
\| |/
|
||||
R -- R
|
||||
\ /
|
||||
R--R
|
||||
|\/|
|
||||
|/\|
|
||||
R--R
|
||||
"""
|
||||
# to provide redundancy there must be at least two replicas per site
|
||||
assert site_size >= 2
|
||||
# do not handle master other than the rest of the servers
|
||||
servers = [master] + replicas
|
||||
|
||||
# split servers into sites
|
||||
it = [iter(servers)] * site_size
|
||||
sites = [(x[0], x[1], x[2:]) for x in zip(*it)]
|
||||
num_sites = len(sites)
|
||||
|
||||
for i in range(num_sites):
|
||||
(a, b, _ignore) = sites[i]
|
||||
# create agreement inside the site
|
||||
yield a, b
|
||||
|
||||
# create agreement to one server in two next sites
|
||||
for (c, d, _ignore) in [sites[(i+n) % num_sites] for n in [1, 2]]:
|
||||
yield b, c
|
||||
|
||||
if site_size > 2:
|
||||
# deploy servers inside the site
|
||||
for site in sites:
|
||||
site_servers = list(site[2])
|
||||
yield site[0], site_servers[0]
|
||||
for edge in complete_topo(site_servers[0], site_servers[1:]):
|
||||
yield edge
|
||||
yield site[1], site_servers[-1]
|
||||
|
||||
|
||||
def install_topo(topo, master, replicas, clients, domain_level=None,
|
||||
def install_topo(topo, master, replicas, clients,
|
||||
skip_master=False, setup_replica_cas=True):
|
||||
"""Install IPA servers and clients in the given topology"""
|
||||
replicas = list(replicas)
|
||||
installed = {master}
|
||||
if not skip_master:
|
||||
install_master(master, domain_level=domain_level)
|
||||
install_master(master)
|
||||
|
||||
add_a_records_for_hosts_in_master_domain(master)
|
||||
|
||||
@@ -1016,7 +650,7 @@ def install_clients(servers, clients):
|
||||
def _entries_to_ldif(entries):
|
||||
"""Format LDAP entries as LDIF"""
|
||||
lines = []
|
||||
io = StringIO()
|
||||
io = StringIO.StringIO()
|
||||
writer = LDIFWriter(io)
|
||||
for entry in entries:
|
||||
writer.unparse(str(entry.dn), dict(entry))
|
||||
@@ -1061,21 +695,16 @@ def add_a_records_for_hosts_in_master_domain(master):
|
||||
for host in master.domain.hosts:
|
||||
# We don't need to take care of the zone creation since it is master
|
||||
# domain
|
||||
try:
|
||||
verify_host_resolvable(host.hostname, log)
|
||||
log.debug("The host (%s) is resolvable." % host.domain.name)
|
||||
except errors.DNSNotARecordError:
|
||||
log.debug("Hostname (%s) does not have A/AAAA record. Adding new one.",
|
||||
master.hostname)
|
||||
add_a_record(master, host)
|
||||
add_a_record(master, host)
|
||||
|
||||
|
||||
def add_a_record(master, host):
|
||||
# Find out if the record is already there
|
||||
cmd = master.run_command(['ipa',
|
||||
'dnsrecord-show',
|
||||
'dnsrecord-find',
|
||||
master.domain.name,
|
||||
host.hostname + "."],
|
||||
host.hostname,
|
||||
'--a-rec', host.ip],
|
||||
raiseonerr=False)
|
||||
|
||||
# If not, add it
|
||||
@@ -1083,99 +712,5 @@ def add_a_record(master, host):
|
||||
master.run_command(['ipa',
|
||||
'dnsrecord-add',
|
||||
master.domain.name,
|
||||
host.hostname + ".",
|
||||
host.hostname,
|
||||
'--a-rec', host.ip])
|
||||
|
||||
|
||||
def resolve_record(nameserver, query, rtype="SOA", retry=True, timeout=100):
|
||||
"""Resolve DNS record
|
||||
:retry: if resolution failed try again until timeout is reached
|
||||
:timeout: max period of time while method will try to resolve query
|
||||
(requires retry=True)
|
||||
"""
|
||||
res = dns.resolver.Resolver()
|
||||
res.nameservers = [nameserver]
|
||||
res.lifetime = 10 # wait max 10 seconds for reply
|
||||
|
||||
wait_until = time.time() + timeout
|
||||
|
||||
while time.time() < wait_until:
|
||||
try:
|
||||
ans = res.query(query, rtype)
|
||||
return ans
|
||||
except dns.exception.DNSException:
|
||||
if not retry:
|
||||
raise
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def ipa_backup(master):
|
||||
result = master.run_command(["ipa-backup"])
|
||||
path_re = re.compile("^Backed up to (?P<backup>.*)$", re.MULTILINE)
|
||||
matched = path_re.search(result.stdout_text + result.stderr_text)
|
||||
return matched.group("backup")
|
||||
|
||||
|
||||
def ipa_restore(master, backup_path):
|
||||
master.run_command(["ipa-restore", "-U",
|
||||
"-p", master.config.dirman_password,
|
||||
backup_path])
|
||||
|
||||
|
||||
def install_kra(host, domain_level=None, first_instance=False, raiseonerr=True):
|
||||
if domain_level is None:
|
||||
domain_level = domainlevel(host)
|
||||
command = ["ipa-kra-install", "-U", "-p", host.config.dirman_password]
|
||||
if domain_level == DOMAIN_LEVEL_0 and not first_instance:
|
||||
replica_file = get_replica_filename(host)
|
||||
command.append(replica_file)
|
||||
return host.run_command(command, raiseonerr=raiseonerr)
|
||||
|
||||
|
||||
def install_ca(host, domain_level=None, first_instance=False, raiseonerr=True):
|
||||
if domain_level is None:
|
||||
domain_level = domainlevel(host)
|
||||
command = ["ipa-ca-install", "-U", "-p", host.config.dirman_password,
|
||||
"-P", 'admin', "-w", host.config.admin_password]
|
||||
if domain_level == DOMAIN_LEVEL_0 and not first_instance:
|
||||
replica_file = get_replica_filename(host)
|
||||
command.append(replica_file)
|
||||
return host.run_command(command, raiseonerr=raiseonerr)
|
||||
|
||||
|
||||
def install_dns(host, raiseonerr=True):
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--forwarder", host.config.dns_forwarder,
|
||||
"-U",
|
||||
]
|
||||
return host.run_command(args, raiseonerr=raiseonerr)
|
||||
|
||||
|
||||
def uninstall_replica(master, replica):
|
||||
master.run_command(["ipa-replica-manage", "del", "--force",
|
||||
"-p", master.config.dirman_password,
|
||||
replica.hostname], raiseonerr=False)
|
||||
uninstall_master(replica)
|
||||
|
||||
|
||||
def replicas_cleanup(func):
|
||||
"""
|
||||
replicas_cleanup decorator, applied to any test method in integration tests
|
||||
uninstalls all replicas in the topology leaving only master
|
||||
configured
|
||||
"""
|
||||
def wrapped(*args):
|
||||
func(*args)
|
||||
for host in args[0].replicas:
|
||||
uninstall_replica(args[0].master, host)
|
||||
uninstall_client(host)
|
||||
result = args[0].master.run_command(
|
||||
["ipa", "host-del", "--updatedns", host.hostname],
|
||||
raiseonerr=False)
|
||||
# Workaround for 5627
|
||||
if "host not found" in result.stderr_text:
|
||||
args[0].master.run_command(["ipa",
|
||||
"host-del",
|
||||
host.hostname], raiseonerr=False)
|
||||
return wrapped
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# Authors:
|
||||
# Gabe Alford <redhatrises@gmail.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/>.
|
||||
|
||||
# FIXME: Pylint errors
|
||||
# pylint: disable=no-member
|
||||
|
||||
import re
|
||||
from ipatests.test_integration import tasks
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
|
||||
|
||||
def run_advice(master, advice_id, advice_regex, raiseerr):
|
||||
# Obtain the advice from the server
|
||||
tasks.kinit_admin(master)
|
||||
result = master.run_command(['ipa-advise', advice_id],
|
||||
raiseonerr=raiseerr)
|
||||
|
||||
if not result.stdout_text:
|
||||
advice = result.stderr_text
|
||||
else:
|
||||
advice = result.stdout_text
|
||||
|
||||
assert re.search(advice_regex, advice, re.S)
|
||||
|
||||
|
||||
class TestAdvice(IntegrationTest):
|
||||
"""
|
||||
Tests ipa-advise output.
|
||||
"""
|
||||
advice_id = None
|
||||
raiseerr = None
|
||||
advice_regex = ''
|
||||
topology = 'line'
|
||||
|
||||
def test_invalid_advice(self):
|
||||
advice_id = 'invalid-advise-param'
|
||||
advice_regex = "invalid[\s]+\'advice\'.*"
|
||||
raiseerr = False
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_FedoraAuthconfig(self):
|
||||
advice_id = 'config-fedora-authconfig'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"authconfig[\s]+\-\-enableldap[\s]+" \
|
||||
"\-\-ldapserver\=.*[\s]+\-\-enablerfc2307bis[\s]+" \
|
||||
"\-\-enablekrb5"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_FreeBSDNSSPAM(self):
|
||||
advice_id = 'config-freebsd-nss-pam-ldapd'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"pkg_add[\s]+\-r[\s]+nss\-pam\-ldapd[\s]+curl.*" \
|
||||
"\/usr\/local\/etc\/rc\.d\/nslcd[\s]+restart"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_GenericNSSPAM(self):
|
||||
advice_id = 'config-generic-linux-nss-pam-ldapd'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"apt\-get[\s]+\-y[\s]+install[\s]+curl[\s]+openssl[\s]+" \
|
||||
"libnss\-ldapd[\s]+libpam\-ldapd[\s]+nslcd.*" \
|
||||
"service[\s]+nscd[\s]+stop[\s]+\&\&[\s]+service[\s]+" \
|
||||
"nslcd[\s]+restart"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_GenericSSSDBefore19(self):
|
||||
advice_id = 'config-generic-linux-sssd-before-1-9'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"apt\-get[\s]+\-y[\s]+install sssd curl openssl.*" \
|
||||
"service[\s]+sssd[\s]+start"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_RedHatNSS(self):
|
||||
advice_id = 'config-redhat-nss-ldap'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"yum[\s]+install[\s]+\-y[\s]+curl[\s]+openssl[\s]+nss_ldap" \
|
||||
"[\s]+authconfig.*authconfig[\s]+\-\-updateall" \
|
||||
"[\s]+\-\-enableldap[\s]+\-\-enableldaptls"\
|
||||
"[\s]+\-\-enableldapauth[\s]+" \
|
||||
"\-\-ldapserver=.*[\s]+\-\-ldapbasedn=.*"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_RedHatNSSPAM(self):
|
||||
advice_id = 'config-redhat-nss-pam-ldapd'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"yum[\s]+install[\s]+\-y[\s]+curl[\s]+openssl[\s]+" \
|
||||
"nss\-pam\-ldapd[\s]+pam_ldap[\s]+authconfig.*" \
|
||||
"authconfig[\s]+\-\-updateall[\s]+\-\-enableldap"\
|
||||
"[\s]+\-\-enableldaptls[\s]+\-\-enableldapauth[\s]+" \
|
||||
"\-\-ldapserver=.*[\s]+\-\-ldapbasedn=.*"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
|
||||
|
||||
def test_advice_RedHatSSSDBefore19(self):
|
||||
advice_id = 'config-redhat-sssd-before-1-9'
|
||||
advice_regex = "\#\!\/bin\/sh.*" \
|
||||
"yum[\s]+install[\s]+\-y[\s]+sssd[\s]+authconfig[\s]+" \
|
||||
"curl[\s]+openssl.*service[\s]+sssd[\s]+start"
|
||||
raiseerr = True
|
||||
|
||||
run_advice(self.master, advice_id, advice_regex, raiseerr)
|
||||
@@ -1,414 +0,0 @@
|
||||
# Authors:
|
||||
# Petr Viktorin <pviktori@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2014 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
from ipaplatform.constants import constants
|
||||
from ipapython.ipa_log_manager import log_mgr
|
||||
from ipapython.dn import DN
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
from ipatests.test_integration.test_dnssec import wait_until_record_is_signed
|
||||
from ipatests.util import assert_deepequal
|
||||
|
||||
log = log_mgr.get_logger(__name__)
|
||||
|
||||
|
||||
def assert_entries_equal(a, b):
|
||||
assert_deepequal(a.dn, b.dn)
|
||||
assert_deepequal(dict(a), dict(b))
|
||||
|
||||
|
||||
def assert_results_equal(a, b):
|
||||
def to_dict(r):
|
||||
return {
|
||||
'stdout': r.stdout_text,
|
||||
'stderr': r.stderr_text,
|
||||
'returncode': r.returncode,
|
||||
}
|
||||
assert_deepequal(to_dict(a), to_dict(b))
|
||||
|
||||
|
||||
def check_admin_in_ldap(host):
|
||||
ldap = host.ldap_connect()
|
||||
basedn = host.domain.basedn
|
||||
user_dn = DN(('uid', 'admin'), ('cn', 'users'), ('cn', 'accounts'), basedn)
|
||||
entry = ldap.get_entry(user_dn)
|
||||
print(entry)
|
||||
assert entry.dn == user_dn
|
||||
assert entry['uid'] == ['admin']
|
||||
|
||||
del entry['krbLastSuccessfulAuth']
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def check_admin_in_cli(host):
|
||||
result = host.run_command(['ipa', 'user-show', 'admin'])
|
||||
assert 'User login: admin' in result.stdout_text, result.stdout_text
|
||||
return result
|
||||
|
||||
|
||||
def check_admin_in_id(host):
|
||||
result = host.run_command(['id', 'admin'])
|
||||
assert 'admin' in result.stdout_text, result.stdout_text
|
||||
return result
|
||||
|
||||
|
||||
def check_certs(host):
|
||||
result = host.run_command(['ipa', 'cert-find'])
|
||||
assert re.search('^Number of entries returned [1-9]\d*$',
|
||||
result.stdout_text, re.MULTILINE), result.stdout_text
|
||||
return result
|
||||
|
||||
|
||||
def check_dns(host):
|
||||
result = host.run_command(['host', host.hostname, 'localhost'])
|
||||
return result
|
||||
|
||||
|
||||
def check_kinit(host):
|
||||
result = host.run_command(['kinit', 'admin'],
|
||||
stdin_text=host.config.admin_password)
|
||||
return result
|
||||
|
||||
|
||||
CHECKS = [
|
||||
(check_admin_in_ldap, assert_entries_equal),
|
||||
(check_admin_in_cli, assert_results_equal),
|
||||
(check_admin_in_id, assert_results_equal),
|
||||
(check_certs, assert_results_equal),
|
||||
(check_dns, assert_results_equal),
|
||||
(check_kinit, assert_results_equal),
|
||||
]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def restore_checker(host):
|
||||
"""Check that the IPA at host works the same at context enter and exit"""
|
||||
tasks.kinit_admin(host)
|
||||
|
||||
results = []
|
||||
for check, assert_func in CHECKS:
|
||||
log.info('Storing result for %s', check)
|
||||
results.append(check(host))
|
||||
|
||||
yield
|
||||
|
||||
for (check, assert_func), expected in zip(CHECKS, results):
|
||||
log.info('Checking result for %s', check)
|
||||
got = check(host)
|
||||
assert_func(expected, got)
|
||||
|
||||
|
||||
def backup(host):
|
||||
"""Run backup on host, return the path to the backup directory"""
|
||||
result = host.run_command(['ipa-backup', '-v'])
|
||||
|
||||
# Get the backup location from the command's output
|
||||
for line in result.stderr_text.splitlines():
|
||||
prefix = ('ipa.ipaserver.install.ipa_backup.Backup: '
|
||||
'INFO: Backed up to ')
|
||||
if line.startswith(prefix):
|
||||
backup_path = line[len(prefix):].strip()
|
||||
log.info('Backup path for %s is %s', host, backup_path)
|
||||
return backup_path
|
||||
else:
|
||||
raise AssertionError('Backup directory not found in output')
|
||||
|
||||
|
||||
|
||||
class TestBackupAndRestore(IntegrationTest):
|
||||
topology = 'star'
|
||||
|
||||
def test_full_backup_and_restore(self):
|
||||
"""backup, uninstall, restore"""
|
||||
with restore_checker(self.master):
|
||||
backup_path = backup(self.master)
|
||||
|
||||
self.master.run_command(['ipa-server-install',
|
||||
'--uninstall',
|
||||
'-U'])
|
||||
|
||||
dirman_password = self.master.config.dirman_password
|
||||
self.master.run_command(['ipa-restore', backup_path],
|
||||
stdin_text=dirman_password + '\nyes')
|
||||
|
||||
def test_full_backup_and_restore_with_removed_users(self):
|
||||
"""regression test for https://fedorahosted.org/freeipa/ticket/3866"""
|
||||
with restore_checker(self.master):
|
||||
backup_path = backup(self.master)
|
||||
|
||||
self.log.info('Backup path for %s is %s', self.master, backup_path)
|
||||
|
||||
self.master.run_command(['ipa-server-install',
|
||||
'--uninstall',
|
||||
'-U'])
|
||||
|
||||
self.master.run_command(['userdel', constants.DS_USER])
|
||||
self.master.run_command(['userdel', constants.PKI_USER])
|
||||
|
||||
homedir = os.path.join(self.master.config.test_dir,
|
||||
'testuser_homedir')
|
||||
self.master.run_command(['useradd', 'ipatest_user1',
|
||||
'--system',
|
||||
'-d', homedir])
|
||||
try:
|
||||
dirman_password = self.master.config.dirman_password
|
||||
self.master.run_command(['ipa-restore', backup_path],
|
||||
stdin_text=dirman_password + '\nyes')
|
||||
finally:
|
||||
self.master.run_command(['userdel', 'ipatest_user1'])
|
||||
|
||||
def test_full_backup_and_restore_with_selinux_booleans_off(self):
|
||||
"""regression test for https://fedorahosted.org/freeipa/ticket/4157"""
|
||||
with restore_checker(self.master):
|
||||
backup_path = backup(self.master)
|
||||
|
||||
self.log.info('Backup path for %s is %s', self.master, backup_path)
|
||||
|
||||
self.master.run_command(['ipa-server-install',
|
||||
'--uninstall',
|
||||
'-U'])
|
||||
|
||||
self.master.run_command([
|
||||
'setsebool', '-P',
|
||||
'httpd_can_network_connect=off',
|
||||
'httpd_manage_ipa=off',
|
||||
])
|
||||
|
||||
dirman_password = self.master.config.dirman_password
|
||||
self.master.run_command(['ipa-restore', backup_path],
|
||||
stdin_text=dirman_password + '\nyes')
|
||||
|
||||
result = self.master.run_command([
|
||||
'getsebool',
|
||||
'httpd_can_network_connect',
|
||||
'httpd_manage_ipa',
|
||||
])
|
||||
assert 'httpd_can_network_connect --> on' in result.stdout_text
|
||||
assert 'httpd_manage_ipa --> on' in result.stdout_text
|
||||
|
||||
|
||||
class BaseBackupAndRestoreWithDNS(IntegrationTest):
|
||||
"""
|
||||
Abstract class for DNS restore tests
|
||||
"""
|
||||
topology = 'star'
|
||||
|
||||
example_test_zone = "example.test."
|
||||
example2_test_zone = "example2.test."
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
|
||||
def _full_backup_restore_with_DNS_zone(self, reinstall=False):
|
||||
"""backup, uninstall, restore"""
|
||||
with restore_checker(self.master):
|
||||
|
||||
self.master.run_command([
|
||||
'ipa', 'dnszone-add',
|
||||
self.example_test_zone,
|
||||
])
|
||||
|
||||
tasks.resolve_record(self.master.ip, self.example_test_zone)
|
||||
|
||||
backup_path = backup(self.master)
|
||||
|
||||
self.master.run_command(['ipa-server-install',
|
||||
'--uninstall',
|
||||
'-U'])
|
||||
|
||||
if reinstall:
|
||||
tasks.install_master(self.master, setup_dns=True)
|
||||
|
||||
dirman_password = self.master.config.dirman_password
|
||||
self.master.run_command(['ipa-restore', backup_path],
|
||||
stdin_text=dirman_password + '\nyes')
|
||||
|
||||
tasks.resolve_record(self.master.ip, self.example_test_zone)
|
||||
|
||||
tasks.kinit_admin(self.master)
|
||||
self.master.run_command([
|
||||
'ipa', 'dnszone-add',
|
||||
self.example2_test_zone,
|
||||
])
|
||||
|
||||
tasks.resolve_record(self.master.ip, self.example2_test_zone)
|
||||
|
||||
|
||||
class TestBackupAndRestoreWithDNS(BaseBackupAndRestoreWithDNS):
|
||||
def test_full_backup_and_restore_with_DNS_zone(self):
|
||||
"""backup, uninstall, restore"""
|
||||
self._full_backup_restore_with_DNS_zone(reinstall=False)
|
||||
|
||||
|
||||
class TestBackupReinstallRestoreWithDNS(BaseBackupAndRestoreWithDNS):
|
||||
def test_full_backup_reinstall_restore_with_DNS_zone(self):
|
||||
"""backup, uninstall, reinstall, restore"""
|
||||
self._full_backup_restore_with_DNS_zone(reinstall=True)
|
||||
|
||||
|
||||
class BaseBackupAndRestoreWithDNSSEC(IntegrationTest):
|
||||
"""
|
||||
Abstract class for DNSSEC restore tests
|
||||
"""
|
||||
topology = 'star'
|
||||
|
||||
example_test_zone = "example.test."
|
||||
example2_test_zone = "example2.test."
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--dnssec-master",
|
||||
"--forwarder", cls.master.config.dns_forwarder,
|
||||
"-U",
|
||||
]
|
||||
cls.master.run_command(args)
|
||||
|
||||
def _full_backup_and_restore_with_DNSSEC_zone(self, reinstall=False):
|
||||
with restore_checker(self.master):
|
||||
|
||||
self.master.run_command([
|
||||
'ipa', 'dnszone-add',
|
||||
self.example_test_zone,
|
||||
'--dnssec', 'true',
|
||||
])
|
||||
|
||||
assert wait_until_record_is_signed(self.master.ip,
|
||||
self.example_test_zone, self.log), "Zone is not signed"
|
||||
|
||||
backup_path = backup(self.master)
|
||||
|
||||
self.master.run_command(['ipa-server-install',
|
||||
'--uninstall',
|
||||
'-U'])
|
||||
|
||||
if reinstall:
|
||||
tasks.install_master(self.master, setup_dns=True)
|
||||
|
||||
dirman_password = self.master.config.dirman_password
|
||||
self.master.run_command(['ipa-restore', backup_path],
|
||||
stdin_text=dirman_password + '\nyes')
|
||||
|
||||
assert wait_until_record_is_signed(self.master.ip,
|
||||
self.example_test_zone, self.log), ("Zone is not signed after "
|
||||
"restore")
|
||||
|
||||
tasks.kinit_admin(self.master)
|
||||
self.master.run_command([
|
||||
'ipa', 'dnszone-add',
|
||||
self.example2_test_zone,
|
||||
'--dnssec', 'true',
|
||||
])
|
||||
|
||||
assert wait_until_record_is_signed(self.master.ip,
|
||||
self.example2_test_zone, self.log), "A new zone is not signed"
|
||||
|
||||
|
||||
class TestBackupAndRestoreWithDNSSEC(BaseBackupAndRestoreWithDNSSEC):
|
||||
def test_full_backup_and_restore_with_DNSSEC_zone(self):
|
||||
"""backup, uninstall, restore"""
|
||||
self._full_backup_and_restore_with_DNSSEC_zone(reinstall=False)
|
||||
|
||||
|
||||
class TestBackupReinstallRestoreWithDNSSEC(BaseBackupAndRestoreWithDNSSEC):
|
||||
def test_full_backup_reinstall_restore_with_DNSSEC_zone(self):
|
||||
"""backup, uninstall, install, restore"""
|
||||
self._full_backup_and_restore_with_DNSSEC_zone(reinstall=True)
|
||||
|
||||
|
||||
class BaseBackupAndRestoreWithKRA(IntegrationTest):
|
||||
"""
|
||||
Abstract class for KRA restore tests
|
||||
"""
|
||||
topology = 'star'
|
||||
|
||||
vault_name = "ci_test_vault"
|
||||
vault_password = "password"
|
||||
vault_data = "SSBsb3ZlIENJIHRlc3RzCg=="
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True, setup_kra=True)
|
||||
|
||||
def _full_backup_restore_with_vault(self, reinstall=False):
|
||||
with restore_checker(self.master):
|
||||
# create vault
|
||||
self.master.run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.master.run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
|
||||
# retrieve secret
|
||||
self.master.run_command([
|
||||
"ipa", "vault-retrieve",
|
||||
self.vault_name,
|
||||
"--password", self.vault_password,
|
||||
])
|
||||
|
||||
backup_path = backup(self.master)
|
||||
|
||||
self.master.run_command(['ipa-server-install',
|
||||
'--uninstall',
|
||||
'-U'])
|
||||
|
||||
if reinstall:
|
||||
tasks.install_master(self.master, setup_dns=True)
|
||||
|
||||
dirman_password = self.master.config.dirman_password
|
||||
self.master.run_command(['ipa-restore', backup_path],
|
||||
stdin_text=dirman_password + '\nyes')
|
||||
|
||||
tasks.kinit_admin(self.master)
|
||||
# retrieve secret after restore
|
||||
self.master.run_command([
|
||||
"ipa", "vault-retrieve",
|
||||
self.vault_name,
|
||||
"--password", self.vault_password,
|
||||
])
|
||||
|
||||
|
||||
class TestBackupAndRestoreWithKRA(BaseBackupAndRestoreWithKRA):
|
||||
def test_full_backup_restore_with_vault(self):
|
||||
"""backup, uninstall, restore"""
|
||||
self._full_backup_restore_with_vault(reinstall=False)
|
||||
|
||||
class TestBackupReinstallRestoreWithKRA(BaseBackupAndRestoreWithKRA):
|
||||
def test_full_backup_reinstall_restore_with_vault(self):
|
||||
"""backup, uninstall, reinstall, restore"""
|
||||
self._full_backup_restore_with_vault(reinstall=True)
|
||||
@@ -24,7 +24,6 @@ import base64
|
||||
import glob
|
||||
import contextlib
|
||||
import nose
|
||||
import pytest
|
||||
|
||||
from ipalib import x509
|
||||
from ipapython import ipautil
|
||||
@@ -32,6 +31,7 @@ from ipaplatform.paths import paths
|
||||
from ipapython.dn import DN
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
from ipatests.order_plugin import ordered
|
||||
|
||||
_DEFAULT = object()
|
||||
|
||||
@@ -67,8 +67,8 @@ def assert_error(result, stderr_text, returncode=None):
|
||||
|
||||
class CALessBase(IntegrationTest):
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
super(CALessBase, cls).install(mh)
|
||||
def install(cls):
|
||||
super(CALessBase, cls).install()
|
||||
cls.cert_dir = tempfile.mkdtemp(prefix="ipatest-")
|
||||
cls.pem_filename = os.path.join(cls.cert_dir, 'root.pem')
|
||||
scriptfile = os.path.join(os.path.dirname(__file__),
|
||||
@@ -108,11 +108,17 @@ class CALessBase(IntegrationTest):
|
||||
host.transport.put_file(source, dest)
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls, mh):
|
||||
def uninstall(cls):
|
||||
# Remove the NSS database
|
||||
shutil.rmtree(cls.cert_dir)
|
||||
|
||||
super(CALessBase, cls).uninstall(mh)
|
||||
# Remove CA cert in /etc/pki/nssdb, in case of failed (un)install
|
||||
for host in cls.get_all_hosts():
|
||||
cls.master.run_command(['certutil', '-d', paths.NSS_DB_DIR, '-D',
|
||||
'-n', 'External CA cert'],
|
||||
raiseonerr=False)
|
||||
|
||||
super(CALessBase, cls).uninstall()
|
||||
|
||||
@classmethod
|
||||
def install_server(cls, host=None,
|
||||
@@ -140,17 +146,17 @@ class CALessBase(IntegrationTest):
|
||||
for filename in set(files_to_copy):
|
||||
cls.copy_cert(host, filename)
|
||||
|
||||
host.collect_log(paths.IPASERVER_INSTALL_LOG)
|
||||
host.collect_log(paths.IPACLIENT_INSTALL_LOG)
|
||||
cls.collect_log(host, paths.IPASERVER_INSTALL_LOG)
|
||||
cls.collect_log(host, paths.IPACLIENT_INSTALL_LOG)
|
||||
inst = host.domain.realm.replace('.', '-')
|
||||
host.collect_log(paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst)
|
||||
host.collect_log(paths.SLAPD_INSTANCE_ACCESS_LOG_TEMPLATE % inst)
|
||||
cls.collect_log(host, paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst)
|
||||
cls.collect_log(host, paths.SLAPD_INSTANCE_ACCESS_LOG_TEMPLATE % inst)
|
||||
|
||||
args = [
|
||||
'ipa-server-install',
|
||||
'--http-cert-file', http_pkcs12,
|
||||
'--dirsrv-cert-file', dirsrv_pkcs12,
|
||||
'--ca-cert-file', root_ca_file,
|
||||
'--http_pkcs12', http_pkcs12,
|
||||
'--dirsrv_pkcs12', dirsrv_pkcs12,
|
||||
'--root-ca-file', root_ca_file,
|
||||
'--ip-address', host.ip,
|
||||
'-r', host.domain.name,
|
||||
'-p', host.config.dirman_password,
|
||||
@@ -160,9 +166,9 @@ class CALessBase(IntegrationTest):
|
||||
]
|
||||
|
||||
if http_pin is not None:
|
||||
args.extend(['--http-pin', http_pin])
|
||||
args.extend(['--http_pin', http_pin])
|
||||
if dirsrv_pin is not None:
|
||||
args.extend(['--dirsrv-pin', dirsrv_pin])
|
||||
args.extend(['--dirsrv_pin', dirsrv_pin])
|
||||
if unattended:
|
||||
args.extend(['-U'])
|
||||
|
||||
@@ -211,11 +217,11 @@ class CALessBase(IntegrationTest):
|
||||
os.path.join(self.cert_dir, filename),
|
||||
os.path.join(master.config.test_dir, filename))
|
||||
|
||||
replica.collect_log(paths.IPAREPLICA_INSTALL_LOG)
|
||||
replica.collect_log(paths.IPACLIENT_INSTALL_LOG)
|
||||
self.collect_log(replica, paths.IPAREPLICA_INSTALL_LOG)
|
||||
self.collect_log(replica, paths.IPACLIENT_INSTALL_LOG)
|
||||
inst = replica.domain.realm.replace('.', '-')
|
||||
replica.collect_log(paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst)
|
||||
replica.collect_log(paths.SLAPD_INSTANCE_ACCESS_LOG_TEMPLATE % inst)
|
||||
self.collect_log(replica, paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst)
|
||||
self.collect_log(replica, paths.SLAPD_INSTANCE_ACCESS_LOG_TEMPLATE % inst)
|
||||
|
||||
args = [
|
||||
'ipa-replica-prepare',
|
||||
@@ -224,13 +230,13 @@ class CALessBase(IntegrationTest):
|
||||
]
|
||||
|
||||
if http_pkcs12:
|
||||
args.extend(['--http-cert-file', http_pkcs12])
|
||||
args.extend(['--http_pkcs12', http_pkcs12])
|
||||
if dirsrv_pkcs12:
|
||||
args.extend(['--dirsrv-cert-file', dirsrv_pkcs12])
|
||||
args.extend(['--dirsrv_pkcs12', dirsrv_pkcs12])
|
||||
if http_pin is not None:
|
||||
args.extend(['--http-pin', http_pin])
|
||||
args.extend(['--http_pin', http_pin])
|
||||
if dirsrv_pin is not None:
|
||||
args.extend(['--dirsrv-pin', dirsrv_pin])
|
||||
args.extend(['--dirsrv_pin', dirsrv_pin])
|
||||
|
||||
args.extend([replica.hostname])
|
||||
|
||||
@@ -337,6 +343,12 @@ class TestServerInstall(CALessBase):
|
||||
def tearDown(self):
|
||||
self.uninstall_server()
|
||||
|
||||
# Remove CA cert in /etc/pki/nssdb, in case of failed (un)install
|
||||
for host in self.get_all_hosts():
|
||||
self.master.run_command(['certutil', '-d', paths.NSS_DB_DIR, '-D',
|
||||
'-n', 'External CA cert'],
|
||||
raiseonerr=False)
|
||||
|
||||
def test_nonexistent_ca_pem_file(self):
|
||||
"IPA server install with non-existent CA PEM file "
|
||||
|
||||
@@ -416,8 +428,8 @@ class TestServerInstall(CALessBase):
|
||||
|
||||
result = self.install_server(http_pin=None)
|
||||
assert_error(result,
|
||||
'ipa-server-install: error: You must specify --http-pin '
|
||||
'with --http-cert-file')
|
||||
'ipa-server-install: error: You must specify --http_pin '
|
||||
'with --http_pkcs12')
|
||||
|
||||
def test_missing_ds_password(self):
|
||||
"IPA server install with missing DS PKCS#12 password (unattended)"
|
||||
@@ -429,7 +441,7 @@ class TestServerInstall(CALessBase):
|
||||
result = self.install_server(dirsrv_pin=None)
|
||||
assert_error(result,
|
||||
'ipa-server-install: error: You must specify '
|
||||
'--dirsrv-pin with --dirsrv-cert-file')
|
||||
'--dirsrv_pin with --dirsrv_pkcs12')
|
||||
|
||||
def test_incorect_http_pin(self):
|
||||
"IPA server install with incorrect HTTP PKCS#12 password"
|
||||
@@ -757,7 +769,12 @@ class TestReplicaInstall(CALessBase):
|
||||
self.master.run_command(['ipa', 'host-del', replica.hostname],
|
||||
raiseonerr=False)
|
||||
|
||||
replica.run_command(['certutil', '-d', paths.NSS_DB_DIR, '-D',
|
||||
'-n', 'External CA cert'], raiseonerr=False)
|
||||
|
||||
self.uninstall_server()
|
||||
self.master.run_command(['certutil', '-d', paths.NSS_DB_DIR, '-D',
|
||||
'-n', 'External CA cert'], raiseonerr=False)
|
||||
|
||||
def test_no_certs(self):
|
||||
"IPA replica install without certificates"
|
||||
@@ -767,9 +784,8 @@ class TestReplicaInstall(CALessBase):
|
||||
raiseonerr=False)
|
||||
assert result.returncode > 0
|
||||
assert ('Cannot issue certificates: a CA is not installed. Use the '
|
||||
'--http-cert-file, --dirsrv-cert-file options to provide '
|
||||
'custom certificates.' in result.stderr_text), \
|
||||
result.stderr_text
|
||||
'--http_pkcs12, --dirsrv_pkcs12 options to provide custom '
|
||||
'certificates.' in result.stderr_text), result.stderr_text
|
||||
|
||||
def test_nonexistent_http_pkcs12_file(self):
|
||||
"IPA replica install with non-existent HTTP PKCS#12 file"
|
||||
@@ -1126,10 +1142,11 @@ class TestClientInstall(CALessBase):
|
||||
self.verify_installation()
|
||||
|
||||
|
||||
@ordered
|
||||
class TestIPACommands(CALessBase):
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
super(TestIPACommands, cls).install(mh)
|
||||
def install(cls):
|
||||
super(TestIPACommands, cls).install()
|
||||
|
||||
cls.export_pkcs12('ca1/server')
|
||||
with open(cls.pem_filename, 'w') as f:
|
||||
@@ -1151,16 +1168,18 @@ class TestIPACommands(CALessBase):
|
||||
result = self.master.run_command(['ipa', command], raiseonerr=False)
|
||||
assert_error(result, "ipa: ERROR: unknown command '%s'" % command)
|
||||
|
||||
@pytest.mark.parametrize('command', (
|
||||
'cert-status',
|
||||
'cert-show',
|
||||
'cert-find',
|
||||
'cert-revoke',
|
||||
'cert-remove-hold',
|
||||
'cert-status'))
|
||||
def test_cert_commands_unavailable(self, command):
|
||||
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"
|
||||
@@ -1228,7 +1247,7 @@ class TestIPACommands(CALessBase):
|
||||
|
||||
class TestCertinstall(CALessBase):
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
def install(cls):
|
||||
super(TestCertinstall, cls).install()
|
||||
|
||||
cls.export_pkcs12('ca1/server')
|
||||
@@ -1460,7 +1479,7 @@ class TestCertinstall(CALessBase):
|
||||
|
||||
args = ['ipa-server-certinstall',
|
||||
'-w', 'server.p12',
|
||||
'--http-pin', self.cert_password]
|
||||
'--http_pin', self.cert_password]
|
||||
|
||||
result = self.certinstall('w', 'ca1/server', args=args)
|
||||
assert result.returncode == 0
|
||||
@@ -1471,7 +1490,7 @@ class TestCertinstall(CALessBase):
|
||||
|
||||
args = ['ipa-server-certinstall',
|
||||
'-d', 'server.p12',
|
||||
'--dirsrv-pin', self.cert_password]
|
||||
'--dirsrv_pin', self.cert_password]
|
||||
stdin_text = self.master.config.dirman_password + '\n'
|
||||
|
||||
result = self.certinstall('d', 'ca1/server',
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
|
||||
DIRSRV_CONFIG_MODS = """
|
||||
# https://fedorahosted.org/freeipa/ticket/4949
|
||||
dn: cn=config,cn=ldbm database,cn=plugins,cn=config
|
||||
changetype: modify
|
||||
replace: nsslapd-db-locks
|
||||
nsslapd-db-locks: 100000
|
||||
|
||||
# https://fedorahosted.org/freeipa/ticket/1930
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
replace: nsslapd-allow-unauthenticated-binds
|
||||
nsslapd-allow-unauthenticated-binds: off
|
||||
-
|
||||
replace: nsslapd-require-secure-binds
|
||||
nsslapd-require-secure-binds: off
|
||||
-
|
||||
replace: nsslapd-allow-anonymous-access
|
||||
nsslapd-allow-anonymous-access: off
|
||||
-
|
||||
replace: nsslapd-minssf
|
||||
nsslapd-minssf: 0
|
||||
|
||||
# https://fedorahosted.org/freeipa/ticket/4048
|
||||
dn: cn=config
|
||||
changetype: modify
|
||||
replace: nssslapd-maxbersize
|
||||
nssslapd-maxbersize: 209715201
|
||||
|
||||
dn: cn=userRoot,cn=ldbm database,cn=plugins,cn=config
|
||||
changetype: modify
|
||||
replace: nsslapd-cachememsize
|
||||
nsslapd-cachememsize: 10485761
|
||||
|
||||
dn: cn=config,cn=ldbm database,cn=plugins,cn=config
|
||||
changetype: modify
|
||||
replace: nsslapd-import_cachesize
|
||||
nsslapd-import_cachesize: 20000001
|
||||
-
|
||||
replace: nsslapd-dbcachesize
|
||||
nsslapd-dbcachesize: 10000001
|
||||
"""
|
||||
|
||||
CONFIG_LDIF_PATH = "/root/dirsrv-config-mod.ldif"
|
||||
|
||||
|
||||
class TestCustomInstallMaster(IntegrationTest):
|
||||
"""
|
||||
Install master with customized DS config
|
||||
"""
|
||||
topology = 'star'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
# just prepare LDIF file on both master and replica
|
||||
cls.master.put_file_contents(CONFIG_LDIF_PATH, DIRSRV_CONFIG_MODS)
|
||||
|
||||
def test_customized_ds_install_master(self):
|
||||
tasks.install_master(self.master, setup_dns=False, extra_args=[
|
||||
'--dirsrv-config-file', CONFIG_LDIF_PATH
|
||||
])
|
||||
|
||||
|
||||
class TestCustomInstallReplica(IntegrationTest):
|
||||
"""
|
||||
Install replica with customized DS config
|
||||
"""
|
||||
topology = 'star'
|
||||
num_replicas = 1
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
# just prepare LDIF file on both master and replica
|
||||
cls.replicas[0].put_file_contents(CONFIG_LDIF_PATH, DIRSRV_CONFIG_MODS)
|
||||
tasks.install_master(cls.master)
|
||||
|
||||
def test_customized_ds_install_replica(self):
|
||||
tasks.install_replica(
|
||||
self.master, self.replicas[0], setup_ca=False,
|
||||
extra_args=['--dirsrv-config-file', CONFIG_LDIF_PATH])
|
||||
@@ -1,563 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import dns.dnssec
|
||||
import dns.resolver
|
||||
import dns.name
|
||||
import time
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
test_zone = "dnssec.test."
|
||||
test_zone_repl = "dnssec-replica.test."
|
||||
root_zone = "."
|
||||
example_test_zone = "example.test."
|
||||
example2_test_zone = "example2.test."
|
||||
example3_test_zone = "example3.test."
|
||||
|
||||
|
||||
def resolve_with_dnssec(nameserver, query, log, rtype="SOA"):
|
||||
res = dns.resolver.Resolver()
|
||||
res.nameservers = [nameserver]
|
||||
res.lifetime = 10 # wait max 10 seconds for reply
|
||||
# enable Authenticated Data + Checking Disabled flags
|
||||
res.set_flags(dns.flags.AD | dns.flags.CD)
|
||||
|
||||
# enable EDNS v0 + enable DNSSEC-Ok flag
|
||||
res.use_edns(0, dns.flags.DO, 0)
|
||||
|
||||
ans = res.query(query, rtype)
|
||||
return ans
|
||||
|
||||
def get_RRSIG_record(nameserver, query, log, rtype="SOA"):
|
||||
ans = resolve_with_dnssec(nameserver, query, log, rtype=rtype)
|
||||
return ans.response.find_rrset(
|
||||
ans.response.answer, dns.name.from_text(query),
|
||||
dns.rdataclass.IN, dns.rdatatype.RRSIG,
|
||||
dns.rdatatype.from_text(rtype))
|
||||
|
||||
|
||||
def is_record_signed(nameserver, query, log, rtype="SOA"):
|
||||
try:
|
||||
get_RRSIG_record(nameserver, query, log, rtype=rtype)
|
||||
except KeyError:
|
||||
return False
|
||||
except dns.exception.DNSException:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def wait_until_record_is_signed(nameserver, record, log, rtype="SOA",
|
||||
timeout=100):
|
||||
"""
|
||||
Returns True if record is signed, or False on timeout
|
||||
:param nameserver: nameserver to query
|
||||
:param record: query
|
||||
:param log: logger
|
||||
:param rtype: record type
|
||||
:param timeout:
|
||||
:return: True if records is signed, False if timeout
|
||||
"""
|
||||
log.info("Waiting for signed %s record of %s from server %s (timeout %s "
|
||||
"sec)", rtype, record, nameserver, timeout)
|
||||
wait_until = time.time() + timeout
|
||||
while time.time() < wait_until:
|
||||
if is_record_signed(nameserver, record, log, rtype=rtype):
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
|
||||
class TestInstallDNSSECLast(IntegrationTest):
|
||||
"""Simple DNSSEC test
|
||||
|
||||
Install a server and a replica with DNS, then reinstall server
|
||||
as DNSSEC master
|
||||
"""
|
||||
num_replicas = 1
|
||||
topology = 'star'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
tasks.install_replica(cls.master, cls.replicas[0], setup_dns=True)
|
||||
|
||||
def test_install_dnssec_master(self):
|
||||
"""Both master and replica have DNS installed"""
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--dnssec-master",
|
||||
"--forwarder", self.master.config.dns_forwarder,
|
||||
"-U",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
def test_if_zone_is_signed_master(self):
|
||||
# add zone with enabled DNSSEC signing on master
|
||||
args = [
|
||||
"ipa",
|
||||
"dnszone-add", test_zone,
|
||||
"--skip-overlap-check",
|
||||
"--dnssec", "true",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
# test master
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, test_zone, self.log, timeout=100
|
||||
), "Zone %s is not signed (master)" % test_zone
|
||||
|
||||
# test replica
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, test_zone, self.log, timeout=200
|
||||
), "DNS zone %s is not signed (replica)" % test_zone
|
||||
|
||||
def test_if_zone_is_signed_replica(self):
|
||||
# add zone with enabled DNSSEC signing on replica
|
||||
args = [
|
||||
"ipa",
|
||||
"dnszone-add", test_zone_repl,
|
||||
"--skip-overlap-check",
|
||||
"--dnssec", "true",
|
||||
]
|
||||
self.replicas[0].run_command(args)
|
||||
|
||||
# test replica
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, test_zone_repl, self.log, timeout=300
|
||||
), "Zone %s is not signed (replica)" % test_zone_repl
|
||||
|
||||
# we do not need to wait, on master zones should be singed faster
|
||||
# than on replicas
|
||||
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, test_zone_repl, self.log, timeout=5
|
||||
), "DNS zone %s is not signed (master)" % test_zone
|
||||
|
||||
def test_disable_reenable_signing_master(self):
|
||||
|
||||
dnskey_old = resolve_with_dnssec(self.master.ip, test_zone,
|
||||
self.log, rtype="DNSKEY").rrset
|
||||
|
||||
# disable DNSSEC signing of zone on master
|
||||
args = [
|
||||
"ipa",
|
||||
"dnszone-mod", test_zone,
|
||||
"--dnssec", "false",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
time.sleep(20) # sleep a bit until LDAP changes are applied to DNS
|
||||
|
||||
# test master
|
||||
assert not is_record_signed(
|
||||
self.master.ip, test_zone, self.log
|
||||
), "Zone %s is still signed (master)" % test_zone
|
||||
|
||||
# test replica
|
||||
assert not is_record_signed(
|
||||
self.replicas[0].ip, test_zone, self.log
|
||||
), "DNS zone %s is still signed (replica)" % test_zone
|
||||
|
||||
# reenable DNSSEC signing
|
||||
args = [
|
||||
"ipa",
|
||||
"dnszone-mod", test_zone,
|
||||
"--dnssec", "true",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
time.sleep(20) # sleep a bit until LDAP changes are applied to DNS
|
||||
|
||||
# test master
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, test_zone, self.log, timeout=100
|
||||
), "Zone %s is not signed (master)" % test_zone
|
||||
|
||||
# test replica
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, test_zone, self.log, timeout=200
|
||||
), "DNS zone %s is not signed (replica)" % test_zone
|
||||
|
||||
dnskey_new = resolve_with_dnssec(self.master.ip, test_zone,
|
||||
self.log, rtype="DNSKEY").rrset
|
||||
assert dnskey_old != dnskey_new, "DNSKEY should be different"
|
||||
|
||||
def test_disable_reenable_signing_replica(self):
|
||||
|
||||
dnskey_old = resolve_with_dnssec(self.replicas[0].ip, test_zone_repl,
|
||||
self.log, rtype="DNSKEY").rrset
|
||||
|
||||
# disable DNSSEC signing of zone on replica
|
||||
args = [
|
||||
"ipa",
|
||||
"dnszone-mod", test_zone_repl,
|
||||
"--dnssec", "false",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
time.sleep(20) # sleep a bit until LDAP changes are applied to DNS
|
||||
|
||||
# test master
|
||||
assert not is_record_signed(
|
||||
self.master.ip, test_zone_repl, self.log
|
||||
), "Zone %s is still signed (master)" % test_zone_repl
|
||||
|
||||
# test replica
|
||||
assert not is_record_signed(
|
||||
self.replicas[0].ip, test_zone_repl, self.log
|
||||
), "DNS zone %s is still signed (replica)" % test_zone_repl
|
||||
|
||||
# reenable DNSSEC signing
|
||||
args = [
|
||||
"ipa",
|
||||
"dnszone-mod", test_zone_repl,
|
||||
"--dnssec", "true",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
time.sleep(20) # sleep a bit until LDAP changes are applied to DNS
|
||||
|
||||
# test master
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, test_zone_repl, self.log, timeout=100
|
||||
), "Zone %s is not signed (master)" % test_zone_repl
|
||||
|
||||
# test replica
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, test_zone_repl, self.log, timeout=200
|
||||
), "DNS zone %s is not signed (replica)" % test_zone_repl
|
||||
|
||||
dnskey_new = resolve_with_dnssec(self.replicas[0].ip, test_zone_repl,
|
||||
self.log, rtype="DNSKEY").rrset
|
||||
assert dnskey_old != dnskey_new, "DNSKEY should be different"
|
||||
|
||||
class TestInstallDNSSECFirst(IntegrationTest):
|
||||
"""Simple DNSSEC test
|
||||
|
||||
Install the server with DNSSEC and then install the replica with DNS
|
||||
"""
|
||||
num_replicas = 1
|
||||
topology = 'star'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False)
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--dnssec-master",
|
||||
"--forwarder", cls.master.config.dns_forwarder,
|
||||
"-U",
|
||||
]
|
||||
cls.master.run_command(args)
|
||||
|
||||
tasks.install_replica(cls.master, cls.replicas[0], setup_dns=True)
|
||||
|
||||
# backup trusted key
|
||||
tasks.backup_file(cls.master, paths.DNSSEC_TRUSTED_KEY)
|
||||
tasks.backup_file(cls.replicas[0], paths.DNSSEC_TRUSTED_KEY)
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls, mh):
|
||||
# restore trusted key
|
||||
tasks.restore_files(cls.master)
|
||||
tasks.restore_files(cls.replicas[0])
|
||||
|
||||
super(TestInstallDNSSECFirst, cls).uninstall(mh)
|
||||
|
||||
def test_sign_root_zone(self):
|
||||
args = [
|
||||
"ipa", "dnszone-add", root_zone, "--dnssec", "true",
|
||||
"--skip-overlap-check",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
# make BIND happy: add the glue record and delegate zone
|
||||
args = [
|
||||
"ipa", "dnsrecord-add", root_zone, self.master.hostname,
|
||||
"--a-rec=" + self.master.ip
|
||||
]
|
||||
self.master.run_command(args)
|
||||
time.sleep(10) # sleep a bit until data are provided by bind-dyndb-ldap
|
||||
|
||||
args = [
|
||||
"ipa", "dnsrecord-add", root_zone, self.master.domain.name,
|
||||
"--ns-rec=" + self.master.hostname
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
# test master
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, root_zone, self.log, timeout=100
|
||||
), "Zone %s is not signed (master)" % root_zone
|
||||
|
||||
# test replica
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, root_zone, self.log, timeout=300
|
||||
), "Zone %s is not signed (replica)" % root_zone
|
||||
|
||||
def test_chain_of_trust(self):
|
||||
"""
|
||||
Validate signed DNS records, using our own signed root zone
|
||||
:return:
|
||||
"""
|
||||
|
||||
# add test zone
|
||||
args = [
|
||||
"ipa", "dnszone-add", example_test_zone, "--dnssec", "true",
|
||||
"--skip-overlap-check",
|
||||
]
|
||||
|
||||
self.master.run_command(args)
|
||||
|
||||
# delegation
|
||||
args = [
|
||||
"ipa", "dnsrecord-add", root_zone, example_test_zone,
|
||||
"--ns-rec=" + self.master.hostname
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, example_test_zone, self.log, timeout=100
|
||||
), "Zone %s is not signed (master)" % example_test_zone
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, example_test_zone, self.log, timeout=200
|
||||
), "Zone %s is not signed (replica)" % example_test_zone
|
||||
|
||||
# GET DNSKEY records from zone
|
||||
ans = resolve_with_dnssec(self.master.ip, example_test_zone, self.log,
|
||||
rtype="DNSKEY")
|
||||
dnskey_rrset = ans.response.get_rrset(
|
||||
ans.response.answer,
|
||||
dns.name.from_text(example_test_zone),
|
||||
dns.rdataclass.IN,
|
||||
dns.rdatatype.DNSKEY)
|
||||
assert dnskey_rrset, "No DNSKEY records received"
|
||||
|
||||
self.log.debug("DNSKEY records returned: %s", dnskey_rrset.to_text())
|
||||
|
||||
# generate DS records
|
||||
ds_records = []
|
||||
for key_rdata in dnskey_rrset:
|
||||
if key_rdata.flags != 257:
|
||||
continue # it is not KSK
|
||||
ds_records.append(dns.dnssec.make_ds(example_test_zone, key_rdata,
|
||||
'sha256'))
|
||||
assert ds_records, ("No KSK returned from the %s zone" %
|
||||
example_test_zone)
|
||||
|
||||
self.log.debug("DS records for %s created: %r", example_test_zone,
|
||||
ds_records)
|
||||
|
||||
# add DS records to root zone
|
||||
args = [
|
||||
"ipa", "dnsrecord-add", root_zone, example_test_zone,
|
||||
# DS record requires to coexists with NS
|
||||
"--ns-rec", self.master.hostname,
|
||||
]
|
||||
for ds in ds_records:
|
||||
args.append("--ds-rec")
|
||||
args.append(ds.to_text())
|
||||
|
||||
self.master.run_command(args)
|
||||
|
||||
# wait until DS records it replicated
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, example_test_zone, self.log, timeout=100,
|
||||
rtype="DS"
|
||||
), "No DS record of '%s' returned from replica" % example_test_zone
|
||||
|
||||
# extract DSKEY from root zone
|
||||
ans = resolve_with_dnssec(self.master.ip, root_zone, self.log,
|
||||
rtype="DNSKEY")
|
||||
dnskey_rrset = ans.response.get_rrset(ans.response.answer,
|
||||
dns.name.from_text(root_zone),
|
||||
dns.rdataclass.IN,
|
||||
dns.rdatatype.DNSKEY)
|
||||
assert dnskey_rrset, "No DNSKEY records received"
|
||||
|
||||
self.log.debug("DNSKEY records returned: %s", dnskey_rrset.to_text())
|
||||
|
||||
# export trust keys for root zone
|
||||
root_key_rdatas = []
|
||||
for key_rdata in dnskey_rrset:
|
||||
if key_rdata.flags != 257:
|
||||
continue # it is not KSK
|
||||
root_key_rdatas.append(key_rdata)
|
||||
|
||||
assert root_key_rdatas, "No KSK returned from the root zone"
|
||||
|
||||
root_keys_rrset = dns.rrset.from_rdata_list(dnskey_rrset.name,
|
||||
dnskey_rrset.ttl,
|
||||
root_key_rdatas)
|
||||
self.log.debug("Root zone trusted key: %s", root_keys_rrset.to_text())
|
||||
|
||||
# set trusted key for our root zone
|
||||
self.master.put_file_contents(paths.DNSSEC_TRUSTED_KEY,
|
||||
root_keys_rrset.to_text() + '\n')
|
||||
self.replicas[0].put_file_contents(paths.DNSSEC_TRUSTED_KEY,
|
||||
root_keys_rrset.to_text() + '\n')
|
||||
|
||||
# verify signatures
|
||||
time.sleep(1)
|
||||
args = [
|
||||
"drill", "@localhost", "-k",
|
||||
paths.DNSSEC_TRUSTED_KEY, "-S",
|
||||
example_test_zone, "SOA"
|
||||
]
|
||||
|
||||
# test if signature chains are valid
|
||||
self.master.run_command(args)
|
||||
self.replicas[0].run_command(args)
|
||||
|
||||
|
||||
class TestMigrateDNSSECMaster(IntegrationTest):
|
||||
"""test DNSSEC master migration
|
||||
|
||||
Install a server and a replica with DNS, then reinstall server
|
||||
as DNSSEC master
|
||||
Test:
|
||||
* migrate dnssec master to replica
|
||||
* create new zone
|
||||
* verify if zone is signed on all replicas
|
||||
* add new replica
|
||||
* add new zone
|
||||
* test if new zone is signed on all replicas
|
||||
"""
|
||||
num_replicas = 2
|
||||
topology = 'star'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--dnssec-master",
|
||||
"--forwarder", cls.master.config.dns_forwarder,
|
||||
"-U",
|
||||
]
|
||||
cls.master.run_command(args)
|
||||
tasks.install_replica(cls.master, cls.replicas[0], setup_dns=True)
|
||||
|
||||
def test_migrate_dnssec_master(self):
|
||||
"""Both master and replica have DNS installed"""
|
||||
backup_filename = "/var/lib/ipa/ipa-kasp.db.backup"
|
||||
replica_backup_filename = "/tmp/ipa-kasp.db.backup"
|
||||
|
||||
# add test zone
|
||||
args = [
|
||||
"ipa", "dnszone-add", example_test_zone, "--dnssec", "true",
|
||||
"--skip-overlap-check",
|
||||
]
|
||||
|
||||
self.master.run_command(args)
|
||||
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, example_test_zone, self.log, timeout=100
|
||||
), "Zone %s is not signed (master)" % example_test_zone
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, example_test_zone, self.log, timeout=200
|
||||
), "Zone %s is not signed (replica)" % example_test_zone
|
||||
|
||||
dnskey_old = resolve_with_dnssec(self.master.ip, example_test_zone,
|
||||
self.log, rtype="DNSKEY").rrset
|
||||
|
||||
# migrate dnssec master to replica
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--disable-dnssec-master",
|
||||
"--forwarder", self.master.config.dns_forwarder,
|
||||
"--force",
|
||||
"-U",
|
||||
]
|
||||
self.master.run_command(args)
|
||||
|
||||
# move content of "ipa-kasp.db.backup" to replica
|
||||
kasp_db_backup = self.master.get_file_contents(backup_filename)
|
||||
self.replicas[0].put_file_contents(replica_backup_filename,
|
||||
kasp_db_backup)
|
||||
|
||||
args = [
|
||||
"ipa-dns-install",
|
||||
"--dnssec-master",
|
||||
"--kasp-db", replica_backup_filename,
|
||||
"--forwarder", self.master.config.dns_forwarder,
|
||||
"-U",
|
||||
]
|
||||
self.replicas[0].run_command(args)
|
||||
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, example_test_zone, self.log, timeout=100
|
||||
), "Zone %s is not signed after migration (master)" % example_test_zone
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, example_test_zone, self.log, timeout=200
|
||||
), "Zone %s is not signed after migration (replica)" % example_test_zone
|
||||
|
||||
# test if dnskey are the same
|
||||
dnskey_new = resolve_with_dnssec(self.master.ip, example_test_zone,
|
||||
self.log, rtype="DNSKEY").rrset
|
||||
assert dnskey_old == dnskey_new, "DNSKEY should be the same"
|
||||
|
||||
# add test zone
|
||||
args = [
|
||||
"ipa", "dnszone-add", example2_test_zone, "--dnssec", "true",
|
||||
"--skip-overlap-check",
|
||||
]
|
||||
self.replicas[0].run_command(args)
|
||||
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, example2_test_zone, self.log, timeout=100
|
||||
), ("Zone %s is not signed after migration (replica - dnssec master)"
|
||||
% example2_test_zone)
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, example2_test_zone, self.log, timeout=200
|
||||
), ("Zone %s is not signed after migration (master)"
|
||||
% example2_test_zone)
|
||||
|
||||
# add new replica
|
||||
tasks.install_replica(self.master, self.replicas[1], setup_dns=True)
|
||||
|
||||
# test if originial zones are signed on new replica
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[1].ip, example_test_zone, self.log, timeout=200
|
||||
), ("Zone %s is not signed (new replica)"
|
||||
% example_test_zone)
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[1].ip, example2_test_zone, self.log, timeout=200
|
||||
), ("Zone %s is not signed (new replica)"
|
||||
% example2_test_zone)
|
||||
|
||||
# add new zone to new replica
|
||||
args = [
|
||||
"ipa", "dnszone-add", example3_test_zone, "--dnssec", "true",
|
||||
"--skip-overlap-check",
|
||||
]
|
||||
self.replicas[1].run_command(args)
|
||||
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[1].ip, example3_test_zone, self.log, timeout=200
|
||||
), ("Zone %s is not signed (new replica)"
|
||||
% example3_test_zone)
|
||||
assert wait_until_record_is_signed(
|
||||
self.replicas[0].ip, example3_test_zone, self.log, timeout=200
|
||||
), ("Zone %s is not signed (replica)"
|
||||
% example3_test_zone)
|
||||
# wait until zone is signed
|
||||
assert wait_until_record_is_signed(
|
||||
self.master.ip, example3_test_zone, self.log, timeout=200
|
||||
), ("Zone %s is not signed (master)"
|
||||
% example3_test_zone)
|
||||
@@ -33,9 +33,7 @@ class TestExternalCA(IntegrationTest):
|
||||
'-a', self.master.config.admin_password,
|
||||
'-p', self.master.config.dirman_password,
|
||||
'--setup-dns', '--no-forwarders',
|
||||
'-n', self.master.domain.name,
|
||||
'-r', self.master.domain.realm,
|
||||
'--domain-level=%i' % self.master.config.domain_level,
|
||||
'-r', self.master.domain.name,
|
||||
'--external-ca'
|
||||
])
|
||||
|
||||
@@ -99,8 +97,8 @@ class TestExternalCA(IntegrationTest):
|
||||
'ipa-server-install',
|
||||
'-a', self.master.config.admin_password,
|
||||
'-p', self.master.config.dirman_password,
|
||||
'--external-cert-file', external_cert_file,
|
||||
'--external-cert-file', external_ca_file
|
||||
'--external_cert_file', external_cert_file,
|
||||
'--external_ca_file', external_ca_file
|
||||
])
|
||||
|
||||
# Make sure IPA server is working properly
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import os
|
||||
import subprocess
|
||||
from ipaplatform.paths import paths
|
||||
import pytest
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
@@ -36,8 +35,8 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
num_clients = 1
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
super(TestForcedClientReenrollment, cls).install(mh)
|
||||
def install(cls):
|
||||
super(TestForcedClientReenrollment, cls).install()
|
||||
tasks.install_master(cls.master)
|
||||
tasks.install_replica(cls.master, cls.replicas[0], setup_ca=False)
|
||||
cls.BACKUP_KEYTAB = os.path.join(
|
||||
@@ -45,7 +44,15 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
'krb5.keytab'
|
||||
)
|
||||
|
||||
def test_reenroll_with_force_join(self, client):
|
||||
def setUp(self):
|
||||
tasks.prepare_host(self.clients[0])
|
||||
tasks.install_client(self.master, self.clients[0])
|
||||
|
||||
def tearDown(self):
|
||||
tasks.uninstall_client(self.clients[0])
|
||||
self.delete_client_host_entry()
|
||||
|
||||
def test_reenroll_with_force_join(self):
|
||||
"""
|
||||
Client re-enrollment using admin credentials (--force-join)
|
||||
"""
|
||||
@@ -56,7 +63,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
sshfp_record_post = self.get_sshfp_record()
|
||||
assert sshfp_record_pre == sshfp_record_post
|
||||
|
||||
def test_reenroll_with_keytab(self, client):
|
||||
def test_reenroll_with_keytab(self):
|
||||
"""
|
||||
Client re-enrollment using keytab
|
||||
"""
|
||||
@@ -69,7 +76,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
sshfp_record_post = self.get_sshfp_record()
|
||||
assert sshfp_record_pre == sshfp_record_post
|
||||
|
||||
def test_reenroll_with_both_force_join_and_keytab(self, client):
|
||||
def test_reenroll_with_both_force_join_and_keytab(self):
|
||||
"""
|
||||
Client re-enrollment using both --force-join and --keytab options
|
||||
"""
|
||||
@@ -82,7 +89,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
sshfp_record_post = self.get_sshfp_record()
|
||||
assert sshfp_record_pre == sshfp_record_post
|
||||
|
||||
def test_reenroll_to_replica(self, client):
|
||||
def test_reenroll_to_replica(self):
|
||||
"""
|
||||
Client re-enrollment using keytab, to a replica
|
||||
"""
|
||||
@@ -95,7 +102,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
sshfp_record_post = self.get_sshfp_record()
|
||||
assert sshfp_record_pre == sshfp_record_post
|
||||
|
||||
def test_try_to_reenroll_with_disabled_host(self, client):
|
||||
def test_try_to_reenroll_with_disabled_host(self):
|
||||
"""
|
||||
Client re-enrollment using keytab, with disabled host
|
||||
"""
|
||||
@@ -106,7 +113,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
self.restore_keytab()
|
||||
self.reenroll_client(keytab=self.BACKUP_KEYTAB, expect_fail=True)
|
||||
|
||||
def test_try_to_reenroll_with_uninstalled_host(self, client):
|
||||
def test_try_to_reenroll_with_uninstalled_host(self):
|
||||
"""
|
||||
Client re-enrollment using keytab, with uninstalled host
|
||||
"""
|
||||
@@ -117,7 +124,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
self.restore_keytab()
|
||||
self.reenroll_client(keytab=self.BACKUP_KEYTAB, expect_fail=True)
|
||||
|
||||
def test_try_to_reenroll_with_deleted_host(self, client):
|
||||
def test_try_to_reenroll_with_deleted_host(self):
|
||||
"""
|
||||
Client re-enrollment using keytab, with deleted host
|
||||
"""
|
||||
@@ -128,7 +135,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
self.restore_keytab()
|
||||
self.reenroll_client(keytab=self.BACKUP_KEYTAB, expect_fail=True)
|
||||
|
||||
def test_try_to_reenroll_with_incorrect_keytab(self, client):
|
||||
def test_try_to_reenroll_with_incorrect_keytab(self):
|
||||
"""
|
||||
Client re-enrollment using keytab, with incorrect keytab file
|
||||
"""
|
||||
@@ -198,7 +205,7 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
assert 'IPA Server: %s' % server.hostname in result.stderr_text
|
||||
|
||||
if expect_fail:
|
||||
err_msg = "Kerberos authentication failed: "
|
||||
err_msg = 'Kerberos authentication failed using keytab'
|
||||
assert result.returncode == 1
|
||||
assert err_msg in result.stderr_text
|
||||
elif force_join and keytab:
|
||||
@@ -227,11 +234,10 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
['ipa', 'host-disable', self.clients[0].hostname]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def delete_client_host_entry(cls):
|
||||
def delete_client_host_entry(self):
|
||||
try:
|
||||
cls.master.run_command(
|
||||
['ipa', 'host-del', cls.clients[0].hostname]
|
||||
self.master.run_command(
|
||||
['ipa', 'host-del', self.clients[0].hostname]
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 2:
|
||||
@@ -275,13 +281,3 @@ class TestForcedClientReenrollment(IntegrationTest):
|
||||
if not contents.startswith(nameserver):
|
||||
contents = nameserver + contents.replace(nameserver, '')
|
||||
client.put_file_contents(paths.RESOLV_CONF, contents)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(request):
|
||||
tasks.install_client(request.cls.master, request.cls.clients[0])
|
||||
|
||||
def teardown_client():
|
||||
tasks.uninstall_client(request.cls.clients[0])
|
||||
request.cls.delete_client_host_entry()
|
||||
request.addfinalizer(teardown_client)
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Module provides tests which testing ability of various subsystems to be
|
||||
installed.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
|
||||
|
||||
class InstallTestBase1(IntegrationTest):
|
||||
|
||||
num_replicas = 3
|
||||
topology = 'star'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False)
|
||||
|
||||
def test_replica0_ca_less_install(self):
|
||||
tasks.install_replica(self.master, self.replicas[0], setup_ca=False)
|
||||
|
||||
def test_replica0_ipa_ca_install(self):
|
||||
tasks.install_ca(self.replicas[0])
|
||||
|
||||
def test_replica0_ipa_kra_install(self):
|
||||
tasks.install_kra(self.replicas[0], first_instance=True)
|
||||
|
||||
def test_replica0_ipa_dns_install(self):
|
||||
tasks.install_dns(self.replicas[0])
|
||||
|
||||
def test_replica1_with_ca_install(self):
|
||||
tasks.install_replica(self.master, self.replicas[1], setup_ca=True)
|
||||
|
||||
def test_replica1_ipa_kra_install(self):
|
||||
tasks.install_kra(self.replicas[1])
|
||||
|
||||
def test_replica1_ipa_dns_install(self):
|
||||
tasks.install_dns(self.replicas[1])
|
||||
|
||||
def test_replica2_with_ca_kra_install(self):
|
||||
tasks.install_replica(self.master, self.replicas[2], setup_ca=True,
|
||||
setup_kra=True)
|
||||
|
||||
def test_replica2_ipa_dns_install(self):
|
||||
tasks.install_dns(self.replicas[2])
|
||||
|
||||
|
||||
class InstallTestBase2(IntegrationTest):
|
||||
|
||||
num_replicas = 3
|
||||
topology = 'star'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False)
|
||||
|
||||
def test_replica0_with_ca_kra_dns_install(self):
|
||||
tasks.install_replica(self.master, self.replicas[0], setup_ca=True,
|
||||
setup_kra=True, setup_dns=True)
|
||||
|
||||
def test_replica1_with_ca_dns_install(self):
|
||||
tasks.install_replica(self.master, self.replicas[1], setup_ca=True,
|
||||
setup_dns=True)
|
||||
|
||||
def test_replica1_ipa_kra_install(self):
|
||||
tasks.install_kra(self.replicas[1])
|
||||
|
||||
def test_replica2_with_dns_install(self):
|
||||
tasks.install_replica(self.master, self.replicas[2], setup_ca=False,
|
||||
setup_dns=True)
|
||||
|
||||
def test_replica2_ipa_ca_install(self):
|
||||
tasks.install_ca(self.replicas[2])
|
||||
|
||||
def test_replica2_ipa_kra_install(self):
|
||||
tasks.install_kra(self.replicas[2])
|
||||
|
||||
|
||||
##
|
||||
# Master X Replicas installation tests
|
||||
##
|
||||
|
||||
class TestInstallWithCA1(InstallTestBase1):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False)
|
||||
|
||||
|
||||
class TestInstallWithCA2(InstallTestBase2):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False)
|
||||
|
||||
|
||||
class TestInstallWithCA_KRA1(InstallTestBase1):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False, setup_kra=True)
|
||||
|
||||
def test_replica0_ipa_kra_install(self):
|
||||
tasks.install_kra(self.replicas[0], first_instance=False)
|
||||
|
||||
|
||||
class TestInstallWithCA_KRA2(InstallTestBase2):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=False, setup_kra=True)
|
||||
|
||||
|
||||
class TestInstallWithCA_DNS1(InstallTestBase1):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
|
||||
|
||||
class TestInstallWithCA_DNS2(InstallTestBase2):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
|
||||
|
||||
class TestInstallWithCA_KRA_DNS1(InstallTestBase1):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True, setup_kra=True)
|
||||
|
||||
def test_replica0_ipa_kra_install(self):
|
||||
tasks.install_kra(self.replicas[0], first_instance=False)
|
||||
|
||||
|
||||
class TestInstallWithCA_KRA_DNS2(InstallTestBase2):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True, setup_kra=True)
|
||||
|
||||
|
||||
##
|
||||
# Rest of master installation tests
|
||||
##
|
||||
|
||||
class TestInstallMaster(IntegrationTest):
|
||||
|
||||
num_replicas = 0
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
pass
|
||||
|
||||
def test_install_master(self):
|
||||
tasks.install_master(self.master, setup_dns=False)
|
||||
|
||||
def test_install_kra(self):
|
||||
tasks.install_kra(self.master, first_instance=True)
|
||||
|
||||
def test_install_dns(self):
|
||||
tasks.install_dns(self.master)
|
||||
|
||||
|
||||
class TestInstallMasterKRA(IntegrationTest):
|
||||
|
||||
num_replicas = 0
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
pass
|
||||
|
||||
def test_install_master(self):
|
||||
tasks.install_master(self.master, setup_dns=False, setup_kra=True)
|
||||
|
||||
def test_install_dns(self):
|
||||
tasks.install_dns(self.master)
|
||||
|
||||
|
||||
class TestInstallMasterDNS(IntegrationTest):
|
||||
|
||||
num_replicas = 0
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
pass
|
||||
|
||||
def test_install_master(self):
|
||||
tasks.install_master(self.master, setup_dns=True)
|
||||
|
||||
def test_install_kra(self):
|
||||
tasks.install_kra(self.master, first_instance=True)
|
||||
@@ -17,9 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# FIXME: Pylint errors
|
||||
# pylint: disable=no-member
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -46,10 +43,6 @@ class BaseTestLegacyClient(object):
|
||||
'/etc/nsswitch.conf',
|
||||
paths.SSSD_CONF]
|
||||
|
||||
homedir_template = "/home/{domain}/{username}"
|
||||
required_extra_roles = ()
|
||||
optional_extra_roles = ()
|
||||
|
||||
# Actual test classes need to override these attributes to set the expected
|
||||
# values on the UID and GID results, since this varies with the usage of the
|
||||
# POSIX and non-POSIX ID ranges
|
||||
@@ -62,6 +55,26 @@ class BaseTestLegacyClient(object):
|
||||
# To allow custom validation dependent on the trust type
|
||||
posix_trust = False
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
super(BaseTestLegacyClient, cls).setup_class()
|
||||
cls.ad = cls.ad_domains[0].ads[0]
|
||||
|
||||
cls.legacy_client = cls.host_by_role(cls.required_extra_roles[0])
|
||||
|
||||
# Determine whether the subdomain AD is available
|
||||
try:
|
||||
child_ad = cls.host_by_role(cls.optional_extra_roles[0])
|
||||
cls.ad_subdomain = '.'.join(
|
||||
child_ad.hostname.split('.')[1:])
|
||||
except LookupError:
|
||||
cls.ad_subdomain = None
|
||||
|
||||
tasks.apply_common_fixes(cls.legacy_client)
|
||||
|
||||
for f in cls.backup_files:
|
||||
tasks.backup_file(cls.legacy_client, f)
|
||||
|
||||
def test_apply_advice(self):
|
||||
# Obtain the advice from the server
|
||||
tasks.kinit_admin(self.master)
|
||||
@@ -327,8 +340,8 @@ class BaseTestLegacyClient(object):
|
||||
assert result.returncode != 0
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
super(BaseTestLegacyClient, cls).install(mh)
|
||||
def install(cls):
|
||||
super(BaseTestLegacyClient, cls).install()
|
||||
|
||||
tasks.kinit_admin(cls.master)
|
||||
|
||||
@@ -346,25 +359,8 @@ class BaseTestLegacyClient(object):
|
||||
|
||||
cls.master.run_command(['ipa', 'user-disable', 'disabledipauser'])
|
||||
|
||||
cls.ad = cls.ad_domains[0].ads[0]
|
||||
|
||||
cls.legacy_client = cls.host_by_role(cls.required_extra_roles[0])
|
||||
|
||||
# Determine whether the subdomain AD is available
|
||||
try:
|
||||
child_ad = cls.host_by_role(cls.optional_extra_roles[0])
|
||||
cls.ad_subdomain = '.'.join(
|
||||
child_ad.hostname.split('.')[1:])
|
||||
except LookupError:
|
||||
cls.ad_subdomain = None
|
||||
|
||||
tasks.apply_common_fixes(cls.legacy_client)
|
||||
|
||||
for f in cls.backup_files:
|
||||
tasks.backup_file(cls.legacy_client, f)
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls, mh):
|
||||
def uninstall(cls):
|
||||
cls.master.run_command(['ipa', 'user-del', 'disabledipauser'],
|
||||
raiseonerr=False)
|
||||
|
||||
@@ -372,7 +368,7 @@ class BaseTestLegacyClient(object):
|
||||
if hasattr(cls, 'legacy_client'):
|
||||
tasks.unapply_fixes(cls.legacy_client)
|
||||
|
||||
super(BaseTestLegacyClient, cls).uninstall(mh)
|
||||
super(BaseTestLegacyClient, cls).uninstall()
|
||||
|
||||
|
||||
# Base classes with attributes that are specific for each legacy client test
|
||||
@@ -414,6 +410,7 @@ class BaseTestLegacyClientPosix(BaseTestLegacyClient,
|
||||
testuser_gid_regex = '10047'
|
||||
subdomain_testuser_uid_regex = '10142'
|
||||
subdomain_testuser_gid_regex = '10147'
|
||||
homedir_template = "/home/{domain}/{username}"
|
||||
posix_trust = True
|
||||
|
||||
def test_remove_trust_with_posix_attributes(self):
|
||||
@@ -427,6 +424,7 @@ class BaseTestLegacyClientNonPosix(BaseTestLegacyClient,
|
||||
testuser_gid_regex = '(?!10047)(\d+)'
|
||||
subdomain_testuser_uid_regex = '(?!10142)(\d+)'
|
||||
subdomain_testuser_gid_regex = '(?!10147)(\d+)'
|
||||
homedir_template = '/home/{domain}/{username}'
|
||||
|
||||
def test_remove_nonposix_trust(self):
|
||||
pass
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Authors:
|
||||
# Petr Viktorin <pviktori@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2014 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Test the ordering of tests
|
||||
|
||||
IPA integration tests, marked with `@ordered`, require tests to be run
|
||||
in a specific order:
|
||||
- Base classes first
|
||||
- Within a class, test methods are ordered according to source line
|
||||
"""
|
||||
|
||||
from pytest_sourceorder import ordered
|
||||
|
||||
|
||||
@ordered
|
||||
class TestBase(object):
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.value = 'unchanged'
|
||||
|
||||
def test_d_first(self):
|
||||
type(self).value = 'changed once'
|
||||
|
||||
|
||||
class TestChild(TestBase):
|
||||
def test_b_third(self):
|
||||
assert type(self).value == 'changed twice'
|
||||
type(self).value = 'changed thrice'
|
||||
|
||||
def test_a_fourth(self):
|
||||
assert type(self).value == 'changed thrice'
|
||||
|
||||
|
||||
def test_c_second(self):
|
||||
assert type(self).value == 'changed once'
|
||||
type(self).value = 'changed twice'
|
||||
TestBase.test_c_second = test_c_second
|
||||
del test_c_second
|
||||
@@ -1,223 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
from ipatests.test_integration.test_caless import assert_error
|
||||
from ipalib.constants import DOMAIN_LEVEL_0
|
||||
from ipalib.constants import DOMAIN_LEVEL_1
|
||||
from ipalib.constants import DOMAIN_SUFFIX_NAME
|
||||
from ipatests.test_integration.tasks import replicas_cleanup
|
||||
|
||||
|
||||
class ReplicaPromotionBase(IntegrationTest):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, domain_level=cls.domain_level)
|
||||
|
||||
def test_kra_install_master(self):
|
||||
result1 = tasks.install_kra(self.master,
|
||||
first_instance=True,
|
||||
raiseonerr=False)
|
||||
assert result1.returncode == 0, result1.stderr_text
|
||||
tasks.kinit_admin(self.master)
|
||||
result2 = self.master.run_command(["ipa", "vault-find"],
|
||||
raiseonerr=False)
|
||||
found = result2.stdout_text.find("0 vaults matched")
|
||||
assert(found > 0), result2.stdout_text
|
||||
|
||||
|
||||
class TestReplicaPromotionLevel0(ReplicaPromotionBase):
|
||||
|
||||
topology = 'star'
|
||||
num_replicas = 1
|
||||
domain_level = DOMAIN_LEVEL_0
|
||||
|
||||
@replicas_cleanup
|
||||
def test_promotion_disabled(self):
|
||||
"""
|
||||
Testcase:
|
||||
http://www.freeipa.org/page/V4/Replica_Promotion/Test_plan#Test_case:
|
||||
_Make_sure_the_feature_is_unavailable_under_domain_level_0
|
||||
"""
|
||||
client = self.replicas[0]
|
||||
tasks.install_client(self.master, client)
|
||||
args = ['ipa-replica-install', '-U',
|
||||
'-p', self.master.config.dirman_password,
|
||||
'-w', self.master.config.admin_password,
|
||||
'--ip-address', client.ip]
|
||||
result = client.run_command(args, raiseonerr=False)
|
||||
assert_error(result,
|
||||
'You must provide a file generated by ipa-replica-prepare'
|
||||
' to create a replica when the domain is at level 0', 1)
|
||||
|
||||
@replicas_cleanup
|
||||
def test_backup_restore(self):
|
||||
"""
|
||||
TestCase:
|
||||
http://www.freeipa.org/page/V4/Replica_Promotion/Test_plan#Test_case:
|
||||
_ipa-restore_after_domainlevel_raise_restores_original_domain_level
|
||||
"""
|
||||
command = ["ipa", "topologysegment-find", DOMAIN_SUFFIX_NAME]
|
||||
tasks.install_replica(self.master, self.replicas[0])
|
||||
backup_file = tasks.ipa_backup(self.master)
|
||||
self.master.run_command(["ipa", "domainlevel-set", "1"])
|
||||
result = self.master.run_command(command)
|
||||
found1 = result.stdout_text.rfind("1 segment matched")
|
||||
assert(found1 > 0), result.stdout_text
|
||||
tasks.ipa_restore(self.master, backup_file)
|
||||
result2 = self.master.run_command(command, raiseonerr=False)
|
||||
found2 = result2.stdout_text.rfind("0 segments matched")
|
||||
assert(found2 > 0), result2.stdout_text
|
||||
|
||||
|
||||
class TestKRAInstall(IntegrationTest):
|
||||
"""
|
||||
TestCase: http://www.freeipa.org/page/V4/Replica_Promotion/Test_plan
|
||||
#Test_case:_ipa-kra-install_with_replica_file_works_only_on_domain_level_0
|
||||
"""
|
||||
topology = 'star'
|
||||
domain_level = DOMAIN_LEVEL_0
|
||||
num_replicas = 2
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, domain_level=cls.domain_level)
|
||||
|
||||
def test_kra_install_without_replica_file(self):
|
||||
master = self.master
|
||||
replica1 = self.replicas[0]
|
||||
replica2 = self.replicas[1]
|
||||
tasks.install_kra(master, first_instance=True)
|
||||
tasks.install_replica(master, replica1)
|
||||
result1 = tasks.install_kra(replica1,
|
||||
domain_level=DOMAIN_LEVEL_1,
|
||||
raiseonerr=False)
|
||||
assert_error(result1, "A replica file is required", 1)
|
||||
tasks.install_kra(replica1,
|
||||
domain_level=DOMAIN_LEVEL_0,
|
||||
raiseonerr=True)
|
||||
# Now prepare the replica file, copy it to the client and raise
|
||||
# domain level on master to test the reverse situation
|
||||
tasks.replica_prepare(master, replica2)
|
||||
master.run_command(["ipa", "domainlevel-set", str(DOMAIN_LEVEL_1)])
|
||||
tasks.install_replica(master, replica2)
|
||||
result2 = tasks.install_kra(replica2,
|
||||
domain_level=DOMAIN_LEVEL_0,
|
||||
raiseonerr=False)
|
||||
assert_error(result2, "No replica file is required", 1)
|
||||
tasks.install_kra(replica2)
|
||||
|
||||
|
||||
class TestCAInstall(IntegrationTest):
|
||||
topology = 'star'
|
||||
domain_level = DOMAIN_LEVEL_0
|
||||
num_replicas = 2
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, domain_level=cls.domain_level)
|
||||
|
||||
def test_ca_install_without_replica_file(self):
|
||||
"""
|
||||
TestCase:
|
||||
http://www.freeipa.org/page/V4/Replica_Promotion/Test_plan#Test_case:
|
||||
_ipa-ca-install_with_replica_file_works_only_on_domain_level_0
|
||||
"""
|
||||
master = self.master
|
||||
replica1 = self.replicas[0]
|
||||
replica2 = self.replicas[1]
|
||||
for replica in self.replicas:
|
||||
tasks.install_replica(master, replica, setup_ca=False,
|
||||
setup_dns=True)
|
||||
result1 = tasks.install_ca(replica1,
|
||||
domain_level=DOMAIN_LEVEL_1,
|
||||
raiseonerr=False)
|
||||
assert_error(result1, "If you wish to replicate CA to this host,"
|
||||
" please re-run 'ipa-ca-install'\nwith a"
|
||||
" replica file generated on an existing CA"
|
||||
" master as argument.", 1)
|
||||
|
||||
tasks.install_ca(replica1, domain_level=DOMAIN_LEVEL_0)
|
||||
# Now prepare the replica file, copy it to the client and raise
|
||||
# domain level on master to test the reverse situation
|
||||
master.run_command(["ipa", "domainlevel-set", str(DOMAIN_LEVEL_1)])
|
||||
result2 = tasks.install_ca(replica2,
|
||||
domain_level=DOMAIN_LEVEL_0,
|
||||
raiseonerr=False)
|
||||
assert_error(result2, 'Too many parameters provided.'
|
||||
' No replica file is required', 1)
|
||||
tasks.install_ca(replica2, domain_level=DOMAIN_LEVEL_1)
|
||||
|
||||
|
||||
class TestReplicaPromotionLevel1(ReplicaPromotionBase):
|
||||
"""
|
||||
TestCase: http://www.freeipa.org/page/V4/Replica_Promotion/Test_plan#
|
||||
Test_case:_Make_sure_the_old_workflow_is_disabled_at_domain_level_1
|
||||
"""
|
||||
|
||||
topology = 'star'
|
||||
num_replicas = 1
|
||||
domain_level = DOMAIN_LEVEL_1
|
||||
|
||||
@replicas_cleanup
|
||||
def test_replica_prepare_disabled(self):
|
||||
replica = self.replicas[0]
|
||||
args = ['ipa-replica-prepare',
|
||||
'-p', self.master.config.dirman_password,
|
||||
'--ip-address', replica.ip,
|
||||
replica.hostname]
|
||||
result = self.master.run_command(args, raiseonerr=False)
|
||||
assert_error(result, "Replica creation using 'ipa-replica-prepare'"
|
||||
" to generate replica file\n"
|
||||
"is supported only in 0-level IPA domain", 1)
|
||||
|
||||
|
||||
class TestReplicaManageCommands(IntegrationTest):
|
||||
topology = "star"
|
||||
num_replicas = 2
|
||||
domain_level = DOMAIN_LEVEL_0
|
||||
|
||||
def test_replica_manage_commands(self):
|
||||
"""
|
||||
TestCase: http://www.freeipa.org/page/V4/Replica_Promotion/Test_plan
|
||||
#Test_case:_ipa-replica-manage_connect_is_deprecated_in_domain_level_1
|
||||
"""
|
||||
master = self.master
|
||||
replica1 = self.replicas[0]
|
||||
replica2 = self.replicas[1]
|
||||
|
||||
result1 = master.run_command(["ipa-replica-manage",
|
||||
"connect",
|
||||
replica1.hostname,
|
||||
replica2.hostname],
|
||||
raiseonerr=False)
|
||||
assert result1.returncode == 0, result1.stderr_text
|
||||
result2 = master.run_command(["ipa-replica-manage",
|
||||
"disconnect",
|
||||
replica1.hostname,
|
||||
replica2.hostname],
|
||||
raiseonerr=False)
|
||||
assert result2.returncode == 0, result2.stderr_text
|
||||
master.run_command(["ipa", "domainlevel-set", str(DOMAIN_LEVEL_1)])
|
||||
result3 = master.run_command(["ipa-replica-manage",
|
||||
"connect",
|
||||
replica1.hostname,
|
||||
replica2.hostname],
|
||||
raiseonerr=False)
|
||||
assert_error(result3, 'Creation of IPA replication agreement is'
|
||||
' deprecated with managed IPA replication'
|
||||
' topology. Please use `ipa topologysegment-*`'
|
||||
' commands to manage the topology', 1)
|
||||
tasks.create_segment(master, replica1, replica2)
|
||||
result4 = master.run_command(["ipa-replica-manage",
|
||||
"disconnect",
|
||||
replica1.hostname,
|
||||
replica2.hostname],
|
||||
raiseonerr=False)
|
||||
assert_error(result4, 'Removal of IPA replication agreement is'
|
||||
' deprecated with managed IPA replication'
|
||||
' topology. Please use `ipa topologysegment-*`'
|
||||
' commands to manage the topology', 1)
|
||||
@@ -1,123 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import time
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
|
||||
|
||||
class LayoutsBaseTest(IntegrationTest):
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
# tests use custom installation
|
||||
pass
|
||||
|
||||
def replication_is_working(self):
|
||||
test_user = 'replication-testuser'
|
||||
self.master.run_command(
|
||||
['ipa', 'user-add', test_user, '--first', 'test', '--last', 'user']
|
||||
)
|
||||
|
||||
time.sleep(60) # make sure the replication of user is done
|
||||
|
||||
for r in self.replicas:
|
||||
r.run_command(['ipa', 'user-show', test_user])
|
||||
|
||||
|
||||
class TestLineTopologyWithoutCA(LayoutsBaseTest):
|
||||
|
||||
num_replicas = 3
|
||||
|
||||
def test_line_topology_without_ca(self):
|
||||
tasks.install_topo('line', self.master, self.replicas, [],
|
||||
setup_replica_cas=False)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestLineTopologyWithCA(LayoutsBaseTest):
|
||||
|
||||
num_replicas = 3
|
||||
|
||||
def test_line_topology_with_ca(self):
|
||||
tasks.install_topo('line', self.master, self.replicas, [],
|
||||
setup_replica_cas=True)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestStarTopologyWithoutCA(LayoutsBaseTest):
|
||||
|
||||
num_replicas = 3
|
||||
|
||||
def test_star_topology_without_ca(self):
|
||||
tasks.install_topo('star', self.master, self.replicas, [],
|
||||
setup_replica_cas=False)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestStarTopologyWithCA(LayoutsBaseTest):
|
||||
|
||||
num_replicas = 3
|
||||
|
||||
def test_star_topology_with_ca(self):
|
||||
tasks.install_topo('star', self.master, self.replicas, [],
|
||||
setup_replica_cas=True)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestCompleteTopologyWithoutCA(LayoutsBaseTest):
|
||||
|
||||
num_replicas = 3
|
||||
|
||||
def test_complete_topology_without_ca(self):
|
||||
tasks.install_topo('complete', self.master, self.replicas, [],
|
||||
setup_replica_cas=False)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestCompleteTopologyWithCA(LayoutsBaseTest):
|
||||
|
||||
num_replicas = 3
|
||||
|
||||
def test_complete_topology_with_ca(self):
|
||||
tasks.install_topo('complete', self.master, self.replicas, [],
|
||||
setup_replica_cas=True)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class Test2ConnectedTopologyWithoutCA(LayoutsBaseTest):
|
||||
num_replicas = 33
|
||||
|
||||
def test_2_connected_topology_without_ca(self):
|
||||
tasks.install_topo('2-connected', self.master, self.replicas, [],
|
||||
setup_replica_cas=False)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class Test2ConnectedTopologyWithCA(LayoutsBaseTest):
|
||||
num_replicas = 33
|
||||
|
||||
def test_2_connected_topology_with_ca(self):
|
||||
tasks.install_topo('2-connected', self.master, self.replicas, [],
|
||||
setup_replica_cas=True)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestDoubleCircleTopologyWithoutCA(LayoutsBaseTest):
|
||||
num_replicas = 29
|
||||
|
||||
def test_2_connected_topology_with_ca(self):
|
||||
tasks.install_topo('double-circle', self.master, self.replicas, [],
|
||||
setup_replica_cas=False)
|
||||
self.replication_is_working()
|
||||
|
||||
|
||||
class TestDoubleCircleTopologyWithCA(LayoutsBaseTest):
|
||||
num_replicas = 29
|
||||
|
||||
def test_2_connected_topology_with_ca(self):
|
||||
tasks.install_topo('double-circle', self.master, self.replicas, [],
|
||||
setup_replica_cas=True)
|
||||
self.replication_is_working()
|
||||
@@ -1,82 +0,0 @@
|
||||
# Authors:
|
||||
# Petr Viktorin <pviktori@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2014 Red Hat
|
||||
# see file 'COPYING' for use and warranty information
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
|
||||
|
||||
class TestServicePermissions(IntegrationTest):
|
||||
topology = 'star'
|
||||
|
||||
def test_service_as_user_admin(self):
|
||||
"""Test that a service in User Administrator role can manage users"""
|
||||
|
||||
service_name = 'testservice/%s@%s' % (self.master.hostname,
|
||||
self.master.domain.realm)
|
||||
keytab_file = os.path.join(self.master.config.test_dir,
|
||||
'testservice_keytab')
|
||||
|
||||
# Prepare a service
|
||||
|
||||
self.master.run_command(['ipa', 'service-add', service_name])
|
||||
|
||||
self.master.run_command(['ipa-getkeytab',
|
||||
'-p', service_name,
|
||||
'-k', keytab_file,
|
||||
'-s', self.master.hostname])
|
||||
|
||||
# Check that the service cannot add a user
|
||||
|
||||
self.master.run_command(['kdestroy'])
|
||||
self.master.run_command(['kinit', '-k', service_name,
|
||||
'-t', keytab_file])
|
||||
|
||||
result = self.master.run_command(['ipa', 'role-add-member',
|
||||
'User Administrator',
|
||||
'--service', service_name],
|
||||
raiseonerr=False)
|
||||
assert result.returncode > 0
|
||||
|
||||
# Add service to User Administrator role
|
||||
|
||||
self.master.run_command(['kdestroy'])
|
||||
tasks.kinit_admin(self.master)
|
||||
|
||||
self.master.run_command(['ipa', 'role-add-member',
|
||||
'User Administrator',
|
||||
'--service', service_name])
|
||||
|
||||
# Check that the service now can add a user
|
||||
|
||||
self.master.run_command(['kdestroy'])
|
||||
self.master.run_command(['kinit', '-k', service_name,
|
||||
'-t', keytab_file])
|
||||
|
||||
self.master.run_command(['ipa', 'user-add', 'tuser',
|
||||
'--first', 'a', '--last', 'b', '--random'])
|
||||
|
||||
# Clean up
|
||||
|
||||
self.master.run_command(['kdestroy'])
|
||||
tasks.kinit_admin(self.master)
|
||||
|
||||
self.master.run_command(['ipa', 'service-del', service_name])
|
||||
self.master.run_command(['ipa', 'user-del', 'tuser'])
|
||||
@@ -17,8 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from ipapython.dn import DN
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
@@ -49,7 +47,7 @@ class TestSimpleReplication(IntegrationTest):
|
||||
user_dn = DN(('uid', login), ('cn', 'users'), ('cn', 'accounts'),
|
||||
basedn)
|
||||
entry = ldap.get_entry(user_dn)
|
||||
print(entry)
|
||||
print entry
|
||||
assert entry.dn == user_dn
|
||||
assert entry['uid'] == [login]
|
||||
|
||||
|
||||
@@ -17,11 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration.tasks import clear_sssd_cache, modify_sssd_conf
|
||||
from ipatests.test_integration import util
|
||||
from ipatests.test_integration.tasks import clear_sssd_cache
|
||||
|
||||
|
||||
class TestSudo(IntegrationTest):
|
||||
@@ -33,8 +30,8 @@ class TestSudo(IntegrationTest):
|
||||
topology = 'line'
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
super(TestSudo, cls).install(mh)
|
||||
def setup_class(cls):
|
||||
super(TestSudo, cls).setup_class()
|
||||
|
||||
cls.client = cls.clients[0]
|
||||
|
||||
@@ -73,10 +70,10 @@ class TestSudo(IntegrationTest):
|
||||
'localuser'])
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls, mh):
|
||||
def teardown_class(cls):
|
||||
cls.client.run_command(['groupdel', 'localgroup'], raiseonerr=False)
|
||||
cls.client.run_command(['userdel', 'localuser'], raiseonerr=False)
|
||||
super(TestSudo, cls).uninstall(mh)
|
||||
super(TestSudo, cls).teardown_class()
|
||||
|
||||
def list_sudo_commands(self, user, raiseonerr=False, verbose=False):
|
||||
clear_sssd_cache(self.client)
|
||||
@@ -272,38 +269,13 @@ class TestSudo(IntegrationTest):
|
||||
'--hostgroups', 'testhostgroup'])
|
||||
|
||||
def test_sudo_rule_restricted_to_one_hostmask_setup(self):
|
||||
# We need to detect the hostmask first
|
||||
full_ip = util.get_host_ip_with_hostmask(self.client)
|
||||
|
||||
# Make a note for the next test, which needs to be skipped
|
||||
# if hostmask detection failed
|
||||
self.__class__.skip_hostmask_based = False
|
||||
|
||||
if not full_ip:
|
||||
self.__class__.skip_hostmask_based = True
|
||||
raise pytest.skip("Hostmask could not be detected")
|
||||
|
||||
# Add the client's /24 hostmask to the rule
|
||||
ip = self.client.ip
|
||||
self.master.run_command(['ipa', '-n', 'sudorule-add-host',
|
||||
'testrule',
|
||||
'--hostmask', full_ip])
|
||||
|
||||
# SSSD >= 1.13.3-3 uses native IPA schema instead of compat entries to
|
||||
# pull in sudoers. Since native schema does not (yet) support
|
||||
# hostmasks, we need to point ldap_sudo_search_base to the old schema
|
||||
domain = self.client.domain
|
||||
modify_sssd_conf(
|
||||
self.client,
|
||||
domain.name,
|
||||
{
|
||||
'ldap_sudo_search_base': 'ou=sudoers,{}'.format(domain.basedn)
|
||||
},
|
||||
provider_subtype='sudo'
|
||||
)
|
||||
'--hostmask', '%s/24' % ip])
|
||||
|
||||
def test_sudo_rule_restricted_to_one_hostmask(self):
|
||||
if self.__class__.skip_hostmask_based:
|
||||
raise pytest.skip("Hostmask could not be detected")
|
||||
|
||||
result1 = self.list_sudo_commands("testuser1")
|
||||
assert "(ALL : ALL) NOPASSWD: ALL" in result1.stdout_text
|
||||
|
||||
@@ -312,16 +284,11 @@ class TestSudo(IntegrationTest):
|
||||
assert result.returncode != 0
|
||||
|
||||
def test_sudo_rule_restricted_to_one_hostmask_teardown(self):
|
||||
if self.__class__.skip_hostmask_based:
|
||||
raise pytest.skip("Hostmask could not be detected")
|
||||
|
||||
# Detect the hostmask first to delete the hostmask based rule
|
||||
full_ip = util.get_host_ip_with_hostmask(self.client)
|
||||
|
||||
# Remove the client's hostmask from the rule
|
||||
# Remove the client's /24 hostmask from the rule
|
||||
ip = self.client.ip
|
||||
self.master.run_command(['ipa', '-n', 'sudorule-remove-host',
|
||||
'testrule',
|
||||
'--hostmask', full_ip])
|
||||
'--hostmask', '%s/24' % ip])
|
||||
|
||||
def test_sudo_rule_restricted_to_one_hostmask_negative_setup(self):
|
||||
# Add the master's hostmask to the rule
|
||||
@@ -341,18 +308,6 @@ class TestSudo(IntegrationTest):
|
||||
'testrule',
|
||||
'--hostmask', '%s/32' % ip])
|
||||
|
||||
# reset ldap_sudo_search_base back to the default value, the old
|
||||
# schema is not needed for the upcoming tests
|
||||
domain = self.client.domain
|
||||
modify_sssd_conf(
|
||||
self.client,
|
||||
domain.name,
|
||||
{
|
||||
'ldap_sudo_search_base': None
|
||||
},
|
||||
provider_subtype='sudo'
|
||||
)
|
||||
|
||||
def test_sudo_rule_restricted_to_one_command_setup(self):
|
||||
# Reset testrule configuration
|
||||
self.reset_rule_categories()
|
||||
|
||||
@@ -23,25 +23,23 @@ import copy
|
||||
from ipatests.test_integration import config
|
||||
from ipapython.ipautil import write_tmp_file
|
||||
from ipatests.util import assert_deepequal
|
||||
from ipalib.constants import MAX_DOMAIN_LEVEL
|
||||
|
||||
DEFAULT_OUTPUT_DICT = {
|
||||
"nis_domain": "ipatest",
|
||||
"test_dir": "/root/ipatests",
|
||||
"debug": False,
|
||||
"ad_admin_name": "Administrator",
|
||||
"ipv6": False,
|
||||
"ssh_key_filename": "~/.ssh/id_rsa",
|
||||
"ssh_username": "root",
|
||||
"root_ssh_key_filename": "~/.ssh/id_rsa",
|
||||
"admin_name": "admin",
|
||||
"ad_admin_password": "Secret123",
|
||||
"ssh_password": None,
|
||||
"root_password": None,
|
||||
"dns_forwarder": "8.8.8.8",
|
||||
"domains": [],
|
||||
"dirman_dn": "cn=Directory Manager",
|
||||
"dirman_password": "Secret123",
|
||||
"ntp_server": "ntp.clock.test",
|
||||
"admin_password": "Secret123",
|
||||
"domain_level": MAX_DOMAIN_LEVEL
|
||||
"admin_password": "Secret123"
|
||||
}
|
||||
|
||||
DEFAULT_OUTPUT_ENV = {
|
||||
@@ -59,7 +57,6 @@ DEFAULT_OUTPUT_ENV = {
|
||||
"ADADMINPW": "Secret123",
|
||||
"IPv6SETUP": "",
|
||||
"IPADEBUG": "",
|
||||
"DOMAINLVL": str(MAX_DOMAIN_LEVEL),
|
||||
}
|
||||
|
||||
DEFAULT_INPUT_ENV = {
|
||||
@@ -132,12 +129,6 @@ class CheckConfig(object):
|
||||
assert_deepequal(self.get_output_dict(), conf.to_dict())
|
||||
self.check_config(conf)
|
||||
|
||||
# Settings to override:
|
||||
extra_input_dict = {}
|
||||
extra_input_env = {}
|
||||
extra_output_dict = {}
|
||||
extra_output_env = {}
|
||||
|
||||
|
||||
class TestEmptyConfig(CheckConfig):
|
||||
extra_input_dict = {}
|
||||
|
||||
@@ -96,86 +96,3 @@ def test_topology_complete():
|
||||
(2, 3),
|
||||
]
|
||||
assert list(topo('M', [])) == []
|
||||
|
||||
|
||||
def test_topology_two_connected():
|
||||
topo = tasks.get_topo('2-connected')
|
||||
assert topo == tasks.two_connected_topo
|
||||
assert list(topo('M', [1, 2, 3, 4, 5, 6, 7, 8])) == [
|
||||
('M', 1),
|
||||
('M', 2),
|
||||
(2, 3),
|
||||
(1, 3),
|
||||
('M', 4),
|
||||
('M', 5),
|
||||
(4, 6),
|
||||
(5, 6),
|
||||
(2, 4),
|
||||
(2, 7),
|
||||
(4, 8),
|
||||
(7, 8),
|
||||
]
|
||||
assert list(topo('M', [])) == []
|
||||
|
||||
|
||||
def test_topology_double_circle_topo():
|
||||
topo = tasks.get_topo('double-circle')
|
||||
assert topo == tasks.double_circle_topo
|
||||
assert list(topo('M', list(range(1, 30)))) == [
|
||||
('M', 1),
|
||||
(1, 6),
|
||||
(1, 12),
|
||||
(6, 7),
|
||||
(7, 12),
|
||||
(7, 18),
|
||||
(12, 13),
|
||||
(13, 18),
|
||||
(13, 24),
|
||||
(18, 19),
|
||||
(19, 24),
|
||||
(19, 'M'),
|
||||
(24, 25),
|
||||
(25, 'M'),
|
||||
(25, 6),
|
||||
('M', 2),
|
||||
(2, 3),
|
||||
(2, 4),
|
||||
(2, 5),
|
||||
(3, 4),
|
||||
(3, 5),
|
||||
(4, 5),
|
||||
(1, 5),
|
||||
(6, 8),
|
||||
(8, 9),
|
||||
(8, 10),
|
||||
(8, 11),
|
||||
(9, 10),
|
||||
(9, 11),
|
||||
(10, 11),
|
||||
(7, 11),
|
||||
(12, 14),
|
||||
(14, 15),
|
||||
(14, 16),
|
||||
(14, 17),
|
||||
(15, 16),
|
||||
(15, 17),
|
||||
(16, 17),
|
||||
(13, 17),
|
||||
(18, 20),
|
||||
(20, 21),
|
||||
(20, 22),
|
||||
(20, 23),
|
||||
(21, 22),
|
||||
(21, 23),
|
||||
(22, 23),
|
||||
(19, 23),
|
||||
(24, 26),
|
||||
(26, 27),
|
||||
(26, 28),
|
||||
(26, 29),
|
||||
(27, 28),
|
||||
(27, 29),
|
||||
(28, 29),
|
||||
(25, 29),
|
||||
]
|
||||
assert list(topo('M', [])) == []
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
from ipatests.test_integration.env_config import get_global_config
|
||||
from ipalib.constants import DOMAIN_SUFFIX_NAME
|
||||
|
||||
config = get_global_config()
|
||||
reasoning = "Topology plugin disabled due to domain level 0"
|
||||
|
||||
@pytest.mark.skipif(config.domain_level == 0, reason=reasoning)
|
||||
class TestTopologyOptions(IntegrationTest):
|
||||
num_replicas = 2
|
||||
topology = 'star'
|
||||
rawsegment_re = ('Segment name: (?P<name>.*?)',
|
||||
'\s+Left node: (?P<lnode>.*?)',
|
||||
'\s+Right node: (?P<rnode>.*?)',
|
||||
'\s+Connectivity: (?P<connectivity>\S+)')
|
||||
segment_re = re.compile("\n".join(rawsegment_re))
|
||||
noentries_re = re.compile("Number of entries returned (\d+)")
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_topo(cls.topology, cls.master,
|
||||
cls.replicas[:-1],
|
||||
cls.clients)
|
||||
|
||||
def tokenize_topologies(self, command_output):
|
||||
"""
|
||||
takes an output of `ipa topologysegment-find` and returns an array of
|
||||
segment hashes
|
||||
"""
|
||||
segments = command_output.split("-----------------")[2]
|
||||
raw_segments = segments.split('\n\n')
|
||||
result = []
|
||||
for i in raw_segments:
|
||||
matched = self.segment_re.search(i)
|
||||
if matched:
|
||||
result.append({'leftnode': matched.group('lnode'),
|
||||
'rightnode': matched.group('rnode'),
|
||||
'name': matched.group('name'),
|
||||
'connectivity': matched.group('connectivity')
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def test_topology_updated_on_replica_install_remove(self):
|
||||
"""
|
||||
Install and remove a replica and make sure topology information is
|
||||
updated on all other replicas
|
||||
Testcase: http://www.freeipa.org/page/V4/Manage_replication_topology/
|
||||
Test_plan#Test_case:
|
||||
_Replication_topology_should_be_saved_in_the_LDAP_tree
|
||||
"""
|
||||
tasks.kinit_admin(self.master)
|
||||
result1 = self.master.run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
first_segment_name = "%s-to-%s" % (self.master.hostname,
|
||||
self.replicas[0].hostname)
|
||||
output1 = result1.stdout_text
|
||||
firstsegment = self.tokenize_topologies(output1)[0]
|
||||
assert(firstsegment['name'] == first_segment_name)
|
||||
assert(self.noentries_re.search(output1).group(1) == "1")
|
||||
assert(firstsegment['leftnode'] == self.master.hostname)
|
||||
assert(firstsegment['rightnode'] == self.replicas[0].hostname)
|
||||
tasks.install_replica(self.master, self.replicas[1], setup_ca=False,
|
||||
setup_dns=False)
|
||||
# We need to make sure topology information is consistent across all
|
||||
# replicas
|
||||
result2 = self.master.run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
result3 = self.replicas[0].run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
result4 = self.replicas[1].run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
segments = self.tokenize_topologies(result2.stdout_text)
|
||||
assert(len(segments) == 2)
|
||||
assert(result2.stdout_text == result3.stdout_text)
|
||||
assert(result3.stdout_text == result4.stdout_text)
|
||||
# Now let's check that uninstalling the replica will update the topology
|
||||
# info on the rest of replicas.
|
||||
tasks.uninstall_master(self.replicas[1])
|
||||
tasks.clean_replication_agreement(self.master, self.replicas[1])
|
||||
result5 = self.master.run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
assert(self.noentries_re.search(result5.stdout_text).group(1) == "1")
|
||||
|
||||
def test_add_remove_segment(self):
|
||||
"""
|
||||
Make sure a topology segment can be manually created and deleted
|
||||
with the influence on the real topology
|
||||
Testcase http://www.freeipa.org/page/V4/Manage_replication_topology/
|
||||
Test_plan#Test_case:_Basic_CRUD_test
|
||||
"""
|
||||
tasks.kinit_admin(self.master)
|
||||
# Install the second replica
|
||||
tasks.install_replica(self.master, self.replicas[1], setup_ca=False,
|
||||
setup_dns=False)
|
||||
# turn a star into a ring
|
||||
segment, err = tasks.create_segment(self.master,
|
||||
self.replicas[0],
|
||||
self.replicas[1])
|
||||
assert err == "", err
|
||||
# Make sure the new segment is shown by `ipa topologysegment-find`
|
||||
result1 = self.master.run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
assert(result1.stdout_text.find(segment['name']) > 0)
|
||||
# Remove master <-> replica2 segment and make sure that the changes get
|
||||
# there through replica1
|
||||
deleteme = "%s-to-%s" % (self.master.hostname,
|
||||
self.replicas[1].hostname)
|
||||
returncode, error = tasks.destroy_segment(self.master, deleteme)
|
||||
assert returncode == 0, error
|
||||
# make sure replica1 does not have segment that was deleted on master
|
||||
result3 = self.replicas[0].run_command(['ipa', 'topologysegment-find',
|
||||
DOMAIN_SUFFIX_NAME])
|
||||
assert(result3.stdout_text.find(deleteme) < 0)
|
||||
# Create test data on master and make sure it gets all the way down to
|
||||
# replica2 through replica1
|
||||
self.master.run_command(['ipa', 'user-add', 'someuser',
|
||||
'--first', 'test',
|
||||
'--last', 'user'])
|
||||
time.sleep(60) # replication requires some time
|
||||
users_on_replica2 = self.replicas[1].run_command(['ipa',
|
||||
'user-find'])
|
||||
assert(users_on_replica2.find('someuser') > 0)
|
||||
# We end up having a line topology: master <-> replica1 <-> replica2
|
||||
|
||||
def test_remove_the_only_connection(self):
|
||||
"""
|
||||
Testcase: http://www.freeipa.org/page/V4/Manage_replication_topology/
|
||||
Test_plan#Test_case:
|
||||
_Removal_of_a_topology_segment_is_allowed_only_if_there_is_at_least_one_more_segment_connecting_the_given_replica
|
||||
"""
|
||||
text = "Removal of Segment disconnects topology"
|
||||
error1 = "The system should not have let you remove the segment"
|
||||
error2 = "Wrong error message thrown during segment removal: \"%s\""
|
||||
replicas = (self.replicas[0].hostname, self.replicas[1].hostname)
|
||||
|
||||
returncode, error = tasks.destroy_segment(self.master, "%s-to-%s" % replicas)
|
||||
assert returncode != 0, error1
|
||||
assert error.count(text) == 1, error2 % error
|
||||
newseg, err = tasks.create_segment(self.master,
|
||||
self.master,
|
||||
self.replicas[1])
|
||||
assert err == "", err
|
||||
returncode, error = tasks.destroy_segment(self.master, "%s-to-%s" % replicas)
|
||||
assert returncode == 0, error
|
||||
@@ -33,8 +33,8 @@ class ADTrustBase(IntegrationTest):
|
||||
optional_extra_roles = ['ad_subdomain']
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
super(ADTrustBase, cls).install(mh)
|
||||
def install(cls):
|
||||
super(ADTrustBase, cls).install()
|
||||
cls.ad = cls.ad_domains[0].ads[0]
|
||||
cls.install_adtrust()
|
||||
cls.check_sid_generation()
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import time
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.test_integration import tasks
|
||||
|
||||
WAIT_AFTER_ARCHIVE = 30 # give some time to replication
|
||||
|
||||
|
||||
class TestInstallKRA(IntegrationTest):
|
||||
"""
|
||||
Test if vault feature behaves as expected, when KRA is installed or not
|
||||
installed on replica
|
||||
"""
|
||||
num_replicas = 1
|
||||
topology = 'star'
|
||||
|
||||
vault_password = "password"
|
||||
vault_data = "SSBsb3ZlIENJIHRlc3RzCg=="
|
||||
vault_name_master = "ci_test_vault_master"
|
||||
vault_name_master2 = "ci_test_vault_master2"
|
||||
vault_name_master3 = "ci_test_vault_master3"
|
||||
vault_name_replica_without_KRA = "ci_test_vault_replica_without_kra"
|
||||
vault_name_replica_with_KRA = "ci_test_vault_replica_with_kra"
|
||||
vault_name_replica_KRA_uninstalled = "ci_test_vault_replica_KRA_uninstalled"
|
||||
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_kra=True)
|
||||
# do not install KRA on replica, it is part of test
|
||||
tasks.install_replica(cls.master, cls.replicas[0], setup_kra=False)
|
||||
|
||||
def _retrieve_secret(self, vault_names=[]):
|
||||
# try to retrieve secret from vault on both master and replica
|
||||
for vault_name in vault_names:
|
||||
self.master.run_command([
|
||||
"ipa", "vault-retrieve",
|
||||
vault_name,
|
||||
"--password", self.vault_password,
|
||||
])
|
||||
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-retrieve",
|
||||
vault_name,
|
||||
"--password", self.vault_password,
|
||||
])
|
||||
|
||||
def test_create_and_retrieve_vault_master(self):
|
||||
# create vault
|
||||
self.master.run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name_master,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.master.run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name_master,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
time.sleep(WAIT_AFTER_ARCHIVE)
|
||||
|
||||
self._retrieve_secret([self.vault_name_master])
|
||||
|
||||
def test_create_and_retrieve_vault_replica_without_kra(self):
|
||||
# create vault
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name_replica_without_KRA,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name_replica_without_KRA,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
time.sleep(WAIT_AFTER_ARCHIVE)
|
||||
|
||||
self._retrieve_secret([self.vault_name_replica_without_KRA])
|
||||
|
||||
def test_create_and_retrieve_vault_replica_with_kra(self):
|
||||
|
||||
# install KRA on replica
|
||||
tasks.install_kra(self.replicas[0], first_instance=False)
|
||||
|
||||
# create vault
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name_replica_with_KRA,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name_replica_with_KRA,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
time.sleep(WAIT_AFTER_ARCHIVE)
|
||||
|
||||
self._retrieve_secret([self.vault_name_replica_with_KRA])
|
||||
|
||||
################# master #################
|
||||
# test master again after KRA was installed on replica
|
||||
# create vault
|
||||
self.master.run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name_master2,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.master.run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name_master2,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
time.sleep(WAIT_AFTER_ARCHIVE)
|
||||
|
||||
self._retrieve_secret([self.vault_name_master2])
|
||||
|
||||
################ old vaults ###############
|
||||
# test if old vaults are still accessible
|
||||
self._retrieve_secret([
|
||||
self.vault_name_master,
|
||||
self.vault_name_replica_without_KRA,
|
||||
])
|
||||
|
||||
|
||||
def test_create_and_retrieve_vault_after_kra_uninstall_on_replica(self):
|
||||
# uninstall KRA on replica
|
||||
self.replicas[0].run_command([
|
||||
"ipa-kra-install",
|
||||
"-U",
|
||||
"--uninstall",
|
||||
])
|
||||
|
||||
# create vault
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name_replica_KRA_uninstalled,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.replicas[0].run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name_replica_KRA_uninstalled,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
time.sleep(WAIT_AFTER_ARCHIVE)
|
||||
|
||||
self._retrieve_secret([self.vault_name_replica_KRA_uninstalled])
|
||||
|
||||
################# master #################
|
||||
# test master again after KRA was uninstalled on replica
|
||||
# create vault
|
||||
self.master.run_command([
|
||||
"ipa", "vault-add",
|
||||
self.vault_name_master3,
|
||||
"--password", self.vault_password,
|
||||
"--type", "symmetric",
|
||||
])
|
||||
|
||||
# archive secret
|
||||
self.master.run_command([
|
||||
"ipa", "vault-archive",
|
||||
self.vault_name_master3,
|
||||
"--password", self.vault_password,
|
||||
"--data", self.vault_data,
|
||||
])
|
||||
time.sleep(WAIT_AFTER_ARCHIVE)
|
||||
|
||||
self._retrieve_secret([self.vault_name_master3,])
|
||||
|
||||
################ old vaults ###############
|
||||
# test if old vaults are still accessible
|
||||
self._retrieve_secret([
|
||||
self.vault_name_master,
|
||||
self.vault_name_master2,
|
||||
self.vault_name_replica_without_KRA,
|
||||
self.vault_name_replica_with_KRA,
|
||||
])
|
||||
443
ipatests/test_integration/transport.py
Normal file
443
ipatests/test_integration/transport.py
Normal file
@@ -0,0 +1,443 @@
|
||||
# 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
|
||||
@@ -18,7 +18,16 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import re
|
||||
|
||||
|
||||
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,
|
||||
@@ -57,21 +66,5 @@ def run_repeatedly(host, command, assert_zero_rc=True, test=None,
|
||||
raise AssertionError("Command: {cmd} repeatedly failed {times} times, "
|
||||
"exceeding the timeout of {timeout} seconds."
|
||||
.format(cmd=' '.join(command),
|
||||
times=timeout // time_step,
|
||||
times=timeout / time_step,
|
||||
timeout=timeout))
|
||||
|
||||
|
||||
def get_host_ip_with_hostmask(host):
|
||||
"""
|
||||
Detects the IP of the host including the hostmask.
|
||||
|
||||
Returns None if the IP could not be detected.
|
||||
"""
|
||||
|
||||
ip = host.ip
|
||||
result = host.run_command(['ip', 'addr'])
|
||||
full_ip_regex = r'(?P<full_ip>%s/\d{1,2}) ' % re.escape(ip)
|
||||
match = re.search(full_ip_regex, result.stdout_text)
|
||||
|
||||
if match:
|
||||
return match.group('full_ip')
|
||||
|
||||
Reference in New Issue
Block a user