Files
kayobe/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
Mark Goddard c603be2536 Ubuntu: add support for Apt repository configuration
This change adds support for configuring Apt repositories on Ubuntu
hosts during host configuration.

Repositories are configured in a single file
(/etc/apt/sources.list.d/kayobe.sources), using the modern deb822
format [1]. This format is more flexible and readable than the original
single-line format, particularly if multiple options are used.

Using a single file allows us to more easily keep the set of
repositories in sync, since Ansible doesn't make it easy to clean things
up.

Support is added for marking repositories as signed by a particular GPG
key. This approach is now preferred over the deprecated [2] apt-key
tool, which resulted in a set of globally trusted keys.

It is also possible to disable the repositories in
/etc/apt/sources.list via apt_disable_sources_list. This allows for
replacing the standard repositories with a local mirror.

CI tests and documentation are provided.

[1] https://manpages.ubuntu.com/manpages/focal/en/man5/sources.list.5.html
[2] https://manpages.ubuntu.com/manpages/groovy/man8/apt-key.8.html

Story: 2009655
Task: 43818

Change-Id: I3f821937b0930a0ac9341178de7ae5123d82b957
2022-03-23 06:47:17 +00:00

307 lines
9.9 KiB
Python

#!/usr/bin/env python3
# Kayobe overcloud host configure tests.
# Uses py.test and TestInfra.
import ipaddress
import os
import time
import distro
import pytest
def _is_firewalld_supported():
info = distro.id()
return info in ['centos', 'rocky']
def _is_apt():
info = distro.linux_distribution()
return info[0].startswith('Ubuntu')
def _is_dnf():
info = distro.id()
return info in ['centos', 'rocky']
def _is_dnf_mirror():
info = distro.id()
return info == 'centos'
def test_network_ethernet(host):
interface = host.interface('dummy2')
assert interface.exists
assert '192.168.34.1' in interface.addresses
routes = host.check_output('/sbin/ip route show dev dummy2')
assert '192.168.40.0/24 via 192.168.34.254' in routes
def test_network_ethernet_vlan(host):
interface = host.interface('dummy2.42')
assert interface.exists
assert '192.168.35.1' in interface.addresses
assert host.file('/sys/class/net/dummy2.42/lower_dummy2').exists
routes = host.check_output(
'/sbin/ip route show dev dummy2.42 table kayobe-test-route-table')
assert '192.168.40.0/24 via 192.168.35.254' in routes
rules = host.check_output(
'/sbin/ip rule show table kayobe-test-route-table')
expected = 'from 192.168.35.0/24 lookup kayobe-test-route-table'
assert expected in rules
def test_network_bridge(host):
interface = host.interface('br0')
assert interface.exists
assert '192.168.36.1' in interface.addresses
ports = ['dummy3', 'dummy4']
sys_ports = host.check_output('ls -1 /sys/class/net/br0/brif')
assert sys_ports == "\n".join(ports)
for port in ports:
interface = host.interface(port)
assert interface.exists
v4_addresses = [a for a in interface.addresses
if ipaddress.ip_address(a).version == '4']
assert not v4_addresses
def test_network_bridge_vlan(host):
interface = host.interface('br0.43')
assert interface.exists
assert '192.168.37.1' in interface.addresses
assert host.file('/sys/class/net/br0.43/lower_br0').exists
def test_network_bond(host):
interface = host.interface('bond0')
assert interface.exists
assert '192.168.38.1' in interface.addresses
sys_slaves = host.check_output('cat /sys/class/net/bond0/bonding/slaves')
# Ordering is not guaranteed, so compare sets.
sys_slaves = set(sys_slaves.split())
slaves = set(['dummy5', 'dummy6'])
assert sys_slaves == slaves
for slave in slaves:
interface = host.interface(slave)
assert interface.exists
assert not interface.addresses
def test_network_bond_vlan(host):
interface = host.interface('bond0.44')
assert interface.exists
assert '192.168.39.1' in interface.addresses
assert host.file('/sys/class/net/bond0.44/lower_bond0').exists
def test_network_bridge_no_ip(host):
interface = host.interface('br1')
assert interface.exists
assert not '192.168.40.1' in interface.addresses
def test_additional_user_account(host):
user = host.user("kayobe-test-user")
assert user.name == "kayobe-test-user"
assert user.group == "kayobe-test-user"
assert set(user.groups) == {"kayobe-test-user", "stack"}
assert user.gecos == "Kayobe test user"
with host.sudo():
assert user.password == 'kayobe-test-user-password'
def test_software_RAID(host):
slaves = host.check_output("ls -1 /sys/class/block/md0/slaves/")
assert slaves == "loop0\nloop1"
def test_luks(host):
# blkid returns an emptry string without root permissions
with host.sudo():
blkid = host.check_output('blkid /dev/md0')
assert 'TYPE="crypto_LUKS"' in blkid
def test_sysctls(host):
assert host.sysctl("fs.mount-max") == 99999
def test_cloud_init_is_disabled(host):
assert host.file("/etc/cloud/cloud-init.disabled").exists
def test_docker_storage_driver_is_devicemapper(host):
with host.sudo("stack"):
info = host.check_output("docker info")
assert "devicemapper" in info
@pytest.mark.parametrize('user', ['kolla', 'stack'])
def test_docker_image_download(host, user):
with host.sudo(user):
host.check_output("docker pull alpine")
@pytest.mark.parametrize('user', ['kolla', 'stack'])
def test_docker_container_run(host, user):
with host.sudo(user):
host.check_output("docker run --rm alpine /bin/true")
def test_timezone(host):
status = host.check_output("timedatectl status")
assert "Pacific/Honolulu" in status
def test_ntp_alternative_services_disabled(host):
# Tests that we don't have any conflicting NTP servers running
# NOTE(wszumski): We always mask services even if they don't exist
ntpd_service = host.service("ntp")
assert ntpd_service.is_masked
assert not ntpd_service.is_running
timesyncd_service = host.service("systemd-timesyncd")
assert timesyncd_service.is_masked
assert not timesyncd_service.is_running
def test_ntp_running(host):
# Tests that NTP services are enabled and running
assert host.package("chrony").is_installed
assert host.service("chronyd").is_enabled
assert host.service("chronyd").is_running
def test_ntp_non_default_time_server(host):
# Tests that the NTP pool has been changed from pool.ntp.org to
# time.cloudflare.com
if ('centos' in host.system_info.distribution.lower() or
'rocky' in host.system_info.distribution.lower()):
chrony_config = host.file("/etc/chrony.conf")
else:
# Debian based distributions use the following path
chrony_config = host.file("/etc/chrony/chrony.conf")
assert chrony_config.exists
assert "time.cloudflare.com" in chrony_config.content_string
def test_ntp_clock_synchronized(host):
# Tests that the clock is synchronized
status_output = host.check_output("timedatectl status")
assert "synchronized: yes" in status_output
@pytest.mark.skipif(not _is_apt(), reason="Apt only supported on Ubuntu")
def test_apt_custom_package_repository_is_available(host):
with host.sudo():
host.check_output("apt -y install td-agent")
assert host.package("td-agent").is_installed
@pytest.mark.parametrize('repo', ["appstream", "baseos", "extras", "epel",
"epel-modular"])
@pytest.mark.skipif(not _is_dnf_mirror(), reason="DNF OpenDev mirror only for CentOS 8")
def test_dnf_local_package_mirrors(host, repo):
# Depends on SITE_MIRROR_FQDN environment variable.
assert os.getenv('SITE_MIRROR_FQDN')
# NOTE(mgoddard): Should not require sudo but some files
# (/var/cache/dnf/expired_repos.json) can have incorrect permissions.
# https://bugzilla.redhat.com/show_bug.cgi?id=1636909
with host.sudo():
info = host.check_output("dnf repoinfo %s", repo)
assert os.getenv('SITE_MIRROR_FQDN') in info
@pytest.mark.skipif(not _is_dnf(), reason="DNF only supported on CentOS 8/Rocky 8")
def test_dnf_custom_package_repository_is_available(host):
with host.sudo():
host.check_output("dnf -y install td-agent")
assert host.package("td-agent").is_installed
@pytest.mark.skipif(not _is_dnf(), reason="DNF only supported on CentOS 8/Rocky 8")
def test_dnf_automatic(host):
assert host.package("dnf-automatic").is_installed
assert host.service("dnf-automatic.timer").is_enabled
assert host.service("dnf-automatic.timer").is_running
@pytest.mark.skipif(not _is_dnf(),
reason="tuned profile setting only supported on CentOS 8/Rocky 8")
def test_tuned_profile_is_active(host):
tuned_output = host.check_output("tuned-adm active")
assert "throughput-performance" in tuned_output
@pytest.mark.skipif(not _is_firewalld_supported(),
reason="Firewalld only supported on CentOS and Rocky")
def test_firewalld_running(host):
assert host.package("firewalld").is_installed
assert host.service("firewalld.service").is_enabled
assert host.service("firewalld.service").is_running
@pytest.mark.skipif(not _is_firewalld_supported(),
reason="Firewalld only supported on CentOS and Rocky")
def test_firewalld_zones(host):
# Verify that interfaces are on correct zones.
expected_zones = {
'dummy2.42': 'test-zone1',
'br0': 'test-zone2',
'br0.43': 'test-zone3',
'bond0': 'test-zone3',
'bond0.44': 'public'
}
for interface, expected_zone in expected_zones.items():
with host.sudo():
zone = host.check_output(
"firewall-cmd --get-zone-of-interface %s", interface)
assert zone == expected_zone
zone = host.check_output(
"firewall-cmd --permanent --get-zone-of-interface %s",
interface)
assert zone == expected_zone
@pytest.mark.skipif(not _is_firewalld_supported(),
reason="Firewalld only supported on CentOS and Rocky")
def test_firewalld_rules(host):
# Verify that expected rules are present.
expected_info = {
'test-zone1': [
' services: ',
' ports: 8080/tcp',
' icmp-blocks: ',
],
'test-zone2': [
' services: http',
' ports: ',
' icmp-blocks: ',
],
'test-zone3': [
' services: ',
' ports: ',
' icmp-blocks: echo-request',
],
'public': [
' services: dhcpv6-client ssh',
' ports: ',
' icmp-blocks: ',
],
}
for zone, expected_lines in expected_info.items():
with host.sudo():
info = host.check_output(
"firewall-cmd --info-zone %s", zone)
info = info.splitlines()
perm_info = host.check_output(
"firewall-cmd --permanent --info-zone %s", zone)
perm_info = perm_info.splitlines()
for expected_line in expected_lines:
assert expected_line in info
assert expected_line in perm_info