freeipa/ipatests/azure/scripts/setup_containers.py
Stanislav Levin 1c82895c20 azure: Wait for systemd booted
The calling of systemd's utils during systemd boot may lead to
unpredictable results. For example, if DBus(dbus-broker) service
is not started then DBus request goes nowhere and eventually will
be timeouted. So, it's safer to wait fully booted system.

Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
2021-05-25 10:45:49 +03:00

432 lines
13 KiB
Python

from __future__ import annotations
from datetime import datetime
import logging
import os
import subprocess
import time
import docker
from jinja2 import Template
from typing import NamedTuple, TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Tuple, Union
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
IPA_TESTS_ENV_WORKING_DIR = os.environ.get("IPA_TESTS_ENV_WORKING_DIR")
IPA_TESTS_ENV_NAME = os.environ.get("IPA_TESTS_ENV_NAME")
IPA_TESTS_ENV_ID = os.environ.get("IPA_TESTS_ENV_ID", "1")
IPA_TESTS_CLIENTS = int(os.environ.get("IPA_TESTS_CLIENTS", 0))
IPA_TESTS_REPLICAS = int(os.environ.get("IPA_TESTS_REPLICAS", 0))
IPA_TESTS_DOMAIN = os.environ.get("IPA_TESTS_DOMAIN", "ipa.test")
IPA_SSH_PRIV_KEY = os.environ.get("IPA_SSH_PRIV_KEY", "/root/.ssh/id_rsa")
IPA_DNS_FORWARDER = os.environ.get("IPA_DNS_FORWARDER", "8.8.8.8")
IPA_NETWORK = os.environ.get("IPA_NETWORK", "ipanet")
IPA_CONTROLLER_TYPE = os.environ.get("IPA_CONTROLLER_TYPE", "master")
IPA_TEST_CONFIG_TEMPLATE = os.environ.get(
"IPA_TEST_CONFIG_TEMPLATE", "./templates/ipa-test-config-template.yaml"
)
IPA_TESTS_ENV_DIR = os.path.join(IPA_TESTS_ENV_WORKING_DIR, IPA_TESTS_ENV_NAME)
IPA_TEST_CONFIG = "ipa-test-config.yaml"
class ExecRunReturn(NamedTuple): # pylint: disable=inherit-non-class #3876
exit_code: int
output: Tuple[bytes, bytes]
class Container:
"""
Represents Docker container
"""
def __init__(self, name, hostname, network):
self.name = name
self.hostname = hostname
self.network = network
self.dclient = docker.from_env()
@property
def ip(self):
"""
ipv4 address of container
"""
if not hasattr(self, "_ip"):
dcont = self.dclient.containers.get(self.name)
self._ip = dcont.attrs["NetworkSettings"]["Networks"][
self.network
]["IPAddress"]
return self._ip
@property
def ipv6(self):
"""
ipv6 address of container
"""
if not hasattr(self, "_ipv6"):
dcont = self.dclient.containers.get(self.name)
self._ipv6 = dcont.attrs["NetworkSettings"]["Networks"][
self.network
]["GlobalIPv6Address"]
return self._ipv6
def execute(
self, args: Union[str, List[str]], raiseonerr: bool = True
) -> ExecRunReturn:
"""
Exec an arbitrary command within container
"""
dcont = self.dclient.containers.get(self.name)
logging.info("%s: run: %s", dcont.name, args)
result: ExecRunReturn = dcont.exec_run(args, demux=True)
if result.output[0] is not None:
logging.info("%s: %s", dcont.name, result.output[0])
logging.info("%s: result: %s", dcont.name, result.exit_code)
if result.exit_code and raiseonerr:
logging.error("stderr: %s", result.output[1].decode())
raise subprocess.CalledProcessError(
result.exit_code, args, result.output[1]
)
return result
class ContainersGroup:
"""
Represents group of Docker containers
"""
HOME_SSH_DIR = "/root/.ssh"
def __init__(
self,
role,
nameservers=[IPA_DNS_FORWARDER],
scale=1,
prefix=IPA_TESTS_ENV_ID,
domain=IPA_TESTS_DOMAIN,
):
self.role = role
self.scale = scale
self.prefix = prefix
self.nameservers = nameservers
self.domain = domain
# initialize containers
self.containers = [
Container(
name=f"{self.prefix}_{self.role}_{c}",
hostname=f"{self.role}{c}.{self.domain}",
network=f"{IPA_TESTS_ENV_ID}_{IPA_NETWORK}",
)
for c in range(1, self.scale + 1)
]
def execute_all(self, args):
"""
Sequentially exec an arbitrary command within every container of group
"""
results = []
for cont in self.containers:
results.append(cont.execute(args))
return results
def ips(self):
return [cont.ip for cont in self.containers]
def umount_docker_resource(self, path):
"""
Umount resource by its path
"""
cmd = ["/bin/umount", path]
self.execute_all(cmd)
cmd = [
"/bin/chmod",
"a-x",
path,
]
self.execute_all(cmd)
def add_ssh_pubkey(self, key):
"""
Add ssh public key into every container of group
"""
auth_keys = os.path.join(self.HOME_SSH_DIR, "authorized_keys")
cmd = [
"/bin/bash",
"-c",
(
f"mkdir {self.HOME_SSH_DIR} "
f"; chmod 0700 {self.HOME_SSH_DIR} "
f"&& touch {auth_keys} "
f"&& chmod 0600 {auth_keys} "
f"&& echo {key} >> {auth_keys}"
),
]
self.execute_all(cmd)
def setup_hosts(self):
"""
Overwrite hosts within every container of group
"""
self.umount_docker_resource("/etc/hosts")
for cont in self.containers:
hosts = "\n".join(
[
"127.0.0.1 localhost",
"::1 localhost",
f"{cont.ip} {cont.hostname}",
f"{cont.ipv6} {cont.hostname}",
]
)
cmd = ["/bin/bash", "-c", f"echo -e '{hosts}' > /etc/hosts"]
cont.execute(cmd)
def setup_hostname(self):
self.umount_docker_resource("/etc/hostname")
for cont in self.containers:
cmd = [
"/bin/bash",
"-c",
f"echo -e '{cont.hostname}' > /etc/hostname",
]
cont.execute(cmd)
cmd = ["hostnamectl", "set-hostname", cont.hostname]
cont.execute(cmd)
def setup_resolvconf(self):
"""
Overwrite resolv conf within every container of group
"""
self.umount_docker_resource("/etc/resolv.conf")
nameservers = "\n".join(
[f"nameserver {ns}" for ns in self.nameservers]
)
cmd = [
"/bin/bash",
"-c",
f"echo -e '{nameservers}' > /etc/resolv.conf",
]
self.execute_all(cmd)
def ignore_service_in_container(self, service):
"""
Amend systemd service configuration to be ignored in a container
"""
service_dir = os.path.join(
"/etc/systemd/system", "{}.service.d".format(service)
)
override_file = os.path.join(service_dir, "ipa-override.conf")
cmds = [
"/bin/bash",
"-c",
(
f"mkdir -p {service_dir};"
f"echo '[Unit]' > {override_file};"
f"echo 'ConditionVirtualization=!container' >> {override_file}"
),
]
self.execute_all(cmds)
def setup_container_overrides(self):
"""
Set services known to not work in containers to be ignored
"""
for service in [
"nis-domainname",
]:
self.ignore_service_in_container(service)
self.execute_all(["systemctl", "daemon-reload"])
def wait_systemd_target_reached(
self,
target_name: str = "default.target",
startup_timeout: int = 180,
) -> None:
RETRY_DELAY_SEC = 5
cmd = ["systemctl", "is-active", "--quiet", target_name]
for cont in self.containers:
reached = False
start = datetime.today()
while not reached:
result = cont.execute(cmd, raiseonerr=False)
reached = result.exit_code == 0
if not reached:
elapsed = int((datetime.today() - start).total_seconds())
if elapsed > startup_timeout:
raise RuntimeError(
f"Systemd's target '{target_name}' wasn't reached "
f"in container: '{cont.name}'\n"
f"stderr: {result.output[1].decode('utf-8')}"
)
time.sleep(RETRY_DELAY_SEC)
class Controller(Container):
"""
Represents Controller, which manages groups of containers groups
"""
def __init__(self, contr_type=IPA_CONTROLLER_TYPE):
self.containers_groups = []
self.contr_type = contr_type
def append(self, containers_group):
self.containers_groups.append(containers_group)
def wait_systemd_target_reached(
self, target_name: str = "multi-user.target"
) -> None:
for containers_group in self.containers_groups:
containers_group.wait_systemd_target_reached(target_name)
def setup_ssh(self):
"""
Generate ssh key pair and copy public part to all containers
"""
cmd = ["rm", "-f", IPA_SSH_PRIV_KEY]
self.execute(cmd)
cmd = [
"ssh-keygen",
"-q",
"-f",
IPA_SSH_PRIV_KEY,
"-t",
"rsa",
"-m",
"PEM",
"-N",
"",
]
self.execute(cmd)
cmd = ["/bin/bash", "-c", "cat {}.pub".format(IPA_SSH_PRIV_KEY)]
key = self.execute(cmd).output[0].decode().rstrip()
for containers_group in self.containers_groups:
containers_group.add_ssh_pubkey(key)
@property
def master_container(self):
if not hasattr(self, "_master_container"):
master_containers_group = None
for containers_group in self.containers_groups:
if containers_group.role == "master":
master_containers_group = containers_group
break
if master_containers_group is None:
raise ValueError(
"There must be container group with master role"
)
# assume the only master
self._master_container = master_containers_group.containers[0]
return self._master_container
def execute(self, args):
"""
Execute a command on controller (either master or local machine)
"""
if self.contr_type != "master":
proc = subprocess.run(args, check=True, capture_output=True)
return [proc.stdout.decode().rstrip().strip("'")]
return self.master_container.execute(args)
def setup_hosts(self):
"""
Overwrite Docker's hosts
"""
hosts = []
for containers_group in self.containers_groups:
containers_group.setup_hosts()
# prevent duplication of master entries
if (
self.contr_type == "master"
and containers_group.role == "master"
):
continue
for container in containers_group.containers:
hosts.append(f"{container.ip} {container.hostname}")
hosts.append(f"{container.ipv6} {container.hostname}")
cmd = [
"/bin/bash",
"-c",
"echo -e '{hosts}' >> /etc/hosts".format(hosts="\n".join(hosts)),
]
self.execute(cmd)
def setup_hostname(self):
"""
Overwrite Docker's hostname
"""
for containers_group in self.containers_groups:
containers_group.setup_hostname()
def setup_resolvconf(self):
"""
Overwrite Docker's embedded DNS ns
"""
for containers_group in self.containers_groups:
containers_group.setup_resolvconf()
def generate_ipa_test_config(self, config):
with open(IPA_TEST_CONFIG_TEMPLATE, "r") as f:
template = Template(f.read(), trim_blocks=True, lstrip_blocks=True)
logging.info(template.render(config))
with open(os.path.join(IPA_TESTS_ENV_DIR, IPA_TEST_CONFIG), "w") as f:
f.write(template.render(config))
def setup_container_overrides(self):
"""
Override services known to not work in containers
"""
for containers_group in self.containers_groups:
containers_group.setup_container_overrides()
controller = Controller()
master = ContainersGroup(role="master")
# assume the only master
master_ips = [master.containers[0].ip, master.containers[0].ipv6]
clients = ContainersGroup(
role="client", scale=IPA_TESTS_CLIENTS, nameservers=master_ips
)
replicas = ContainersGroup(
role="replica", scale=IPA_TESTS_REPLICAS, nameservers=master_ips
)
controller.append(master)
controller.append(clients)
controller.append(replicas)
controller.wait_systemd_target_reached()
controller.setup_ssh()
controller.setup_hosts()
controller.setup_hostname()
controller.setup_resolvconf()
controller.setup_container_overrides()
config = {
"dns_forwarder": IPA_DNS_FORWARDER,
"ssh_private_key": IPA_SSH_PRIV_KEY,
"domain_name": IPA_TESTS_DOMAIN,
"master": master.ips(),
"replicas": replicas.ips(),
"clients": clients.ips(),
}
controller.generate_ipa_test_config(config)