Import Upstream version 4.12.4
This commit is contained in:
@@ -29,7 +29,7 @@ def pytest_generate_tests(metafunc):
|
||||
if callable(test):
|
||||
description = '%s: %s' % (
|
||||
str(i).zfill(4),
|
||||
test.__name__, # test is not a dict. pylint: disable=E1103
|
||||
test.__name__,
|
||||
)
|
||||
else:
|
||||
description = '%s: %s: %s' % (str(i).zfill(4),
|
||||
|
||||
@@ -28,12 +28,15 @@ import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import re
|
||||
import functools
|
||||
|
||||
import pytest
|
||||
from pytest_multihost import make_multihost_fixture
|
||||
|
||||
from ipapython import ipautil
|
||||
from ipaplatform.paths import paths
|
||||
from ipaplatform.constants import constants
|
||||
from . import fips
|
||||
from .config import Config
|
||||
from .env_config import get_global_config
|
||||
from . import tasks
|
||||
@@ -41,15 +44,19 @@ from . import tasks
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CLASS_LOGFILES = [
|
||||
# BIND logs
|
||||
os.path.join(paths.NAMED_VAR_DIR, constants.NAMED_DATA_DIR),
|
||||
# dirsrv logs
|
||||
paths.VAR_LOG_DIRSRV,
|
||||
# IPA install logs
|
||||
paths.IPASERVER_INSTALL_LOG,
|
||||
paths.IPASERVER_ADTRUST_INSTALL_LOG,
|
||||
paths.IPASERVER_DNS_INSTALL_LOG,
|
||||
paths.IPASERVER_KRA_INSTALL_LOG,
|
||||
paths.IPACLIENT_INSTALL_LOG,
|
||||
paths.IPAREPLICA_INSTALL_LOG,
|
||||
paths.IPAREPLICA_CONNCHECK_LOG,
|
||||
paths.IPAREPLICA_CA_INSTALL_LOG,
|
||||
paths.IPASERVER_KRA_INSTALL_LOG,
|
||||
paths.IPA_CUSTODIA_AUDIT_LOG,
|
||||
paths.IPACLIENTSAMBA_INSTALL_LOG,
|
||||
paths.IPACLIENTSAMBA_UNINSTALL_LOG,
|
||||
@@ -62,6 +69,8 @@ CLASS_LOGFILES = [
|
||||
# IPA backup and restore logs
|
||||
paths.IPARESTORE_LOG,
|
||||
paths.IPABACKUP_LOG,
|
||||
# EPN log
|
||||
paths.IPAEPN_LOG,
|
||||
# kerberos related logs
|
||||
paths.KADMIND_LOG,
|
||||
paths.KRB5KDC_LOG,
|
||||
@@ -69,6 +78,12 @@ CLASS_LOGFILES = [
|
||||
paths.VAR_LOG_HTTPD_DIR,
|
||||
# dogtag logs
|
||||
paths.VAR_LOG_PKI_DIR,
|
||||
# dogtag conf
|
||||
paths.PKI_TOMCAT_SERVER_XML,
|
||||
paths.PKI_TOMCAT + "/ca/CS.cfg",
|
||||
paths.PKI_TOMCAT + "/kra/CS.cfg",
|
||||
paths.PKI_TOMCAT_ALIAS_DIR,
|
||||
paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT,
|
||||
# selinux logs
|
||||
paths.VAR_LOG_AUDIT,
|
||||
# sssd
|
||||
@@ -76,6 +91,15 @@ CLASS_LOGFILES = [
|
||||
# system
|
||||
paths.RESOLV_CONF,
|
||||
paths.HOSTS,
|
||||
# IPA renewal lock
|
||||
paths.IPA_RENEWAL_LOCK,
|
||||
paths.LETS_ENCRYPT_LOG,
|
||||
# resolvers management
|
||||
paths.NETWORK_MANAGER_CONFIG,
|
||||
paths.NETWORK_MANAGER_CONFIG_DIR,
|
||||
paths.SYSTEMD_RESOLVED_CONF,
|
||||
paths.SYSTEMD_RESOLVED_CONF_DIR,
|
||||
'/var/log/samba',
|
||||
]
|
||||
|
||||
|
||||
@@ -195,8 +219,17 @@ def collect_logs(name, logs_dict, logfile_dir=None, beakerlib_plugin=None):
|
||||
tmpname = cmd.stdout_text.strip()
|
||||
# Tar up the logs on the remote server
|
||||
cmd = host.run_command(
|
||||
['tar', 'cJvf', tmpname, '--ignore-failed-read'] + logs,
|
||||
log_stdout=False, raiseonerr=False)
|
||||
[
|
||||
"tar",
|
||||
"cJvf",
|
||||
tmpname,
|
||||
"--ignore-failed-read",
|
||||
"--warning=no-failed-read",
|
||||
"--dereference",
|
||||
] + logs,
|
||||
log_stdout=False,
|
||||
raiseonerr=False,
|
||||
)
|
||||
if cmd.returncode:
|
||||
logger.warning('Could not collect all requested logs')
|
||||
# fetch tar file
|
||||
@@ -262,8 +295,8 @@ class IntegrationLogs:
|
||||
def init_method_logs(self):
|
||||
"""Initilize method logs with the class ones"""
|
||||
self._method_logs = {}
|
||||
for k in self._class_logs:
|
||||
self._method_logs[k] = list(self._class_logs[k])
|
||||
for host, logs in self._class_logs.items():
|
||||
self._method_logs[host] = list(logs)
|
||||
|
||||
def collect_class_log(self, host, filename):
|
||||
"""Add class scope log
|
||||
@@ -470,3 +503,18 @@ def del_compat_attrs(cls):
|
||||
del cls.ad_subdomains
|
||||
del cls.ad_treedomains
|
||||
del cls.ad_domains
|
||||
|
||||
|
||||
def skip_if_fips(reason='Not supported in FIPS mode', host='master'):
|
||||
if callable(reason):
|
||||
raise TypeError('Invalid decorator usage, add "()"')
|
||||
|
||||
def decorator(test_method):
|
||||
@functools.wraps(test_method)
|
||||
def wrapper(instance, *args, **kwargs):
|
||||
if fips.is_fips_enabled(getattr(instance, host)):
|
||||
pytest.skip(reason)
|
||||
else:
|
||||
test_method(instance, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@@ -44,6 +44,9 @@ class Config(pytest_multihost.config.Config):
|
||||
'domain_level',
|
||||
'log_journal_since',
|
||||
'fips_mode',
|
||||
'token_name',
|
||||
'token_password',
|
||||
'token_library',
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -69,6 +72,9 @@ class Config(pytest_multihost.config.Config):
|
||||
if self.domain_level is None:
|
||||
self.domain_level = MAX_DOMAIN_LEVEL
|
||||
self.fips_mode = kwargs.get('fips_mode', False)
|
||||
self.token_name = kwargs.get('token_name', None)
|
||||
self.token_password = kwargs.get('token_password', None)
|
||||
self.token_library = kwargs.get('token_library', None)
|
||||
|
||||
def get_domain_class(self):
|
||||
return Domain
|
||||
@@ -100,11 +106,11 @@ class Config(pytest_multihost.config.Config):
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env):
|
||||
from ipatests.pytest_ipa.integration.env_config import config_from_env
|
||||
from .env_config import config_from_env # pylint: disable=cyclic-import
|
||||
return config_from_env(env)
|
||||
|
||||
def to_env(self, **kwargs):
|
||||
from ipatests.pytest_ipa.integration.env_config import config_to_env
|
||||
from .env_config import config_to_env # pylint: disable=cyclic-import
|
||||
return config_to_env(self, **kwargs)
|
||||
|
||||
def filter(self, descriptions):
|
||||
@@ -156,7 +162,7 @@ class Domain(pytest_multihost.config.Domain):
|
||||
raise LookupError(self.type)
|
||||
|
||||
def get_host_class(self, host_dict):
|
||||
from ipatests.pytest_ipa.integration.host import Host, WinHost
|
||||
from .host import Host, WinHost # pylint: disable=cyclic-import
|
||||
|
||||
if self.is_ipa_type:
|
||||
return Host
|
||||
|
||||
183
ipatests/pytest_ipa/integration/create_bridge.py
Normal file
183
ipatests/pytest_ipa/integration/create_bridge.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from ipatests.pytest_ipa.integration import tasks
|
||||
|
||||
|
||||
def setup_scim_server(host, version="main"):
|
||||
dir = "/opt/ipa-tuura"
|
||||
password = host.config.admin_password
|
||||
tasks.install_packages(host, ["unzip", "java-11-openjdk-headless",
|
||||
"openssl", "maven", "wget", "git",
|
||||
"firefox", "xorg-x11-server-Xvfb",
|
||||
"python3-pip"])
|
||||
|
||||
# Download ipa-tuura project
|
||||
url = "https://github.com/freeipa/ipa-tuura"
|
||||
host.run_command(["git", "clone", "-b", f"{version}", f"{url}", f"{dir}"])
|
||||
|
||||
# Prepare SSSD config
|
||||
host.run_command(["python", "./prepare_sssd.py"],
|
||||
cwd=f"{dir}/src/install")
|
||||
|
||||
# Get keytab for scim bridge service
|
||||
master = host.domain.hosts_by_role("master")[0].hostname
|
||||
princ = f"admin@{host.domain.realm}"
|
||||
ktfile = "/root/scim.keytab"
|
||||
sendpass = f"{password}\n{password}"
|
||||
tasks.kdestroy_all(host)
|
||||
tasks.kinit_admin(host)
|
||||
host.run_command(["ipa-getkeytab", "-s", master, "-p", princ,
|
||||
"-P", "-k", ktfile], stdin_text=sendpass)
|
||||
host.run_command(["kinit", "-k", "-t", ktfile, princ])
|
||||
|
||||
# Install django requirements
|
||||
django_reqs = f"{dir}/src/install/requirements.txt"
|
||||
host.run_command(["pip", "install", "-r", f"{django_reqs}"])
|
||||
|
||||
# Prepare models and database
|
||||
host.run_command(["python", "manage.py", "makemigrations", "scim"],
|
||||
cwd=f"{dir}/src/ipa-tuura")
|
||||
host.run_command(["python", "manage.py", "migrate"],
|
||||
cwd=f"{dir}/src/ipa-tuura")
|
||||
|
||||
# Add necessary admin vars to bashrc
|
||||
env_vars = textwrap.dedent(f"""
|
||||
export DJANGO_SUPERUSER_PASSWORD={password}
|
||||
export DJANGO_SUPERUSER_USERNAME=scim
|
||||
export DJANGO_SUPERUSER_EMAIL=scim@{host.domain.name}
|
||||
""")
|
||||
|
||||
tasks.backup_file(host, '/etc/bashrc')
|
||||
content = host.get_file_contents('/etc/bashrc', encoding='utf-8')
|
||||
new_content = content + f"\n{env_vars}"
|
||||
host.put_file_contents('/etc/bashrc', new_content)
|
||||
host.run_command(['bash'])
|
||||
|
||||
# Create django admin
|
||||
host.run_command(["python", "manage.py", "createsuperuser",
|
||||
"--scim_username", "scim", "--noinput"],
|
||||
cwd=f"{dir}/src/ipa-tuura")
|
||||
|
||||
# Open allowed hosts to any for testing
|
||||
regex = r"^(ALLOWED_HOSTS) .*$"
|
||||
replace = r"\1 = ['*']"
|
||||
settings_file = f"{dir}/src/ipa-tuura/root/settings.py"
|
||||
settings = host.get_file_contents(settings_file, encoding='utf-8')
|
||||
new_settings = re.sub(regex, replace, settings, flags=re.MULTILINE)
|
||||
host.put_file_contents(settings_file, new_settings)
|
||||
|
||||
# Setup keycloak service and config files
|
||||
contents = textwrap.dedent(f"""
|
||||
DJANGO_SUPERUSER_USERNAME=scim
|
||||
DJANGO_SUPERUSER_PASSWORD={password}
|
||||
DJANGO_SUPERUSER_EMAIL=scim@{host.domain.name}
|
||||
""")
|
||||
host.put_file_contents("/etc/sysconfig/scim", contents)
|
||||
|
||||
manage = f"{dir}/src/ipa-tuura/manage.py"
|
||||
contents = textwrap.dedent(f"""
|
||||
[Unit]
|
||||
Description=SCIMv2 Bridge Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory={dir}/src/ipa-tuura/
|
||||
EnvironmentFile=/etc/sysconfig/scim
|
||||
# Fix this later
|
||||
# User=scim
|
||||
# Group=scim
|
||||
ExecStart=/usr/bin/python {manage} runserver 0.0.0.0:8000
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=600
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
""")
|
||||
host.put_file_contents("/etc/systemd/system/scim.service", contents)
|
||||
host.run_command(["systemctl", "daemon-reload"])
|
||||
host.run_command(["systemctl", "start", "scim"])
|
||||
|
||||
|
||||
def setup_keycloak_scim_plugin(host, bridge_server):
|
||||
dir = "/opt/keycloak"
|
||||
password = host.config.admin_password
|
||||
|
||||
# Install needed packages
|
||||
tasks.install_packages(host, ["unzip", "java-11-openjdk-headless",
|
||||
"openssl", "maven"])
|
||||
|
||||
# Add necessary admin vars to bashrc
|
||||
env_vars = textwrap.dedent(f"""
|
||||
export KEYCLOAK_PATH={dir}
|
||||
""")
|
||||
|
||||
content = host.get_file_contents('/etc/bashrc', encoding='utf-8')
|
||||
new_content = content + f"\n{env_vars}"
|
||||
host.put_file_contents('/etc/bashrc', new_content)
|
||||
host.run_command(['bash'])
|
||||
|
||||
# Download keycloak plugin
|
||||
zipfile = "scim-keycloak-user-storage-spi/archive/refs/tags/0.1.zip"
|
||||
url = f"https://github.com/justin-stephenson/{zipfile}"
|
||||
dest = "/tmp/keycloak-scim-plugin.zip"
|
||||
host.run_command(["wget", "-O", dest, url])
|
||||
|
||||
# Unzip keycloak plugin
|
||||
host.run_command(["unzip", dest, "-d", "/tmp"])
|
||||
|
||||
# Install plugin
|
||||
host.run_command(["./redeploy-plugin.sh"],
|
||||
cwd="/tmp/scim-keycloak-user-storage-spi-0.1")
|
||||
|
||||
# Fix ownership of plugin files
|
||||
host.run_command(["chown", "-R", "keycloak:keycloak", dir])
|
||||
|
||||
# Restore SELinux contexts
|
||||
host.run_command(["restorecon", "-R", f"{dir}"])
|
||||
|
||||
# Rerun Keycloak build step and restart to pickup plugin
|
||||
# This relies on the KC_* vars set in /etc/bashrc from create_keycloak.py
|
||||
host.run_command(['su', '-', 'keycloak', '-c',
|
||||
'/opt/keycloak/bin/kc.sh build'])
|
||||
host.run_command(["systemctl", "restart", "keycloak"])
|
||||
host.run_command(["/opt/keycloak/bin/kc.sh", "show-config"])
|
||||
|
||||
# Login to keycloak as admin
|
||||
kcadmin_sh = "/opt/keycloak/bin/kcadm.sh"
|
||||
kcadmin = [kcadmin_sh, "config", "credentials", "--server",
|
||||
f"https://{host.hostname}:8443/auth/",
|
||||
"--realm", "master", "--user", "admin",
|
||||
"--password", password]
|
||||
tasks.run_repeatedly(host, kcadmin, timeout=60)
|
||||
|
||||
# Configure SCIM User Storage to point to Bridge server
|
||||
provider_type = "org.keycloak.storage.UserStorageProvider"
|
||||
host.run_command([kcadmin_sh, "create", "components",
|
||||
"-r", "master",
|
||||
"-s", "name=scimprov",
|
||||
"-s", "providerId=scim",
|
||||
"-s", f"providerType={provider_type}",
|
||||
"-s", "parentId=master",
|
||||
"-s", f'config.scimurl=["{bridge_server}:8000"]',
|
||||
"-s", 'config.loginusername=["scim"]',
|
||||
"-s", f'config.loginpassword=["{password}"]'])
|
||||
|
||||
|
||||
def uninstall_scim_server(host):
|
||||
host.run_command(["systemctl", "stop", "scim"], raiseonerr=False)
|
||||
host.run_command(["rm", "-rf", "/opt/ipa-tuura",
|
||||
"/etc/sysconfig/scim",
|
||||
"/etc/systemd/system/scim.service",
|
||||
"/tmp/scim-keycloak-user-storage-spi-0.1",
|
||||
"/tmp/keycloak-scim-plugin.zip",
|
||||
"/root/scim.keytab"])
|
||||
host.run_command(["systemctl", "daemon-reload"])
|
||||
tasks.restore_files(host)
|
||||
|
||||
|
||||
def uninstall_scim_plugin(host):
|
||||
host.run_command(["rm", "-rf",
|
||||
"/tmp/scim-keycloak-user-storage-spi-0.1",
|
||||
"/tmp/keycloak-scim-plugin.zip"])
|
||||
@@ -102,7 +102,7 @@ class KRB5PrincipalName(univ.Sequence):
|
||||
|
||||
|
||||
def profile_ca(builder, ca_nick, ca):
|
||||
now = datetime.datetime.utcnow()
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
|
||||
builder = builder.not_valid_before(now)
|
||||
builder = builder.not_valid_after(now + 10 * YEAR)
|
||||
@@ -174,7 +174,7 @@ def profile_ca(builder, ca_nick, ca):
|
||||
def profile_server(builder, ca_nick, ca,
|
||||
warp=datetime.timedelta(days=0), dns_name=None,
|
||||
badusage=False, wildcard=False):
|
||||
now = datetime.datetime.utcnow() + warp
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc) + warp
|
||||
|
||||
builder = builder.not_valid_before(now)
|
||||
builder = builder.not_valid_after(now + YEAR)
|
||||
@@ -231,7 +231,7 @@ def profile_server(builder, ca_nick, ca,
|
||||
def profile_kdc(builder, ca_nick, ca,
|
||||
warp=datetime.timedelta(days=0), dns_name=None,
|
||||
badusage=False):
|
||||
now = datetime.datetime.utcnow() + warp
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc) + warp
|
||||
|
||||
builder = builder.not_valid_before(now)
|
||||
builder = builder.not_valid_after(now + YEAR)
|
||||
@@ -347,7 +347,7 @@ def gen_cert(profile, nick_base, subject, ca=None, **kwargs):
|
||||
|
||||
|
||||
def revoke_cert(ca, serial):
|
||||
now = datetime.datetime.utcnow()
|
||||
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
|
||||
crl_builder = x509.CertificateRevocationListBuilder()
|
||||
crl_builder = crl_builder.issuer_name(ca.cert.subject)
|
||||
@@ -570,7 +570,7 @@ def create_pki():
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, server2)
|
||||
])
|
||||
)
|
||||
ca1 = gen_subtree(u'ca1', u'Example Organization')
|
||||
ca1 = gen_subtree(u'ca1', u'Example Organization Espa\xf1a')
|
||||
gen_subtree(u'subca', u'Subsidiary Example Organization', ca1)
|
||||
gen_subtree(u'ca2', u'Other Example Organization')
|
||||
ca3 = gen_subtree(u'ca3', u'Unknown Organization')
|
||||
|
||||
178
ipatests/pytest_ipa/integration/create_keycloak.py
Normal file
178
ipatests/pytest_ipa/integration/create_keycloak.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import os
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipatests.pytest_ipa.integration import tasks
|
||||
|
||||
|
||||
def setup_keycloakserver(host, version='17.0.0'):
|
||||
dir = "/opt/keycloak"
|
||||
password = host.config.admin_password
|
||||
tasks.install_packages(host, ["unzip", "java-11-openjdk-headless",
|
||||
"openssl", "maven", "wget",
|
||||
"firefox", "xorg-x11-server-Xvfb"])
|
||||
# add keycloak system user/group and folder
|
||||
url = "https://github.com/keycloak/keycloak/releases/download/{0}/keycloak-{0}.zip".format(version) # noqa: E501
|
||||
host.run_command(["wget", url, "-O", "{0}-{1}.zip".format(dir, version)])
|
||||
host.run_command(
|
||||
["unzip", "{0}-{1}.zip".format(dir, version), "-d", "/opt/"]
|
||||
)
|
||||
host.run_command(["mv", "{0}-{1}".format(dir, version), dir])
|
||||
host.run_command(["groupadd", "keycloak"])
|
||||
host.run_command(
|
||||
["useradd", "-r", "-g", "keycloak", "-d", dir, "keycloak"]
|
||||
)
|
||||
host.run_command(["chown", "-R", "keycloak:", dir])
|
||||
host.run_command(["chmod", "o+x", "{0}/bin/".format(dir)])
|
||||
host.run_command(["restorecon", "-R", dir])
|
||||
|
||||
# setup TLS certificate using IPA CA
|
||||
host.run_command(["kinit", "-k"])
|
||||
host.run_command(["ipa", "service-add", "HTTP/{0}".format(host.hostname)])
|
||||
|
||||
key = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.key")
|
||||
crt = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.crt")
|
||||
keystore = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.store")
|
||||
|
||||
host.run_command(["ipa-getcert", "request", "-K",
|
||||
"HTTP/{0}".format(host.hostname),
|
||||
"-D", host.hostname, "-o", "keycloak",
|
||||
"-O", "keycloak", "-m", "0600",
|
||||
"-M", "0644",
|
||||
"-k", key, "-f", crt, "-w"])
|
||||
host.run_command(["keytool", "-import", "-keystore", keystore,
|
||||
"-file", "/etc/ipa/ca.crt",
|
||||
"-alias", "ipa_ca",
|
||||
"-trustcacerts", "-storepass", password, "-noprompt"])
|
||||
host.run_command(["chown", "keycloak:keycloak", keystore])
|
||||
|
||||
# Setup keycloak service and config files
|
||||
contents = textwrap.dedent("""
|
||||
KEYCLOAK_ADMIN=admin
|
||||
KEYCLOAK_ADMIN_PASSWORD={admin_pswd}
|
||||
KC_HOSTNAME={host}:8443
|
||||
KC_HTTPS_CERTIFICATE_FILE={crt}
|
||||
KC_HTTPS_CERTIFICATE_KEY_FILE={key}
|
||||
KC_HTTPS_TRUST_STORE_FILE={store}
|
||||
KC_HTTPS_TRUST_STORE_PASSWORD={store_pswd}
|
||||
KC_HTTP_RELATIVE_PATH=/auth
|
||||
""").format(admin_pswd=password, host=host.hostname, crt=crt, key=key,
|
||||
store=keystore, store_pswd=password)
|
||||
host.put_file_contents("/etc/sysconfig/keycloak", contents)
|
||||
|
||||
contents = textwrap.dedent("""
|
||||
[Unit]
|
||||
Description=Keycloak Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
EnvironmentFile=/etc/sysconfig/keycloak
|
||||
|
||||
User=keycloak
|
||||
Group=keycloak
|
||||
ExecStart=/opt/keycloak/bin/kc.sh start
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=600
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
""")
|
||||
host.put_file_contents("/etc/systemd/system/keycloak.service", contents)
|
||||
host.run_command(["systemctl", "daemon-reload"])
|
||||
|
||||
# Run build stage first
|
||||
env_vars = textwrap.dedent("""
|
||||
export KEYCLOAK_ADMIN=admin
|
||||
export KC_HOSTNAME={hostname}:8443
|
||||
export KC_HTTPS_CERTIFICATE_FILE=/etc/pki/tls/certs/keycloak.crt
|
||||
export KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/pki/tls/private/keycloak.key
|
||||
export KC_HTTPS_TRUST_STORE_FILE=/etc/pki/tls/private/keycloak.store
|
||||
export KC_HTTPS_TRUST_STORE_PASSWORD={STORE_PASS}
|
||||
export KEYCLOAK_ADMIN_PASSWORD={ADMIN_PASS}
|
||||
export KC_HTTP_RELATIVE_PATH=/auth
|
||||
""").format(hostname=host.hostname, STORE_PASS=password,
|
||||
ADMIN_PASS=password)
|
||||
|
||||
tasks.backup_file(host, '/etc/bashrc')
|
||||
content = host.get_file_contents('/etc/bashrc',
|
||||
encoding='utf-8')
|
||||
new_content = content + "\n{}".format(env_vars)
|
||||
host.put_file_contents('/etc/bashrc', new_content)
|
||||
host.run_command(['bash'])
|
||||
host.run_command(
|
||||
['su', '-', 'keycloak', '-c', '/opt/keycloak/bin/kc.sh build'])
|
||||
host.run_command(["systemctl", "start", "keycloak"])
|
||||
host.run_command(["/opt/keycloak/bin/kc.sh", "show-config"])
|
||||
|
||||
# Setup keycloak for use:
|
||||
kcadmin_sh = "/opt/keycloak/bin/kcadm.sh"
|
||||
|
||||
host.run_command([kcadmin_sh, "config", "truststore",
|
||||
"--trustpass", password, keystore])
|
||||
kcadmin = [kcadmin_sh, "config", "credentials", "--server",
|
||||
"https://{0}:8443/auth/".format(host.hostname),
|
||||
"--realm", "master", "--user", "admin",
|
||||
"--password", password
|
||||
]
|
||||
tasks.run_repeatedly(
|
||||
host, kcadmin, timeout=60)
|
||||
host.run_command(
|
||||
[kcadmin_sh, "create", "users", "-r", "master",
|
||||
"-s", "username=testuser1", "-s", "enabled=true",
|
||||
"-s", "email=testuser1@ipa.test"]
|
||||
)
|
||||
host.run_command(
|
||||
[kcadmin_sh, "set-password", "-r", "master",
|
||||
"--username", "testuser1", "--new-password", password]
|
||||
)
|
||||
|
||||
|
||||
def setup_keycloak_client(host):
|
||||
password = host.config.admin_password
|
||||
host.run_command(["/opt/keycloak/bin/kcreg.sh",
|
||||
"config", "credentials", "--server",
|
||||
"https://{0}:8443/auth/".format(host.hostname),
|
||||
"--realm", "master", "--user", "admin",
|
||||
"--password", password]
|
||||
)
|
||||
|
||||
client_json = textwrap.dedent("""
|
||||
{{
|
||||
"enabled" : true,
|
||||
"clientAuthenticatorType" : "client-secret",
|
||||
"redirectUris" : [ "https://ipa-ca.{redirect}/ipa/idp/*" ],
|
||||
"webOrigins" : [ "https://ipa-ca.{web}" ],
|
||||
"protocol" : "openid-connect",
|
||||
"attributes" : {{
|
||||
"oauth2.device.authorization.grant.enabled" : "true",
|
||||
"oauth2.device.polling.interval": "5"
|
||||
}}
|
||||
}}
|
||||
""").format(redirect=host.domain.name, web=host.domain.name)
|
||||
host.put_file_contents("/tmp/ipa_client.json", client_json)
|
||||
host.run_command(["/opt/keycloak/bin/kcreg.sh", "create",
|
||||
"-f", "/tmp/ipa_client.json",
|
||||
"-s", "clientId=ipa_oidc_client",
|
||||
"-s", "secret={0}".format(password)]
|
||||
)
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
def uninstall_keycloak(host):
|
||||
key = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.key")
|
||||
crt = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.crt")
|
||||
keystore = os.path.join(paths.OPENSSL_PRIVATE_DIR, "keycloak.store")
|
||||
|
||||
host.run_command(["systemctl", "stop", "keycloak"], raiseonerr=False)
|
||||
host.run_command(["getcert", "stop-tracking", "-k", key, "-f", crt],
|
||||
raiseonerr=False)
|
||||
host.run_command(["rm", "-rf", "/opt/keycloak",
|
||||
"/etc/sysconfig/keycloak",
|
||||
"/etc/systemd/system/keycloak.service",
|
||||
key, crt, keystore])
|
||||
host.run_command(["systemctl", "daemon-reload"])
|
||||
host.run_command(["userdel", "keycloak"])
|
||||
host.run_command(["groupdel", "keycloak"], raiseonerr=False)
|
||||
tasks.restore_files(host)
|
||||
155
ipatests/pytest_ipa/integration/expect.py
Normal file
155
ipatests/pytest_ipa/integration/expect.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
import pexpect
|
||||
from pexpect.exceptions import ExceptionPexpect, TIMEOUT
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class IpaTestExpect(pexpect.spawn):
|
||||
"""A wrapper class around pexpect.spawn for easier usage in automated tests
|
||||
|
||||
Please see pexpect documentation at
|
||||
https://pexpect.readthedocs.io/en/stable/api/index.html for general usage
|
||||
instructions. Note that usage of "+", "*" and '?' at the end of regular
|
||||
expressions arguments to .expect() is meaningless.
|
||||
|
||||
This wrapper adds ability to use the class as a context manager, which
|
||||
will take care of verifying process return status and terminating
|
||||
the process if it did not do it normally. The context manager is the
|
||||
recommended way of using the class in tests.
|
||||
Basic usage example:
|
||||
|
||||
```
|
||||
with IpaTestExpect('some_command') as e:
|
||||
e.expect_exact('yes or no?')
|
||||
e.sendline('yes')
|
||||
```
|
||||
|
||||
At exit from context manager the following checks are performed by default:
|
||||
1. there is nothing in output since last call to .expect()
|
||||
2. the process has terminated
|
||||
3. return code is 0
|
||||
|
||||
If any check fails, an exceptio is raised. If you want to override checks
|
||||
1 and 3 you can call .expect_exit() explicitly:
|
||||
|
||||
```
|
||||
with IpaTestExpect('some_command') as e:
|
||||
...
|
||||
e.expect_exit(ok_returncode=1, ignore_remaining_output=True)
|
||||
```
|
||||
|
||||
All .expect* methods are strict, meaning that if they do not find the
|
||||
pattern in the output during given amount of time, the exception is raised.
|
||||
So they can directly be used to verify output for presence of specific
|
||||
strings.
|
||||
|
||||
Another addition is .get_last_output() method which can be used get process
|
||||
output from penultimate up to the last call to .expect(). The result can
|
||||
be used for more complex checks which can not be expressed as simple
|
||||
regexes, for example we can check for absence of string in output:
|
||||
|
||||
```
|
||||
with IpaTestExpect('some_command') as e:
|
||||
...
|
||||
e.expect('All done')
|
||||
output = e.get_last_output()
|
||||
assert 'WARNING' not in output
|
||||
```
|
||||
"""
|
||||
def __init__(self, argv, default_timeout=10, encoding='utf-8'):
|
||||
if isinstance(argv, str):
|
||||
command = argv
|
||||
args = []
|
||||
else:
|
||||
command = argv[0]
|
||||
args = argv[1:]
|
||||
logger.debug('Expect will spawn command "%s" with args %s',
|
||||
command, args)
|
||||
super().__init__(
|
||||
command, args, timeout=default_timeout, encoding=encoding,
|
||||
echo=False
|
||||
)
|
||||
|
||||
def expect_exit(self, timeout=-1, ok_returncode=0, raiseonerr=True,
|
||||
ignore_remaining_output=False):
|
||||
if timeout == -1:
|
||||
timeout = self.timeout
|
||||
wait_to_exit_until = time.time() + timeout
|
||||
if not self.eof():
|
||||
self.expect(pexpect.EOF, timeout)
|
||||
errors = []
|
||||
if not ignore_remaining_output and self.before.strip():
|
||||
errors.append('Unexpected output at program exit: {!r}'
|
||||
.format(self.before))
|
||||
|
||||
while time.time() < wait_to_exit_until:
|
||||
if not self.isalive():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
errors.append('Program did not exit after waiting for {} seconds'
|
||||
.format(self.timeout))
|
||||
if (not self.isalive() and raiseonerr
|
||||
and self.exitstatus != ok_returncode):
|
||||
errors.append('Program exited with unexpected status {}'
|
||||
.format(self.exitstatus))
|
||||
self.exit_checked = True
|
||||
if errors:
|
||||
raise ExceptionPexpect(
|
||||
'Program exited with an unexpected state:\n'
|
||||
+ '\n'.join(errors))
|
||||
|
||||
def send(self, s):
|
||||
"""Wrapper to provide logging input string"""
|
||||
logger.debug('Sending %r', s)
|
||||
return super().send(s)
|
||||
|
||||
def expect_list(self, pattern_list, *args, **kwargs):
|
||||
"""Wrapper to provide logging output string and expected patterns"""
|
||||
try:
|
||||
result = super().expect_list(pattern_list, *args, **kwargs)
|
||||
finally:
|
||||
self._log_output(pattern_list)
|
||||
return result
|
||||
|
||||
def expect_exact(self, pattern_list, *args, **kwargs):
|
||||
"""Wrapper to provide logging output string and expected patterns"""
|
||||
try:
|
||||
result = super().expect_exact(pattern_list, *args, **kwargs)
|
||||
finally:
|
||||
self._log_output(pattern_list)
|
||||
return result
|
||||
|
||||
def get_last_output(self):
|
||||
"""Return output consumed by last call to .expect*()"""
|
||||
output = self.before
|
||||
if isinstance(self.after, str):
|
||||
output += self.after
|
||||
return output
|
||||
|
||||
def _log_output(self, expected):
|
||||
logger.debug('Output received: %r, expected: "%s", ',
|
||||
self.get_last_output(), expected)
|
||||
|
||||
def __enter__(self):
|
||||
self.exit_checked = False
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
exception_occurred = bool(exc_type)
|
||||
try:
|
||||
if not self.exit_checked:
|
||||
self.expect_exit(raiseonerr=not exception_occurred,
|
||||
ignore_remaining_output=exception_occurred)
|
||||
except TIMEOUT:
|
||||
if not exception_occurred:
|
||||
raise
|
||||
finally:
|
||||
if self.isalive():
|
||||
logger.error('Command still active, terminating.')
|
||||
self.terminate(True)
|
||||
@@ -37,6 +37,9 @@ def enable_userspace_fips(host):
|
||||
# fake Kernel FIPS mode with bind mount
|
||||
host.run_command(["mkdir", "-p", FIPS_OVERLAY_DIR])
|
||||
host.put_file_contents(FIPS_OVERLAY, "1\n")
|
||||
host.run_command(
|
||||
["chcon", "-t", "sysctl_crypto_t", "-u", "system_u", FIPS_OVERLAY]
|
||||
)
|
||||
host.run_command(
|
||||
["mount", "--bind", FIPS_OVERLAY, paths.PROC_FIPS_ENABLED]
|
||||
)
|
||||
@@ -49,7 +52,7 @@ def enable_userspace_fips(host):
|
||||
["openssl", "md5", "/dev/null"], raiseonerr=False
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert "EVP_DigestInit_ex:disabled for FIPS" in result.stderr_text
|
||||
assert "Error setting digest" in result.stderr_text
|
||||
|
||||
|
||||
def disable_userspace_fips(host):
|
||||
@@ -65,3 +68,9 @@ def disable_userspace_fips(host):
|
||||
# sanity check
|
||||
assert not is_fips_enabled(host)
|
||||
host.run_command(["openssl", "md5", "/dev/null"])
|
||||
|
||||
|
||||
def enable_crypto_subpolicy(host, subpolicy):
|
||||
result = host.run_command(["update-crypto-policies", "--show"])
|
||||
policy = result.stdout_text.strip() + ":" + subpolicy
|
||||
host.run_command(["update-crypto-policies", "--set", policy])
|
||||
|
||||
@@ -231,7 +231,7 @@ class Firewall(FirewallBase):
|
||||
def __init__(self, host):
|
||||
"""Initialize with host where firewall changes should be applied"""
|
||||
# break circular dependency
|
||||
from .tasks import get_platform
|
||||
from .tasks import get_platform # pylint: disable=cyclic-import
|
||||
|
||||
self.host = host
|
||||
platform = get_platform(host)
|
||||
|
||||
@@ -32,6 +32,7 @@ from .fips import (
|
||||
is_fips_enabled, enable_userspace_fips, disable_userspace_fips
|
||||
)
|
||||
from .transport import IPAOpenSSHTransport
|
||||
from .resolver import resolver
|
||||
|
||||
FIPS_NOISE_RE = re.compile(br"FIPS mode initialized\r?\n?")
|
||||
|
||||
@@ -78,6 +79,7 @@ class Host(pytest_multihost.host.Host):
|
||||
)
|
||||
self._fips_mode = None
|
||||
self._userspace_fips = False
|
||||
self.resolver = resolver(self)
|
||||
|
||||
@property
|
||||
def is_fips_mode(self):
|
||||
@@ -204,6 +206,11 @@ class Host(pytest_multihost.host.Host):
|
||||
else:
|
||||
return result
|
||||
|
||||
def spawn_expect(self, argv, default_timeout=10, encoding='utf-8',
|
||||
extra_ssh_options=None):
|
||||
"""Run command on remote host using IpaTestExpect"""
|
||||
return self.transport.spawn_expect(argv, default_timeout, encoding,
|
||||
extra_ssh_options)
|
||||
|
||||
class WinHost(pytest_multihost.host.WinHost):
|
||||
"""
|
||||
|
||||
353
ipatests/pytest_ipa/integration/resolver.py
Normal file
353
ipatests/pytest_ipa/integration/resolver.py
Normal file
@@ -0,0 +1,353 @@
|
||||
import os
|
||||
import abc
|
||||
import logging
|
||||
import re
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Resolver(abc.ABC):
|
||||
def __init__(self, host):
|
||||
self.host = host
|
||||
self.backups = []
|
||||
self.current_state = self._get_state()
|
||||
logger.info('Obtained initial resolver state for host %s: %s',
|
||||
self.host, self.current_state)
|
||||
|
||||
def setup_resolver(self, nameservers, searchdomains=None):
|
||||
"""Configure DNS resolver
|
||||
|
||||
:param nameservers: IP address of nameserver or a list of addresses
|
||||
:param searchdomains: searchdomain or list of searchdomains.
|
||||
None - do not configure
|
||||
|
||||
Resolver.backup() must be called prior to using this method.
|
||||
|
||||
Raises exception if configuration was changed externally since last call
|
||||
to any method of Resolver class.
|
||||
"""
|
||||
if len(self.backups) == 0:
|
||||
raise Exception(
|
||||
'Changing resolver state without backup is forbidden')
|
||||
self.check_state_expected()
|
||||
if isinstance(nameservers, str):
|
||||
nameservers = [nameservers]
|
||||
if isinstance(searchdomains, str):
|
||||
searchdomains = [searchdomains]
|
||||
if searchdomains is None:
|
||||
searchdomains = []
|
||||
logger.info(
|
||||
'Setting up resolver for host %s: nameservers=%s, searchdomains=%s',
|
||||
self.host, nameservers, searchdomains
|
||||
)
|
||||
state = self._make_state_from_args(nameservers, searchdomains)
|
||||
self._set_state(state)
|
||||
|
||||
def backup(self):
|
||||
"""Saves current configuration to stack
|
||||
|
||||
Raises exception if configuration was changed externally since last call
|
||||
to any method of Resolver class.
|
||||
"""
|
||||
self.check_state_expected()
|
||||
self.backups.append(self._get_state())
|
||||
logger.info(
|
||||
'Saved resolver state for host %s, number of saved states: %s',
|
||||
self.host, len(self.backups)
|
||||
)
|
||||
|
||||
def restore(self):
|
||||
"""Restore configuration from stack of backups.
|
||||
|
||||
Raises exception if configuration was changed externally since last call
|
||||
to any method of Resolver class.
|
||||
"""
|
||||
|
||||
if len(self.backups) == 0:
|
||||
raise Exception('No resolver backups found for host {}'.format(
|
||||
self.host))
|
||||
self.check_state_expected()
|
||||
self._set_state(self.backups.pop())
|
||||
logger.info(
|
||||
'Restored resolver state for host %s, number of saved states: %s',
|
||||
self.host, len(self.backups)
|
||||
)
|
||||
|
||||
def has_backups(self):
|
||||
"""Checks if stack of backups is not empty"""
|
||||
return bool(self.backups)
|
||||
|
||||
def check_state_expected(self):
|
||||
"""Checks if resolver configuration has not changed.
|
||||
|
||||
Raises AssertionError if actual configuration has changed since last
|
||||
call to any method of Resolver
|
||||
"""
|
||||
assert self._get_state() == self.current_state, (
|
||||
'Resolver state changed unexpectedly at host {}'.format(self.host))
|
||||
|
||||
def __enter__(self):
|
||||
self.backup()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.restore()
|
||||
|
||||
def _set_state(self, state):
|
||||
self._apply_state(state)
|
||||
logger.info('Applying resolver state for host %s: %s', self.host, state)
|
||||
self.current_state = state
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def is_our_resolver(cls, host):
|
||||
"""Checks if the class is appropriate for managing resolver on the host.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _make_state_from_args(self, nameservers, searchdomains):
|
||||
"""
|
||||
|
||||
:param nameservers: list of ip addresses of nameservers
|
||||
:param searchdomains: list of searchdomain, can be an empty list
|
||||
:return: internal state object specific to subclass implementaion
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_state(self):
|
||||
"""Acquire actual host configuration.
|
||||
|
||||
:return: internal state object specific to subclass implementaion
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _apply_state(self, state):
|
||||
"""Apply configuration to host.
|
||||
|
||||
:param state: internal state object specific to subclass implementaion
|
||||
"""
|
||||
|
||||
def uses_localhost_as_dns(self):
|
||||
"""Return true if the localhost is set as DNS server.
|
||||
|
||||
Default implementation checks the content of /etc/resolv.conf
|
||||
"""
|
||||
resolvconf = self.host.get_file_contents(paths.RESOLV_CONF, 'utf-8')
|
||||
patterns = [r"^\s*nameserver\s+127\.0\.0\.1\s*$",
|
||||
r"^\s*nameserver\s+::1\s*$"]
|
||||
return any(re.search(p, resolvconf, re.MULTILINE) for p in patterns)
|
||||
|
||||
|
||||
class ResolvedResolver(Resolver):
|
||||
RESOLVED_RESOLV_CONF = {
|
||||
"/run/systemd/resolve/stub-resolv.conf",
|
||||
"/run/systemd/resolve/resolv.conf",
|
||||
"/lib/systemd/resolv.conf",
|
||||
"/usr/lib/systemd/resolv.conf",
|
||||
}
|
||||
RESOLVED_CONF_FILE = (
|
||||
'/etc/systemd/resolved.conf.d/zzzz-ipatests-nameservers.conf')
|
||||
RESOLVED_CONF = textwrap.dedent('''
|
||||
# generated by IPA tests
|
||||
[Resolve]
|
||||
DNS={nameservers}
|
||||
Domains=~. {searchdomains}
|
||||
''')
|
||||
|
||||
@classmethod
|
||||
def is_our_resolver(cls, host):
|
||||
res = host.run_command(
|
||||
['stat', '--format', '%F', paths.RESOLV_CONF])
|
||||
filetype = res.stdout_text.strip()
|
||||
if filetype == 'symbolic link':
|
||||
res = host.run_command(['realpath', paths.RESOLV_CONF])
|
||||
return (res.stdout_text.strip() in cls.RESOLVED_RESOLV_CONF)
|
||||
return False
|
||||
|
||||
def _restart_resolved(self):
|
||||
# Restarting service at rapid pace (which is what happens in some test
|
||||
# scenarios) can exceed the threshold configured in systemd option
|
||||
# StartLimitIntervalSec. In that case restart fails, but we can simply
|
||||
# continue trying until it succeeds
|
||||
from . import tasks # pylint: disable=cyclic-import
|
||||
tasks.run_repeatedly(
|
||||
self.host, ['systemctl', 'restart', 'systemd-resolved.service'],
|
||||
timeout=15)
|
||||
|
||||
def _make_state_from_args(self, nameservers, searchdomains):
|
||||
return {
|
||||
'resolved_config': self.RESOLVED_CONF.format(
|
||||
nameservers=' '.join(nameservers),
|
||||
searchdomains=' '.join(searchdomains))
|
||||
}
|
||||
|
||||
def _get_state(self):
|
||||
exists = self.host.transport.file_exists(self.RESOLVED_CONF_FILE)
|
||||
return {
|
||||
'resolved_config':
|
||||
self.host.get_file_contents(self.RESOLVED_CONF_FILE, 'utf-8')
|
||||
if exists else None
|
||||
}
|
||||
|
||||
def _apply_state(self, state):
|
||||
if state['resolved_config'] is None:
|
||||
self.host.run_command(['rm', '-f', self.RESOLVED_CONF_FILE])
|
||||
else:
|
||||
self.host.run_command(
|
||||
['mkdir', '-p', os.path.dirname(self.RESOLVED_CONF_FILE)])
|
||||
self.host.put_file_contents(
|
||||
self.RESOLVED_CONF_FILE, state['resolved_config'])
|
||||
self._restart_resolved()
|
||||
|
||||
def uses_localhost_as_dns(self):
|
||||
"""Return true if the localhost is set as DNS server.
|
||||
|
||||
When systemd-resolved is in use, the DNS can be found using
|
||||
the command resolvectldns.
|
||||
"""
|
||||
dnsconf = self.host.run_command(['resolvectl', 'dns']).stdout_text
|
||||
patterns = [r"^Global:.*\s+127.0.0.1\s+.*$",
|
||||
r"^Global:.*\s+::1\s+.*$"]
|
||||
return any(re.search(p, dnsconf, re.MULTILINE) for p in patterns)
|
||||
|
||||
|
||||
class PlainFileResolver(Resolver):
|
||||
IPATESTS_RESOLVER_COMMENT = '# created by ipatests'
|
||||
|
||||
@classmethod
|
||||
def is_our_resolver(cls, host):
|
||||
res = host.run_command(
|
||||
['stat', '--format', '%F', paths.RESOLV_CONF])
|
||||
filetype = res.stdout_text.strip()
|
||||
if filetype == 'regular file':
|
||||
# We want to be sure that /etc/resolv.conf is not generated
|
||||
# by NetworkManager or systemd-resolved. When it is then
|
||||
# the first line of the file is a comment of the form:
|
||||
#
|
||||
# Generated by NetworkManager
|
||||
#
|
||||
# or
|
||||
#
|
||||
# This file is managed by man:systemd-resolved(8). Do not edit.
|
||||
#
|
||||
# So we check that either first line of resolv.conf
|
||||
# is not a comment or the comment does not mention NM or
|
||||
# systemd-resolved
|
||||
resolv_conf = host.get_file_contents(paths.RESOLV_CONF, 'utf-8')
|
||||
line = resolv_conf.splitlines()[0].strip()
|
||||
return not line.startswith('#') or all([
|
||||
'resolved' not in line,
|
||||
'NetworkManager' not in line
|
||||
])
|
||||
return False
|
||||
|
||||
def _make_state_from_args(self, nameservers, searchdomains):
|
||||
contents_lines = [self.IPATESTS_RESOLVER_COMMENT]
|
||||
contents_lines.extend('nameserver {}'.format(r) for r in nameservers)
|
||||
if searchdomains:
|
||||
contents_lines.append('search {}'.format(' '.join(searchdomains)))
|
||||
contents = '\n'.join(contents_lines)
|
||||
return {'resolv_conf': contents}
|
||||
|
||||
def _get_state(self):
|
||||
return {
|
||||
'resolv_conf': self.host.get_file_contents(
|
||||
paths.RESOLV_CONF, 'utf-8')
|
||||
}
|
||||
|
||||
def _apply_state(self, state):
|
||||
self.host.put_file_contents(paths.RESOLV_CONF, state['resolv_conf'])
|
||||
|
||||
|
||||
class NetworkManagerResolver(Resolver):
|
||||
NM_CONF_FILE = '/etc/NetworkManager/conf.d/zzzz-ipatests.conf'
|
||||
NM_CONF = textwrap.dedent('''
|
||||
# generated by IPA tests
|
||||
[main]
|
||||
dns=default
|
||||
|
||||
[global-dns]
|
||||
searches={searchdomains}
|
||||
|
||||
[global-dns-domain-*]
|
||||
servers={nameservers}
|
||||
''')
|
||||
|
||||
@classmethod
|
||||
def is_our_resolver(cls, host):
|
||||
res = host.run_command(
|
||||
['stat', '--format', '%F', paths.RESOLV_CONF])
|
||||
filetype = res.stdout_text.strip()
|
||||
if filetype == 'regular file':
|
||||
resolv_conf = host.get_file_contents(paths.RESOLV_CONF, 'utf-8')
|
||||
return resolv_conf.startswith('# Generated by NetworkManager')
|
||||
return False
|
||||
|
||||
def _restart_network_manager(self):
|
||||
# Restarting service at rapid pace (which is what happens in some test
|
||||
# scenarios) can exceed the threshold configured in systemd option
|
||||
# StartLimitIntervalSec. In that case restart fails, but we can simply
|
||||
# continue trying until it succeeds
|
||||
from . import tasks # pylint: disable=cyclic-import
|
||||
tasks.run_repeatedly(
|
||||
self.host, ['systemctl', 'restart', 'NetworkManager.service'],
|
||||
timeout=15)
|
||||
|
||||
def _make_state_from_args(self, nameservers, searchdomains):
|
||||
return {'nm_config': self.NM_CONF.format(
|
||||
nameservers=','.join(nameservers),
|
||||
searchdomains=','.join(searchdomains))}
|
||||
|
||||
def _get_state(self):
|
||||
exists = self.host.transport.file_exists(self.NM_CONF_FILE)
|
||||
return {
|
||||
'nm_config':
|
||||
self.host.get_file_contents(self.NM_CONF_FILE, 'utf-8')
|
||||
if exists else None
|
||||
}
|
||||
|
||||
def _apply_state(self, state):
|
||||
def get_resolv_conf_mtime():
|
||||
"""Get mtime of /etc/resolv.conf.
|
||||
|
||||
Returns mtime with sub-second precision as a string with format
|
||||
"2020-08-25 14:35:05.980503425 +0200"
|
||||
"""
|
||||
return self.host.run_command(
|
||||
['stat', '-c', '%y', paths.RESOLV_CONF]).stdout_text.strip()
|
||||
|
||||
if state['nm_config'] is None:
|
||||
self.host.run_command(['rm', '-f', self.NM_CONF_FILE])
|
||||
else:
|
||||
self.host.run_command(
|
||||
['mkdir', '-p', os.path.dirname(self.NM_CONF_FILE)])
|
||||
self.host.put_file_contents(
|
||||
self.NM_CONF_FILE, state['nm_config'])
|
||||
# NetworkManager writes /etc/resolv.conf few moments after
|
||||
# `systemctl restart` returns so we need to wait until the file is
|
||||
# updated
|
||||
mtime_before = get_resolv_conf_mtime()
|
||||
self._restart_network_manager()
|
||||
wait_until = time.time() + 10
|
||||
while time.time() < wait_until:
|
||||
if get_resolv_conf_mtime() != mtime_before:
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise Exception('NetworkManager did not update /etc/resolv.conf '
|
||||
'in 10 seconds after restart')
|
||||
|
||||
|
||||
def resolver(host):
|
||||
for cls in [ResolvedResolver, NetworkManagerResolver,
|
||||
PlainFileResolver]:
|
||||
if cls.is_our_resolver(host):
|
||||
logger.info('Detected DNS resolver manager for host %s is %s',
|
||||
host.hostname, cls)
|
||||
return cls(host)
|
||||
raise Exception('Resolver manager could not be detected')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@ Provides SSH password login for OpenSSH transport
|
||||
"""
|
||||
import os
|
||||
|
||||
from .expect import IpaTestExpect
|
||||
|
||||
from pytest_multihost.transport import OpenSSHTransport
|
||||
|
||||
|
||||
@@ -46,3 +48,12 @@ class IPAOpenSSHTransport(OpenSSHTransport):
|
||||
self.log.debug("SSH invocation: %s", argv)
|
||||
|
||||
return argv
|
||||
|
||||
def spawn_expect(self, argv, default_timeout, encoding, extra_ssh_options):
|
||||
self.log.debug('Starting pexpect ssh session')
|
||||
if isinstance(argv, str):
|
||||
argv = [argv]
|
||||
if extra_ssh_options is None:
|
||||
extra_ssh_options = []
|
||||
argv = self._get_ssh_argv() + ['-q'] + extra_ssh_options + argv
|
||||
return IpaTestExpect(argv, default_timeout, encoding)
|
||||
|
||||
Reference in New Issue
Block a user