#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)## Copyright (C) 2013 Red Hat, Inc.# SPDX-License-Identifier: LGPL-2.1-or-laterimport reimport timeimport packagelibimport submanlibimport testlibos_release = """NAME="Foobar Adventure Linux Server"VERSION="2.0 (Day of Doom)"ID="foobar"VERSION_ID="2.0"PRETTY_NAME="Foobar Adventure Linux Server 2.0 (Day of Doom)""""lscpu = """#!/bin/shecho 'CPU(s): 8'echo 'On-line CPU(s) list: 0-7'echo 'Thread(s) per core: {0}'echo 'Core(s) per socket: 4'echo 'Socket(s): 1'"""# Helper for MB size when handling raw bytes.MB = 1024**2def ssh_reconnect(machine, timeout_sec=120):start_time = time.time()error = Nonewhile (time.time() - start_time) < timeout_sec:try:machine.execute("true", quiet=True)returnexcept Exception as e:error = etime.sleep(0.5)raise errorclass TestSystemInfo(testlib.MachineCase):def setUp(self):super().setUp()# Most OSes don't set nosmt by default, but there are some exceptionsself.expect_smt_default = self.machine.image in ["fedora-coreos"]# Switching into and out of FIPS mode is not supported anymore in newer OSes.self.supportsFIPS = self.machine.image.startswith(("rhel-8", "rhel-9", "centos-9"))@testlib.skipImage("TODO: fix for OpenSUSE", "opensuse-tumbleweed")def testBasic(self):m = self.machineb = self.browser# /etc/os-release might be a symlink and file watching doesn't# follow symlinks, so we remove it and then create a regular# file.## In addition hostnamed does not expect os-release to change so# we force a restart. Usually any such changes to os-release are# expected to happen during reboot, or picked up after a reboot.## subscription-manager also screws with os-release so set it# to immutable#m.execute("rm /etc/os-release")m.write("/etc/os-release", os_release)m.execute("chattr +i /etc/os-release; (systemctl restart systemd-hostnamed || systemctl restart hostnamed)")# set static machine ID, as they have different lengths which disturbs the pixel testm.execute("mv /etc/machine-id /etc/machine-id.orig")m.write("/etc/machine-id", "123456789abcdef123456789abcdef00")# avoid "Last logged in" health card message, it breaks pixel tests and is too unpredictablem.execute("rm -f /var/log/lastlog /var/lib/lastlog/lastlog2.db /var/lib/wtmpdb/wtmp.db")self.login_and_go("/system")b.wait_visible('#system_information_os_text')# Health card can contain only one item - it normally is "Loading available updates fail"# But sometimes it also contains information about failed services which breaks mobile pixel testsm.execute("systemctl reset-failed")b.wait_not_present("#page_status_notification_system_services")# ensure general page/card layout without the changing specifics# need to wait until CPU usage settles down, to avoid a layout-shifting error/warning iconb.wait_not_present("#system-usage-cpu-progress + td .pf-v6-c-progress__status-icon")testlib.wait(lambda: b.get_pf_progress_value("#system-usage-cpu-progress + td") < 30)if b.pixels_label:# Wait for tuned text to appear, even thought we ignore it we need to button to have some text.recommended_profile = m.execute("tuned-adm recommend").strip()b.wait_text("#tuned-status-button", recommended_profile)# Wait for there to be no warning icons in the usage.b.wait_not_present('#system-usage .pf-v6-c-progress__status-icon')b.assert_pixels("#overview", "overview", ignore=[".system-health .pf-v6-c-card__body","#system_uptime",# #system_information_systime_button is not enough, need to grab the icon as well"td[data-label='system-time']",# CPU/memory metrics"#system-usage-cpu-progress + td","#system-usage-memory-progress + td","#tuned-status-button",])m.execute("mv /etc/machine-id.orig /etc/machine-id")# Generate a new rsa key and change the configm.execute("ssh-keygen -f /etc/ssh/weirdname -t rsa -N ''")m.execute("chmod 600 /etc/ssh/weirdname")m.execute("restorecon /etc/ssh/weirdname || true")new_default = m.execute("ssh-keygen -l -f /etc/ssh/weirdname -E md5 | cut -d' ' -f2 | tr -d '\n'")new_alt = m.execute("ssh-keygen -l -f /etc/ssh/weirdname -E sha256 | cut -d' ' -f2 | tr -d '\n'")old_default = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key -E md5 | cut -d' ' -f2 | tr -d '\n'")old_alt = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key -E sha256 | cut -d' ' -f2 | tr -d '\n'")b.click("#system-ssh-keys-link")b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", "ED25519")b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", "RSA")b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", "ECDSA")b.wait_not_in_text("#system_information_ssh_keys .pf-v6-c-list", new_default)b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", old_default)b.wait_not_in_text("#system_information_ssh_keys .pf-v6-c-list", new_alt)b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", old_alt)b.click("#system_information_ssh_keys button:contains('Close')")b.wait_not_present("#system_information_ssh_keys")# Change ssh config and restartself.sed_file(r"s,.*HostKey *,#,; $ a HostKey /etc/ssh/weirdname", "/etc/ssh/sshd_config",self.restart_sshd)ssh_reconnect(m)b.click("#system-ssh-keys-link")b.wait_visible("#system_information_ssh_keys")b.wait_not_in_text("#system_information_ssh_keys .pf-v6-c-list", "ED25519")b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", "RSA")b.wait_not_in_text("#system_information_ssh_keys .pf-v6-c-list", "ECDSA")b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", new_default)b.wait_not_in_text("#system_information_ssh_keys .pf-v6-c-list", old_default)b.wait_not_in_text("#system_information_ssh_keys .pf-v6-c-list", old_alt)b.wait_in_text("#system_information_ssh_keys .pf-v6-c-list", new_alt)b.wait_in_text('#system_information_os_text',"Foobar Adventure Linux Server 2.0 (Day of Doom)")b.click("#system_information_ssh_keys button:contains('Close')")b.wait_not_present("#system_information_ssh_keys")m.execute("hostnamectl set-hostname --static --pretty 'Adventure Box'")b.wait_in_text('#system_information_hostname_text', "Adventure Box")b.click('#system_information_hostname_button')b.wait_visible("#system_information_change_hostname")b.wait_val("#sich-pretty-hostname", "Adventure Box")# Test setting the pretty hostname, changes the normal hostnameb.set_input_text("#sich-pretty-hostname", "Adventure Time")b.wait_val("#sich-hostname", "adventure-time")# Changing the hostname should validateb.set_input_text("#sich-hostname", 65 * "x")b.wait_in_text("#system_information_change_hostname .pf-v6-c-helper-text__item-text", "64 characters or less")b.set_input_text("#sich-hostname", "host1.cockpit.lan$")b.wait_in_text("#system_information_change_hostname .pf-v6-c-helper-text__item-text", "Real host name can only contain")b.set_input_text("#sich-hostname", "host1.cockpit.lan")b.click("#system_information_change_hostname button:contains('Change')")b.wait_not_present("#system_information_change_hostname")b.wait_in_text('#system_information_hostname_text', "Adventure Time (host1.cockpit.lan)")self.assertEqual(m.execute("hostname").strip(), "host1.cockpit.lan")m.execute("hostnamectl set-hostname ''")m.execute("hostnamectl set-hostname --transient 'mydhcpname'")b.wait_in_text('#system_information_hostname_text', 'mydhcpname')b.logout()m.execute("chattr -i /etc/os-release; rm /etc/os-release")m.execute("rm /usr/lib/os-release || true")self.login_and_go("/system")mid = m.execute("cat /etc/machine-id")b.wait_text('#system_machine_id', mid)self.allow_journal_messages("error loading contents of os-release: .*", # C bridge".* Neither /etc/os-release nor /usr/lib/os-release exists", # py bridge"sudo: unable to resolve host host1.cockpit.lan: .*")@testlib.nondestructivedef testUptime(self):m = self.machineb = self.browserself.login_and_go("/system")b.wait_text_not("#system_uptime", "")# replace it with a known value, it should automatically update every minutem.write("/tmp/fake_uptime", "2000.12 12345.30\n")m.execute("mount -o bind /tmp/fake_uptime /proc/uptime")self.addCleanup(m.execute, "umount /proc/uptime")with b.wait_timeout(70):b.wait_text("#system_uptime", "33 minutes ago")# 4 months and a bit, timeformat rounds quite aggressively; also, test a slightly different formatm.write("/tmp/fake_uptime", "10370000 12345.30\n")with b.wait_timeout(70):b.wait_text("#system_uptime", "4 months ago")@testlib.nondestructive@testlib.skipImage("TODO: fix for OpenSUSE", "opensuse-tumbleweed")def testMotd(self):m = self.machineb = self.browserself.restore_file("/etc/motd")# run this test with a tight umask to check preserving file permissionsself.sed_file(r"/^UMASK/ s/0../077/", "/etc/login.defs")m.execute("rm -f /etc/motd")self.login_and_go("/system")b.wait_not_present('#motd-box')m.execute(r"printf '\n \n Hello\n World\n\n' >/etc/motd; chmod 644 /etc/motd")b.wait_visible('#motd-box')# strips empty lines, but not leading spacesb.wait_text('#motd', " Hello\n World")# For some reason, switching to RTL takes a long time# here. Let's increase the delay.b.assert_pixels("#motd-box", "motd", wait_delay=2.0)b.click('#motd-box button:not(#motd-box-edit)')b.wait_not_present('#motd-box')# motd should stay dismissed after a reloadb.reload()b.enter_page("/system")b.wait_not_present('#motd-box')m.execute("echo Hello again >/etc/motd")b.wait_visible('#motd-box')b.wait_text('#motd', "Hello again")# Cancel buttonb.click("#motd-box-edit")b.click("#motd-box-edit-modal button.pf-m-link")b.wait_not_present("motd-box-edit-modal")b.click("#motd-box-edit")b.set_input_text("#motd-box-edit-modal textarea", "Hello cockpit team")b.click("#motd-box-edit-modal button.pf-m-primary")b.wait_not_present("motd-box-edit-modal")b.wait_text('#motd', "Hello cockpit team")self.assertEqual("Hello cockpit team", self.machine.execute("cat /etc/motd").rstrip())@testlib.nondestructivedef testHardwareInfo(self):b = self.browserm = self.machineself.login_and_go("/system")b.wait_in_text('#system_information_hardware_text', "QEMU")hardware_page_link = '.system-information a'b.click(hardware_page_link)b.enter_page("/system/hwinfo")# system infob.wait_in_text('#hwinfo-system-info-list', "CPU")# QEMU VM typeb.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v6-c-description-list__group:nth-of-type(1) dd', "Other")# Nameb.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v6-c-description-list__group:nth-of-type(2) dd', "Standard PC")# BIOSb.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v6-c-description-list__group:nth-of-type(1) dd', "SeaBIOS")# BIOS date gets parsedparsed_bios_date = m.execute("date --date $(cat /sys/class/dmi/id/bios_date) '+%B %-d, %Y'").strip()b.wait_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v6-c-description-list__group:nth-of-type(3) dd', parsed_bios_date)pci_selector = '#hwinfo #pci-listing'heading_selector = ' .pf-v6-c-card__title'# PCIb.wait_in_text(pci_selector + heading_selector, "PCI")b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Slot]', "0000:00:00.0")# sorted by device class by default; this makes some assumptions about QEMU devicesb.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Class]', "Bridge")b.wait_in_text(pci_selector + ' tbody:last-of-type td[data-label=Class]', "Unclassified")# sort by modelb.click(pci_selector + ' thead th:nth-child(2) button')b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Model]', "440")b.wait_in_text(pci_selector + ' tbody:last-of-type td[data-label=Model]', "Virtio SCSI")b.wait_not_in_text(pci_selector + ' tbody:last-of-type td[data-label=Model]', "Unclassified")# go back to system pageb.click('.pf-v6-c-breadcrumb li:first-of-type')b.enter_page("/system")# now pretend this is a system without DMIb.logout()m.execute("mount -t tmpfs none /sys/class/dmi/id")# check if it's mounted as the memory tests umount it.self.addCleanup(m.execute, "! mountpoint -q /sys/class/dmi/id || umount /sys/class/dmi/id")self.login_and_go("/system")# asset tag should be hiddenb.wait_not_present('#system_information_asset_tag_text')# Hardware should be hiddenb.wait_not_present('#system_information_hardware_text')b.click(hardware_page_link)b.enter_page("/system/hwinfo")# CPU should still be shown, but not the DMI fieldsb.wait_in_text('#hwinfo-system-info-list', "CPU")self.assertNotIn('Type', b.text('#hwinfo-system-info-list'))self.assertNotIn('BIOS', b.text('#hwinfo-system-info-list'))# PCI should be shownb.wait_in_text(pci_selector + heading_selector, "PCI")b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Slot]', "0000:00:00.0")# Check also variants when only some fields are presentm.write("/sys/class/dmi/id/chassis_type", "10")b.go("/system")b.enter_page('/system')b.wait_not_present('#system_information_hardware_text')m.write("/sys/class/dmi/id/board_vendor", "VENDOR")m.write("/sys/class/dmi/id/board_name", "NAME")b.reload()b.enter_page('/system')b.wait_in_text('#system_information_hardware_text', "VENDOR NAME")b.click(hardware_page_link)b.enter_page("/system/hwinfo")b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v6-c-description-list__group:nth-of-type(2) dd', "NAME")b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v6-c-description-list__group:nth-of-type(3) dd', "VENDOR")# Clean up after lazy OEMs, falls back to board vendor/namem.write("/sys/class/dmi/id/sys_vendor", "To Be Filled By O.E.M.")m.write("/sys/class/dmi/id/product_name", "To Be Filled By O.E.M.")m.write("/sys/class/dmi/id/product_serial", "PL1234")m.write("/sys/class/dmi/id/board_vendor", "brdven")m.write("/sys/class/dmi/id/board_name", "brdnam")b.reload()b.enter_page("/system/hwinfo")b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v6-c-description-list__group:nth-of-type(2) dd', "brdnam")b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v6-c-description-list__group:nth-of-type(3) dd', "brdven")b.go("/system")b.enter_page('/system')b.wait_in_text('#system_information_hardware_text', "brdven brdnam")if m.execute("uname -m").strip() == "x86_64":b.wait_in_text('#system_information_asset_tag_text', "PL1234")else:b.wait_not_present('#system_information_asset_tag_text')b.click(hardware_page_link)# /proc/cpuinfo on x86; very incomplete, just what pkg/lib/machine-info.js looks atm.write("/tmp/cpuinfo", """processor\t: 0vendor_id\t: GenuineIntelmodel\t\t: 42model name\t: Professor NumberCrunchprocessor\t: 1vendor_id\t: GenuineIntelmodel\t\t: 42model name\t: Professor NumberCrunch""")m.execute("mount -o bind /tmp/cpuinfo /proc/cpuinfo")self.addCleanup(m.execute, "umount /proc/cpuinfo")b.reload()b.enter_page('/system/hwinfo')b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v6-c-description-list__group:nth-of-type(1) dd', "2x Professor NumberCrunch")# /proc/cpuinfo on PowerPC; complete infom.write("/tmp/cpuinfo", """processor\t: 0cpu\t\t: POWER9 (architected), altivec supportedclock\t\t: 3000.000000MHzrevision\t: 2.3 (pvr 004e 1203)processor\t: 1cpu\t\t: POWER9 (architected), altivec supportedclock\t\t: 3000.000000MHzrevision\t: 2.3 (pvr 004e 1203)""")b.reload()b.enter_page('/system/hwinfo')b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v6-c-description-list__group:nth-of-type(1) dd', "2x POWER9 (architected), altivec supported")# correct CPU count on overviewb.go("/system")b.enter_page("/system")b.wait_in_text("#system-usage-cpu-progress + td", "of 2 CPUs")# /proc/cpuinfo on s390x (reduced)m.write("/tmp/cpuinfo", """vendor_id : IBM/S390# processors : 2bogomips per cpu: 3241.00max thread id : 0features : esan3 zarch stfle msa ldisp eimm dfp edat etf3eh highgprs te vx vxd vxe gs vxe2 vxp sort dflt sieprocessor 0: version = FF, identification = 2EB428, machine = 8561processor 1: version = FF, identification = 2EB428, machine = 8561cpu number : 0cpu cores : 1version : FFidentification : 2EB428machine : 8561cpu number : 1cpu cores : 1version : FFidentification : 2EB428machine : 8561""")b.reload()b.enter_page("/system")b.wait_in_text("#system-usage-cpu-progress + td", "of 2 CPUs")b.go('/system/hwinfo')b.enter_page('/system/hwinfo')b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v6-c-description-list__group:nth-of-type(1) dd', "2x IBM/S390")# umount mocked /sys/class/dmi/idm.execute("umount /sys/class/dmi/id")m.execute("udevadm trigger --verbose /sys/devices/virtual/dmi/id")b.reload()b.enter_page('/system/hwinfo')# Memory details should be shown from our mocked DMI information from systemd's test files.b.wait_in_text('#hwinfo #memory-listing' + heading_selector, "Memory")b.wait_in_text('#hwinfo #memory-listing table', "DIMM")b.wait_in_text('#hwinfo #memory-listing table', "RAM")tmp_dmi_tables = "/tmp/dmi_tables"m.execute(f"mkdir {tmp_dmi_tables}")self.addCleanup(m.execute, f"rm -rf {tmp_dmi_tables}")m.upload(["verify/files/dmi/smbios_entry_point", "verify/files/dmi/DMI"], tmp_dmi_tables)m.execute(f"mount -o bind {tmp_dmi_tables} /sys/firmware/dmi/tables")self.addCleanup(m.execute, "umount /sys/firmware/dmi/tables")m.execute("udevadm trigger --verbose /sys/devices/virtual/dmi/id")b.reload()b.enter_page('/system/hwinfo')distro_without_systemd_memory_dmi = m.image.startswith('rhel-8-')# Test more specific memory data with a fake dmidecodeb.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=ID]', "BANK 0: ChannelA-DIMM0")b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Type]', "DDR4")if distro_without_systemd_memory_dmi:b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GB")else:b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GiB")b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=State]', "Present")b.wait_text('#memory-listing tbody:nth-of-type(1) td[data-label="Memory technology"]', "Unknown")b.wait_text('#memory-listing tbody:nth-of-type(1) td[data-label=Rank]', "Single rank")b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Speed]', "2400 MT/s")b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=ID]', "BANK 2: ChannelB-DIMM0")b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=Type]', "DDR4")if distro_without_systemd_memory_dmi:b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GB")else:b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GiB")b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=State]', "Present")b.wait_text('#memory-listing tbody:nth-of-type(2) td[data-label="Memory technology"]', "Unknown")b.wait_text('#memory-listing tbody:nth-of-type(2) td[data-label=Rank]', "Single rank")b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=Speed]', "2400 MT/s")@testlib.nondestructivedef testCPUSecurityMitigationsDetect(self):b = self.browserm = self.machineself.restore_dir("/usr/local/bin")m.start_cockpit()def spoof_threads(threads_per_core, expect_link_present, expect_smt_state=None, cmdline=None):m.write('/usr/local/bin/lscpu', lscpu.format(threads_per_core))m.execute('chmod +x /usr/local/bin/lscpu')if cmdline:m.write('/run/cmdline', cmdline)m.execute('if selinuxenabled 2>/dev/null; then chcon --reference /proc/cmdline /run/cmdline; fi')m.execute('mount --bind /run/cmdline /proc/cmdline; rm /run/cmdline')try:b.login_and_go('/system/hwinfo')if not expect_link_present:b.wait_in_text('#hwinfo-system-info-list', "CPU")b.wait_not_in_text('#hwinfo-system-info-list', "CPU security")else:b.click('#hwinfo button:contains("Mitigations")')if expect_smt_state is not None:b.wait_in_text('#cpu-mitigations-dialog .nosmt-heading', 'nosmt')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +(":checked" if expect_smt_state else ":not(:checked)"))b.logout()finally:if cmdline:m.execute('while ! umount /proc/cmdline; do sleep 1; done')spoof_threads(1, expect_link_present=False)spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 param2 nosmt param3=value3')spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 param2 nosmt=force param3=value3')spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 mitigations=auto,nosmt param3=value3')spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 mitigations=nosmt,something param3=value3')spoof_threads(2, expect_link_present=True, expect_smt_state=False, cmdline='param1 mitigations=something param3=value3')spoof_threads(2, expect_link_present=False, cmdline='param1 nosmt=someunknown param3=value3')spoof_threads(2, expect_link_present=True, expect_smt_state=self.expect_smt_default, cmdline=None)@testlib.skipImage("TODO: add Arch Linux grub entry support", "arch")@testlib.timeout(1200)def testCPUSecurityMitigationsEnable(self):b = self.browserm = self.machine# spoof SMTm.write('/usr/local/bin/lscpu', lscpu.format(2))m.execute('chmod +x /usr/local/bin/lscpu')# Switch nosmt optionself.login_and_go('/system/hwinfo')b.click('#hwinfo button:contains("Mitigations")')b.click('#cpu-mitigations-dialog #nosmt-switch input')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +(':not(:checked)' if self.expect_smt_default else ':checked'))b.click('#cpu-mitigations-dialog Button:contains("Save and reboot")')self.wait_reboot()if self.expect_smt_default:self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))else:self.assertIn('nosmt', m.execute('cat /proc/cmdline'))# Ensure that future kernel upgrades also retain the option# - Debian: no BLS, options go into /etc/default/grub and grub.cfg# - BLS, options go directly into entries, or entries use $kernelopt (defined in grubenv)if not m.ostree_image:m.execute(r"""echo dummy > /boot/vmlinuz-42.0.0; mkdir -p /lib/modules/42.0.0/if type update-grub >/dev/null 2>&1; thenupdate-grub # Debian/Ubuntugrep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfgelif type update-bootloader >/dev/null 2>&1; thenupdate-bootloader # susegrep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfgelsecp -a /boot/grub2/grubenv /boot/grub2/grubenv.prevkernel-install add 42.0.0 /boot/vmlinuz-42.0.0 2>/dev/nullgrep -q '^options.*\bnosmt\b' /boot/loader/entries/*42.0.0*.conf ||( grub2-editenv list | grep -q kernelopts.*nosmt &&grep -q '^options.*$kernelopts' /boot/loader/entries/*42.0.0*.conf )fi""")# clean up so that next reboot worksm.execute(r"""rm /boot/vmlinuz-42.0.0if type update-grub >/dev/null 2>&1; thenupdate-grub # Debian/Ubuntuelif type update-bootloader >/dev/null 2>&1; thenupdate-bootloader # suseelsekernel-install remove 42.0.0 /boot/vmlinuz-42.0.0# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2078359 and https://bugzilla.redhat.com/show_bug.cgi?id=2078379mv /boot/grub2/grubenv.prev /boot/grub2/grubenvfi""")# Switch back nosmt optionself.login_and_go('/system/hwinfo')b.click('#hwinfo button:contains("Mitigations")')b.wait_in_text('#cpu-mitigations-dialog .nosmt-heading', 'nosmt')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +(':not(:checked)' if self.expect_smt_default else ':checked'))b.click('#cpu-mitigations-dialog #nosmt-switch input')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +(':checked' if self.expect_smt_default else ':not(:checked)'))b.click('#cpu-mitigations-dialog Button:contains("Save and reboot")')self.wait_reboot()if self.expect_smt_default:self.assertIn('nosmt', m.execute('cat /proc/cmdline'))else:self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))# updates mitigations=nosmt when that is presentm.upload(["../pkg/lib/kernelopt.sh"], "/tmp/")if self.expect_smt_default:m.execute("/tmp/kernelopt.sh remove nosmt")m.execute("/tmp/kernelopt.sh set mitigations=auto,nosmt")self.reboot()self.login_and_go('/system/hwinfo')b.click('#hwinfo button:contains("Mitigations")')b.wait_in_text('#cpu-mitigations-dialog .nosmt-heading', 'nosmt')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input:checked')b.click('#cpu-mitigations-dialog #nosmt-switch input')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input:not(:checked)')b.click('#cpu-mitigations-dialog Button:contains("Save and reboot")')self.wait_reboot()self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))self.assertIn('mitigations=auto', m.execute('cat /proc/cmdline'))# Behaviour for non-adminsself.login_and_go('/system/hwinfo', superuser=False)b.wait_visible('#cpu_mitigations[disabled]')b.mouse('#tip-cpu-security', 'mouseenter')b.wait_text('.pf-v6-c-tooltip', 'The user admin is not permitted to change cpu security mitigations')b.mouse('#tip-cpu-security', 'mouseleave')b.wait_not_present("div.pf-v6-c-tooltip")# Behaviour if grub update tools are missingb.logout()m.execute('mv /etc/default/grub /etc/default/grub.bak || true')m.write('/tmp/grubby', '#!/bin/sh\necho 0')m.execute('[ ! -f /usr/sbin/grubby ] || mount --bind /tmp/grubby /usr/sbin/grubby')m.write('/tmp/update-bootloader', '#!/bin/sh\necho 0')m.execute('[ ! -f /usr/sbin/update-bootloader ] || mount --bind /tmp/update-bootloader /usr/sbin/update-bootloader')m.execute('systemctl mask --now rpm-ostreed.service')self.login_and_go('/system/hwinfo')b.click('#hwinfo button:contains("Mitigations")')b.click('#cpu-mitigations-dialog #nosmt-switch input')b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input:checked')b.click("#cpu-mitigations-dialog Button:contains('Save and reboot')")b.wait_in_text('#cpu-mitigations-dialog .pf-v6-c-alert__title', 'No supported grub update mechanism found')self.allow_journal_messages('Sourcing file `/etc/default/grub.*','Generating grub configuration file.*','Found linux image.*','Found initrd image.*','.*warning: setlocale: LC_ALL: cannot change locale.*','done')@testlib.nondestructivedef testOverview(self):m = self.machineb = self.browser# packagekit often eats a lot of CPU; silence it to not screw up the "system is idle" testm.execute("systemctl mask packagekit")self.addCleanup(m.execute, "systemctl unmask packagekit")def progressUsage(number):return b.get_pf_progress_value(f".system-usage tr:nth-child({number})")def progressUsageRaw(number):return b.get_raw_pf_progress_value(f".system-usage tr:nth-child({number})")def cpuUsage(raw: bool = False):if raw:return progressUsageRaw(1)else:return progressUsage(1)def memoryUsage(raw: bool = False):if raw:return progressUsageRaw(2)else:return progressUsage(2)self.login_and_go("/system")# CPU# first wait until system settles downtestlib.wait(lambda: cpuUsage() < 20)m.spawn("for i in $(seq $(nproc)); do cat /dev/urandom > /dev/null & done; touch /tmp/cpu_hog", "cpu_hog.log")m.execute("while [ ! -e /tmp/cpu_hog ]; do sleep 1; done")testlib.wait(lambda: cpuUsage() > 75)m.execute("pkill -e -f [c]at.*urandom; rm -f /tmp/cpu_hog")# should go back to idle usage# HACK: work around pmie CPU usage https://bugzilla.redhat.com/show_bug.cgi?id=2140572testlib.wait(lambda: cpuUsage() < 20, tries=200)# memory: our test machines should use a reasonable chunk of available memory; MiB or GiBb.wait_in_text(".system-usage tr:nth-child(2)", "iB")initial_usage = memoryUsage(raw=True)max_memory = int(b.attr(".system-usage tr:nth-child(2) .pf-v6-c-progress__bar", "aria-valuemax"))self.assertGreater(initial_usage, 10 * MB)self.assertLess(initial_usage, max_memory * 0.9)# allocate an extra 200 MB; this may cause other stuff to get unmapped,# thus not exact addition, but usage should go up## The "true" after "sleep" is there to prevent bash from# replacing it's own process with the sleep (as a "tail call# optimization") and thereby dropping the memory blob too early.#mem_hog = m.spawn("""awk 'BEGIN { x = sprintf("%200000000s",""); system("touch /tmp/hogged; sleep infinity") }'""", "mem_hog.log")try:m.execute("while [ ! -e /tmp/hogged ]; do sleep 1; done")# bars update every 5stime.sleep(8)testlib.wait(lambda: memoryUsage(raw=True) >= initial_usage + 10 * MB)hog_usage = memoryUsage()finally:m.execute("kill %d" % mem_hog)# Should go back to initial_usage, but it doesn't always, for example on fedora.# So let's be happy if the usage drops significantlytry:testlib.wait(lambda: memoryUsage() <= hog_usage - 10)except testlib.Error:print(f"Memory before hogging: {initial_usage}, after hogging: {hog_usage}, now: {memoryUsage()}")raiseself.assertGreater(memoryUsage(), 10)@testlib.nondestructivedef testShutdownStatus(self):m = self.machineb = self.browserself.login_and_go("/system")b.wait_not_present("#system-health-shutdown-status")# Schedule a rebootm.execute("shutdown --reboot +10")self.addCleanup(m.execute, "shutdown -c")b.wait_in_text('#system-health-shutdown-status-text', "Scheduled reboot")# Check that reloading still shows the reboot textb.reload()b.enter_page("/system")b.wait_in_text('#system-health-shutdown-status-text', "Scheduled reboot")# Cancelb.click("#system-health-shutdown-status-cancel-btn")b.wait_not_present('#system-health-shutdown-status')# Schedule a poweroffm.execute("shutdown --poweroff +10")b.wait_in_text('#system-health-shutdown-status-text', "Scheduled poweroff")# Cancelb.click("#system-health-shutdown-status-cancel-btn")b.wait_not_present('#system-health-shutdown-status')dbus_call = 'busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager ScheduledShutdown'self.assertIn('(st) "" ', m.execute(dbus_call).strip())@testlib.skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch", "opensuse-tumbleweed")@testlib.skipOstree("crypto-policies not available")def testCryptoPolicies(self):m = self.machineb = self.browserdef shown_profile_text(profile):return "Default" if profile == "DEFAULT" else profiledef change_profile(profile, new_profile):b.click("#crypto-policy-button")b.wait_in_text(".pf-v6-c-menu__item.pf-m-selected", shown_profile_text(profile))profile_button_name = shown_profile_text(new_profile)b.click(f"#crypto-policy-dialog .pf-v6-c-menu__list-item[data-value='{profile_button_name}'] button")b.click("#crypto-policy-save-reboot")# Initramfs re-generation takes a whileself.wait_reboot(timeout_sec=600)m.start_cockpit()self.login_and_go("/system")b.wait_text("#crypto-policy-button", shown_profile_text(new_profile))cmd = "update-crypto-policies"self.login_and_go("/system")profile = m.execute(cmd + " --show").strip()b.wait_text("#crypto-policy-button", shown_profile_text(profile))# RHEL 8/10 have no SHA1 policy, so do not show it.b.click("#crypto-policy-button")func = b.wait_not_present if re.match(r'(centos|rhel)-(8|10).*', m.image) else b.wait_visiblefunc(".pf-v6-c-menu__item-main .pf-v6-c-menu__item-text:contains('DEFAULT:SHA1')")b.click("#crypto-policy-dialog button:contains('Cancel')")b.wait_not_present("#crypto-policy-dialog")# Test if a new subpolicy can be setnew_profile = "LEGACY:AD-SUPPORT"change_profile(profile, new_profile)profile = m.execute(cmd + " --show").strip()self.assertEqual(profile, new_profile)b.wait_text("#crypto-policy-button", shown_profile_text(profile))# Select a custom policy (non-selectable option)profile = "EMPTY"m.execute(cmd + f" --set {profile}")b.enter_page("/system")b.wait_text("#crypto-policy-button", shown_profile_text(profile))b.click("#crypto-policy-button")b.wait_in_text(".pf-v6-c-menu__item.pf-m-selected", shown_profile_text(profile))b.wait_in_text(".pf-v6-c-menu__item.pf-m-selected", "Custom cryptographic policy")b.click("#crypto-policy-dialog button.pf-v6-c-button.pf-m-link")# FIPS modeif self.supportsFIPS:change_profile(profile, "FIPS")else:# FIPS is not an available choiceb.click("#crypto-policy-button")b.wait_in_text(".pf-v6-c-menu__item.pf-m-selected", shown_profile_text(profile))self.assertNotIn("FIPS", b.text("#crypto-policy-dialog"))b.click("#crypto-policy-dialog button:contains('Cancel')")b.wait_not_present("#crypto-policy-dialog")# pretend the machine already has FIPS enabled, then it can't be disabledm.execute(cmd + " --set FIPS") # this is known incomplete/invalid, but suffices to set the UI stateb.wait_text("#crypto-policy-current", "FIPS")self.assertFalse(b.is_present("#crypto-policy-button"))m.execute(cmd + f" --set {profile}") # this is known incomplete/invalid, but suffices to set the UI stateb.wait_text("#crypto-policy-button", shown_profile_text(profile))@testlib.skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch")@testlib.skipOstree("crypto-policies not available")def testInconsistentCryptoPolicy(self):m = self.machineb = self.browsercmd = "update-crypto-policies"# Admin sets FIPS crypto policy in terminal, but FIPS mode is disabledm.execute(cmd + " --set FIPS")self.login_and_go("/system")b.wait_text("#inconsistent_crypto_policy", "FIPS is not properly enabled")b.click(".system-health-crypto-policies button.pf-v6-c-button.pf-m-link")if self.supportsFIPS:# fix the FIPS policyb.wait_in_text(".pf-v6-c-menu__item.pf-m-selected .pf-v6-c-label.pf-m-orange", "inconsistent")else:# pick any valid non-FIPS policyb.click("#crypto-policy-dialog .pf-v6-c-menu__list-item[data-value='DEFAULT'] button")b.click("#crypto-policy-save-reboot")# Initramfs re-generation takes a whileself.wait_reboot(timeout_sec=600)m.start_cockpit()self.login_and_go("/system")if self.supportsFIPS:b.wait_text("#crypto-policy-button", "FIPS")self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "1")else:b.wait_text("#crypto-policy-button", "Default")# rest of the test does not apply any morereturnm.execute(cmd + " --set DEFAULT")b.wait_text("#inconsistent_crypto_policy", "Cryptographic policy is inconsistent")m.execute(cmd + " --set FIPS:OSPP")b.wait_text("#crypto-policy-button", "FIPS:OSPP")b.wait_not_present("#inconsistent_crypto_policy")# Setting via dialogm.execute(cmd + " --set DEFAULT")b.wait_text("#inconsistent_crypto_policy", "Cryptographic policy is inconsistent")b.click(".system-health-crypto-policies button.pf-v6-c-button.pf-m-link")b.wait_in_text(".pf-v6-c-menu__item.pf-m-selected .pf-v6-c-label.pf-m-orange", "inconsistent")b.click("#crypto-policy-save-reboot")# Initramfs re-generation takes a whileself.wait_reboot(timeout_sec=600)m.start_cockpit()self.login_and_go("/system")b.wait_text("#crypto-policy-button", "Default")self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "0")class TestSystemInfoTime(packagelib.PackageCase):def set_change_time_dialog_mode(self, mode: str) -> None:b = self.browserb.click("#change_systime_btn")b.click(f"#change_systime button:contains('{mode}')")b.wait_in_text("#system_information_change_systime #change_systime_btn", mode)def testTime(self):m = self.machineb = self.browserdef ntp_enabled():return 'true' in m.execute('busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 org.freedesktop.timedate1 NTP')# make sure system is on expected timezone EESTm.execute("timedatectl set-timezone Europe/Helsinki")# Something gets confused when systemd-timesyncd isn't# available. This is harmless.#self.allow_journal_messages("org.freedesktop.systemd1: couldn't get property org.freedesktop.systemd1.Service ExecMain ""at /org/freedesktop/systemd1/unit/systemd_2dtimedated_2eservice: ""GDBus.Error:org.freedesktop.DBus.Error.UnknownProperty.*")# journal gets confused with time jumpsself.allow_journal_messages(r"Journal file .*\.journal corrupted, ignoring file.*")self.login_and_go("/system", superuser=False)b.wait_text_not("#system_information_systime_button", "")b.wait_visible('#system_information_systime_button[disabled]')# Gain admin accessb.become_superuser()# Change the dateb.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")self.set_change_time_dialog_mode("Manually")b.set_input_text("#systime-date-input input", "2037-01-24")# invalid timeb.set_input_text("#systime-time-input-input", "25:61")b.click("#system_information_change_systime .apply")b.wait_text("#systime-manual-row .dialog-error", "Invalid time format")# valid timeb.set_input_text("#systime-time-input-input", "08:03")# wait until icon settles downb.wait_visible("#systime-time-input-input[aria-invalid='false']")b.wait_not_present("#systime-manual-row .dialog-error")b.assert_pixels("#system_information_change_systime", "systime-manual-time")b.click("#system_information_change_systime .apply")with b.wait_timeout(60):b.wait_not_present("#system_information_change_systime")b.wait_text("#system_information_systime_button", "Jan 24, 2037, 8:03 AM")self.assertFalse(ntp_enabled())self.assertIn("Sat Jan 24 08:03:", m.execute("date"))self.assertIn("EET 2037\n", m.execute("date"))# Set to NTPb.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")self.set_change_time_dialog_mode("Automatically using NTP")b.click("#system_information_change_systime .apply")with b.wait_timeout(60):b.wait_not_present("#system_information_change_systime")testlib.wait(ntp_enabled)# Change the dateb.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")self.set_change_time_dialog_mode("Manually")b.set_input_text("#systime-date-input input", "2050-06-04")b.set_input_text("#systime-time-input-input", "06:34")b.click("#system_information_change_systime .apply")with b.wait_timeout(120): # Changing time on Arch can be slowb.wait_not_present("#system_information_change_systime")self.assertFalse(ntp_enabled())self.assertIn("Sat Jun 4 06:34:", m.execute("date"))self.assertIn("EEST 2050\n", m.execute("date"))@testlib.skipImage("timesyncd not available", "rhel*", "centos-*")@testlib.skipImage("TODO: fix for OpenSUSE", "opensuse-tumbleweed")def testTimeServersTimesyncd(self):m = self.machineb = self.browser# run this test with a tight umask to check proper file permissions (timesyncd runs as non-root)self.sed_file(r"/^UMASK/ s/0../077/", "/etc/login.defs")if m.image.startswith("debian") or m.image.startswith("ubuntu") or m.image == "arch":if m.execute("type chronyc || true").strip() != "":# chronyd is default, install timesyncdself.addPackageSet("timesyncd")self.enableRepo()m.execute("apt-get update; apt-get install -y systemd-timesyncd")m.execute("systemctl restart systemd-timedated; timedatectl set-ntp off; timedatectl set-ntp on")else:# timesyncd is defaultpasselse:# chronyd is default, give priority to timesyncdself.write_file("/etc/systemd/ntp-units.d/10-test.list", "systemd-timesyncd.service")conf = "/etc/systemd/timesyncd.conf.d/50-cockpit.conf"self.login_and_go("/system")# Wait until everything is ready to go...b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")b.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")def get_timesyncd_start():return int(m.execute("systemctl show -p ExecMainStartTimestampMonotonic --value systemd-timesyncd").strip())prev_timesyncd_start = get_timesyncd_start()# Add two NTP servers.self.set_change_time_dialog_mode("Automatically using specific NTP servers")b.set_input_text("#systime-ntp-servers div:nth-child(1) input", "0.ntp.example.com")b.click('#systime-ntp-servers div:nth-child(1) button')b.set_input_text("#systime-ntp-servers div:nth-child(2) input", "1.ntp.example.com")b.click("#system_information_change_systime .apply")with b.wait_timeout(120): # Changing time on Arch can be slowb.wait_not_present("#system_information_change_systime")self.assertIn("0.ntp.example.com", m.execute(f"grep '^NTP=' {conf}"))self.assertIn("1.ntp.example.com", m.execute(f"grep '^NTP=' {conf}"))# Cockpit created the file with the correct permissionsself.assertEqual(m.execute(f"stat --format '%a' {conf}").strip(), "644")# restarts timesyncd to pick up the new configtestlib.wait(lambda: get_timesyncd_start() > prev_timesyncd_start, delay=0.2)prev_timesyncd_start = get_timesyncd_start()# not predictable which of 0 or 1 timesyncd picks, but it should be one of thoseself.assertIn(".ntp.example.com", m.execute("timedatectl timesync-status"))# Set conf from the outside, check that we pick that up, and# switch to default servers.m.write(conf, "[Time]\nNTP=2.ntp.example.com\n")b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")b.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")b.wait_val("#systime-ntp-servers div:nth-child(1) input", "2.ntp.example.com")self.set_change_time_dialog_mode("Automatically using NTP")b.wait_not_present("#systime-ntp-servers")b.click("#system_information_change_systime .apply")with b.wait_timeout(120): # Changing time on Arch can be slowb.wait_not_present("#system_information_change_systime")self.assertIn("2.ntp.example.com", m.execute(f"grep '^#NTP=' {conf}"))# restarts timesyncd to pick up the new configtestlib.wait(lambda: get_timesyncd_start() > prev_timesyncd_start, delay=0.2)self.assertNotIn("example.com", m.execute("timedatectl timesync-status"))@testlib.skipImage("chronyd not available", "arch")@testlib.skipImage("TODO: fix for OpenSUSE", "opensuse-tumbleweed")def testTimeServersChronyd(self):m = self.machineb = self.browser# run this test with a tight umask to check proper file permissions (chronyd runs as non-root)self.sed_file(r"/^UMASK/ s/0../077/", "/etc/login.defs")enabled_conf = "/etc/chrony/sources.d/cockpit.sources"disabled_conf = "/etc/chrony/sources.d/cockpit.disabled"if m.image.startswith("debian") or m.image.startswith("ubuntu"):# timesyncd is default, install chronydself.addPackageSet("chronyd")self.enableRepo()m.execute("apt-get update; apt-get install -y chrony")m.execute("systemctl restart systemd-timedated; timedatectl set-ntp off; timedatectl set-ntp on")else:# chronyd is defaultpassself.login_and_go("/system")# Wait until everything is ready to go...b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")b.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")def get_chronyd_start():return int(m.execute("systemctl show -p ExecMainStartTimestampMonotonic --value chronyd").strip())prev_chronyd_start = get_chronyd_start()# Add two NTP servers.self.set_change_time_dialog_mode("Automatically using additional NTP servers")b.set_input_text("#systime-ntp-servers div:nth-child(1) input", "0.ntp.example.com")b.click('#systime-ntp-servers div:nth-child(1) button')b.set_input_text("#systime-ntp-servers div:nth-child(2) input", "1.ntp.example.com")b.click("#system_information_change_systime .apply")with b.wait_timeout(60):b.wait_not_present("#system_information_change_systime")m.execute(f"grep 0.ntp.example.com {enabled_conf}")m.execute(f"grep 1.ntp.example.com {enabled_conf}")m.execute(f"! test -f {disabled_conf}")# Cockpit created the file with the correct permissionsself.assertEqual(m.execute(f"stat --format '%a' {enabled_conf}").strip(), "644")self.assertEqual(m.execute("stat --format '%a' /etc/chrony /etc/chrony/sources.d").strip(), "755\n755")# restarts chronyd to pick up the new configtestlib.wait(lambda: get_chronyd_start() > prev_chronyd_start, delay=0.2)prev_chronyd_start = get_chronyd_start()# Set conf from the outside, check that we pick that up, and# switch to default servers.m.write(enabled_conf, "server 2.ntp.example.com\n")b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")b.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")b.wait_val("#systime-ntp-servers div:nth-child(1) input", "2.ntp.example.com")self.set_change_time_dialog_mode("Automatically using NTP")b.wait_not_present("#systime-ntp-servers")b.click("#system_information_change_systime .apply")with b.wait_timeout(60):b.wait_not_present("#system_information_change_systime")m.execute(f"! test -f {enabled_conf}")m.execute(f"grep 2.ntp.example.com {disabled_conf}")# restarts timesyncd to pick up the new configtestlib.wait(lambda: get_chronyd_start() > prev_chronyd_start, delay=0.2)def testTimeServersUnsupported(self):m = self.machineb = self.browserm.execute("! systemctl is-active chronyd || systemctl stop chronyd")m.execute("systemctl mask chronyd.service || systemctl mask chrony.service")m.execute("systemctl mask --now systemd-timesyncd.service")self.login_and_go("/system")# Wait until everything is ready to go...b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")b.click("#system_information_systime_button")b.wait_visible("#system_information_change_systime")b.click("#system_information_change_systime #change_systime_btn")b.wait_in_text("#change_systime", "Automatically using NTP")b.wait_not_present("#change_systime button:contains('Automatically using specific NTP servers')")b.wait_not_present("#change_systime button:contains('Automatically using additional NTP servers')")class TestSystemInfoSubscribed(submanlib.SubscriptionCase):provision = {"0": {"address": "10.111.112.1/20", "dns": "10.111.112.1"},"services": {"image": "services"}}@testlib.onlyImage("insights-client is only on RHEL", "rhel*")@testlib.skipBeiboot("no local overrides/config in beiboot mode")def testInsightsStatus(self):m = self.machineb = self.browser# insights-client requires a valid subscriptionself.setup_candlepin_service(self.machines['services'])self.register_with_candlepin()# insights-client might get started during boot and might then# run concurrently with our explicit "insights-client# --register" below. insights-client is not designed to be# run concurrently and there is no protection against it,# apparently. So let's prevent that.m.execute("systemctl disable --now insights-client")# Pretend that the Subscriptions page can do Insights stuffself.write_file("/etc/cockpit/subscription-manager.override.json", '{ "features": { "insights": true } }')# Run a mock version of the Insights API locally and configure# insights-client to access it. That requires a good enough# TLS mock insights server certificatem.upload(["verify/files/mock-insights", "../src/tls/ca/alice.key", "../src/tls/ca/alice.pem"], self.vm_tmpdir)pid = m.spawn(f"{self.vm_tmpdir}/mock-insights", "mock-insights")self.addCleanup(m.execute, f"kill {pid}")m.execute("while ! ss -tulpn | grep 8443; do sleep 1; done")hostname = m.execute("hostname").rstrip()self.write_file("/etc/insights-client/insights-client.conf", f"""[insights-client]auto_config=Falseauto_update=Falsebase_url={hostname}:8443/r/insightscert_verify=/var/lib/insights/mock-certs/ca.crtusername=adminpassword=foobar""")# Initially we are not registeredself.login_and_go('/system')b.wait_text(".system-health-insights a", "Not connected to Insights")# Enable insights, results should appear automaticallym.execute("insights-client --register", timeout=180)self.addCleanup(m.execute, "insights-client --unregister")with b.wait_timeout(60):b.wait_in_text(".system-health-insights a", "Connected to Insights")b.logout()if __name__ == '__main__':testlib.test_main()