1091 lines
50 KiB
Python
Executable File
1091 lines
50 KiB
Python
Executable File
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
|
|
|
|
# This file is part of Cockpit.
|
|
#
|
|
# Copyright (C) 2013 Red Hat, Inc.
|
|
#
|
|
# Cockpit is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation; either version 2.1 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Cockpit is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import re
|
|
import time
|
|
|
|
from packagelib import PackageCase
|
|
from testlib import enableAxe, nondestructive, onlyImage, skipDistroPackage, skipImage, skipOstree, test_main, wait
|
|
|
|
os_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/sh
|
|
echo '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'
|
|
"""
|
|
|
|
|
|
def ssh_reconnect(machine, timeout_sec=120):
|
|
start_time = time.time()
|
|
error = None
|
|
while (time.time() - start_time) < timeout_sec:
|
|
try:
|
|
machine.execute("true", quiet=True)
|
|
return
|
|
except Exception as e:
|
|
error = e
|
|
time.sleep(0.5)
|
|
|
|
raise error
|
|
|
|
|
|
@skipDistroPackage()
|
|
class TestSystemInfo(PackageCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Most OSes don't set nosmt by default, but there are some exceptions
|
|
self.expect_smt_default = self.machine.image in ["fedora-coreos"]
|
|
|
|
@enableAxe
|
|
def testBasic(self):
|
|
m = self.machine
|
|
b = 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)")
|
|
|
|
self.login_and_go("/system")
|
|
|
|
b.wait_visible('#system_information_os_text')
|
|
|
|
mid = m.execute("cat /etc/machine-id")
|
|
b.wait_text('#system_machine_id', mid)
|
|
|
|
self.check_axe()
|
|
|
|
# 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 tests
|
|
m.execute("systemctl reset-failed")
|
|
b.wait_not_present("#page_status_notification_system_services")
|
|
|
|
# ensure general page/card layout without the changing specifics
|
|
b.assert_pixels("#overview", "overview", ignore=[
|
|
".system-health .pf-v5-c-card__body",
|
|
"#system_machine_id",
|
|
"#system_uptime",
|
|
# #system_information_systime_button is not enough, need to grab the icon as well
|
|
"tr:contains('System time') td",
|
|
# CPU/memory metrics
|
|
"#system-usage-cpu-progress + td",
|
|
"#system-usage-memory-progress + td",
|
|
"#tuned-status-button",
|
|
])
|
|
|
|
# Generate a new rsa key and change the config
|
|
m.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-v5-c-list", "ED25519")
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "RSA")
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "ECDSA")
|
|
b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", new_default)
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", old_default)
|
|
b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", new_alt)
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-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 restart
|
|
self.sed_file(r"s,.*HostKey *,#,; $ a HostKey /etc/ssh/weirdname", "/etc/ssh/sshd_config",
|
|
# Restart sshd but stop socket so we can make sure we are restarted
|
|
"( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
|
|
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-v5-c-list", "ED25519")
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "RSA")
|
|
b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", "ECDSA")
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", new_default)
|
|
b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", old_default)
|
|
b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", old_alt)
|
|
b.wait_in_text("#system_information_ssh_keys .pf-v5-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")
|
|
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 Box (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")
|
|
b.wait_text('#system_machine_id', mid)
|
|
|
|
# uptime (introduced in PR #13885)
|
|
b.wait_text_not("#system_uptime", "")
|
|
# replace it with a known value, it should automatically update every minute
|
|
m.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")
|
|
# 4 months and a bit, timeformat rounds quite aggressively; also, test a slightly different format
|
|
m.write("/tmp/fake_uptime", "10370000 12345.30\n")
|
|
with b.wait_timeout(70):
|
|
b.wait_text("#system_uptime", "4 months")
|
|
|
|
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: .*")
|
|
|
|
def set_change_time_dialog_mode(self, mode):
|
|
b = self.browser
|
|
b.click("#system_information_change_systime .pf-v5-c-form__group-label:contains('Set time') + div > .pf-v5-c-select > button")
|
|
b.click(f"#change_systime button:contains('{mode}')")
|
|
b.wait_in_text("#system_information_change_systime .pf-v5-c-form__group-label:contains('Set time') + div > .pf-v5-c-select > button", mode)
|
|
|
|
def testTime(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
def 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 EEST
|
|
m.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 jumps
|
|
self.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 access
|
|
b.click(".pf-v5-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
|
|
b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
|
|
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
|
|
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
|
|
b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
|
|
b.wait_not_present(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
|
|
|
|
# Change the date
|
|
b.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 time
|
|
b.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 time
|
|
b.set_input_text("#systime-time-input-input", "08:03")
|
|
# wait until icon settles down
|
|
b.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")
|
|
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 NTP
|
|
b.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")
|
|
b.wait_not_present("#system_information_change_systime")
|
|
|
|
wait(ntp_enabled)
|
|
|
|
# Change the date
|
|
b.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", "2018-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 slow
|
|
b.wait_not_present("#system_information_change_systime")
|
|
|
|
self.assertFalse(ntp_enabled())
|
|
self.assertIn("Mon Jun 4 06:34:", m.execute("date"))
|
|
self.assertIn("EEST 2018\n", m.execute("date"))
|
|
|
|
@skipImage("timesyncd not available", "rhel*", "centos-*")
|
|
def testTimeServersTimesyncd(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
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 timesyncd
|
|
self.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 default
|
|
pass
|
|
else:
|
|
# chronyd is default, give priority to timesyncd
|
|
self.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.pool.ntp.org")
|
|
b.click('#systime-ntp-servers div:nth-child(1) button')
|
|
b.set_input_text("#systime-ntp-servers div:nth-child(2) input", "1.pool.ntp.org")
|
|
b.click("#system_information_change_systime .apply")
|
|
with b.wait_timeout(120): # Changing time on Arch can be slow
|
|
b.wait_not_present("#system_information_change_systime")
|
|
|
|
self.assertIn("0.pool.ntp.org", m.execute(f"grep '^NTP=' {conf}"))
|
|
self.assertIn("1.pool.ntp.org", m.execute(f"grep '^NTP=' {conf}"))
|
|
|
|
# restarts timesyncd to pick up the new config
|
|
wait(lambda: get_timesyncd_start() > prev_timesyncd_start, delay=0.2)
|
|
prev_timesyncd_start = get_timesyncd_start()
|
|
|
|
# Set conf from the outside, check that we pick that up, and
|
|
# switch to default servers.
|
|
m.write(conf, "[Time]\nNTP=2.pool.ntp.org\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.pool.ntp.org")
|
|
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 slow
|
|
b.wait_not_present("#system_information_change_systime")
|
|
|
|
self.assertIn("2.pool.ntp.org", m.execute(f"grep '^#NTP=' {conf}"))
|
|
|
|
# restarts timesyncd to pick up the new config
|
|
wait(lambda: get_timesyncd_start() > prev_timesyncd_start, delay=0.2)
|
|
|
|
@skipImage("chronyd not available", "arch")
|
|
def testTimeServersChronyd(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
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 chronyd
|
|
self.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 default
|
|
pass
|
|
|
|
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_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.pool.ntp.org")
|
|
b.click('#systime-ntp-servers div:nth-child(1) button')
|
|
b.set_input_text("#systime-ntp-servers div:nth-child(2) input", "1.pool.ntp.org")
|
|
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.pool.ntp.org {enabled_conf}")
|
|
m.execute(f"grep 1.pool.ntp.org {enabled_conf}")
|
|
m.execute(f"! test -f {disabled_conf}")
|
|
|
|
# restarts chronyd to pick up the new config
|
|
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.pool.ntp.org\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.pool.ntp.org")
|
|
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.pool.ntp.org {disabled_conf}")
|
|
|
|
# restarts timesyncd to pick up the new config
|
|
wait(lambda: get_chronyd_start() > prev_chronyd_start, delay=0.2)
|
|
|
|
def testTimeServersUnsupported(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
m.execute("! systemctl is-active chronyd || systemctl stop chronyd")
|
|
m.execute("! systemctl is-active systemd-timesyncd || systemctl stop systemd-timesyncd")
|
|
m.execute("systemctl mask chronyd.service || systemctl mask chrony.service")
|
|
m.execute("systemctl mask 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 .pf-v5-c-form__group-label:contains('Set time') + div > .pf-v5-c-select > button")
|
|
b.wait_visible("#change_systime button:contains('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')")
|
|
|
|
@nondestructive
|
|
def testMotd(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
self.restore_file("/etc/motd")
|
|
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")
|
|
b.wait_visible('#motd-box')
|
|
# strips empty lines, but not leading spaces
|
|
b.wait_text('#motd', " Hello\n World")
|
|
|
|
b.assert_pixels("#motd-box", "motd")
|
|
|
|
b.click('#motd-box button:not(#motd-box-edit)')
|
|
b.wait_not_present('#motd-box')
|
|
|
|
# motd should stay dismissed after a reload
|
|
b.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 button
|
|
b.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())
|
|
|
|
@nondestructive
|
|
def testHardwareInfo(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.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 info
|
|
b.wait_in_text('#hwinfo-system-info-list', "CPU")
|
|
# QEMU VM type
|
|
b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(1) dd', "Other")
|
|
# Name
|
|
b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(2) dd', "Standard PC")
|
|
# BIOS
|
|
b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(1) dd', "SeaBIOS")
|
|
# BIOS date gets parsed
|
|
parsed_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-v5-c-description-list__group:nth-of-type(3) dd', parsed_bios_date)
|
|
|
|
pci_selector = '#hwinfo #pci-listing'
|
|
heading_selector = ' .pf-v5-c-card__title'
|
|
# PCI
|
|
b.wait_in_text(pci_selector + heading_selector, "PCI")
|
|
|
|
b.wait_in_text(pci_selector + ' tr:first-of-type td[data-label=Slot]', "0000:00:00.0")
|
|
|
|
# sorted by device class by default; this makes some assumptions about QEMU devices
|
|
b.wait_in_text(pci_selector + ' tbody tr:first-of-type td[data-label=Class]', "Bridge")
|
|
b.wait_in_text(pci_selector + ' tbody tr:last-of-type td[data-label=Class]', "Unclassified")
|
|
|
|
# sort by model
|
|
b.click(pci_selector + ' thead th:nth-child(2) button')
|
|
b.wait_in_text(pci_selector + ' tbody tr:first-of-type td[data-label=Model]', "440")
|
|
b.wait_in_text(pci_selector + ' tbody tr:last-of-type td[data-label=Model]', "Virtio SCSI")
|
|
b.wait_not_in_text(pci_selector + ' tbody tr:last-of-type td[data-label=Model]', "Unclassified")
|
|
|
|
# go back to system page
|
|
b.click('.pf-v5-c-breadcrumb li:first')
|
|
|
|
b.enter_page("/system")
|
|
|
|
# now pretend this is a system without DMI
|
|
b.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 hidden
|
|
b.wait_not_present('#system_information_asset_tag_text')
|
|
# Hardware should be hidden
|
|
b.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 fields
|
|
b.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 shown
|
|
b.wait_in_text(pci_selector + heading_selector, "PCI")
|
|
b.wait_in_text(pci_selector + ' tr:first-of-type td[data-label=Slot]', "0000:00:00.0")
|
|
|
|
# Check also variants when only some fields are present
|
|
m.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-v5-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-v5-c-description-list__group:nth-of-type(3) dd', "VENDOR")
|
|
|
|
# Clean up after lazy OEMs, falls back to board vendor/name
|
|
m.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/board_vendor", "brdven")
|
|
m.write("/sys/class/dmi/id/board_name", "brdnam")
|
|
b.reload()
|
|
b.go("/system")
|
|
b.enter_page('/system')
|
|
b.wait_in_text('#system_information_hardware_text', "brdven brdnam")
|
|
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-v5-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-v5-c-description-list__group:nth-of-type(3) dd', "brdven")
|
|
|
|
# /proc/cpuinfo on x86; very incomplete, just what pkg/lib/machine-info.js looks at
|
|
m.write("/tmp/cpuinfo", """processor\t: 0
|
|
vendor_id\t: GenuineIntel
|
|
model\t\t: 42
|
|
model name\t: Professor NumberCrunch
|
|
|
|
processor\t: 1
|
|
vendor_id\t: GenuineIntel
|
|
model\t\t: 42
|
|
model 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-v5-c-description-list__group:nth-of-type(1) dd', "2x Professor NumberCrunch")
|
|
|
|
# /proc/cpuinfo on PowerPC; complete info
|
|
m.write("/tmp/cpuinfo", """processor\t: 0
|
|
cpu\t\t: POWER9 (architected), altivec supported
|
|
clock\t\t: 3000.000000MHz
|
|
revision\t: 2.3 (pvr 004e 1203)
|
|
|
|
processor\t: 1
|
|
cpu\t\t: POWER9 (architected), altivec supported
|
|
clock\t\t: 3000.000000MHz
|
|
revision\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-v5-c-description-list__group:nth-of-type(1) dd', "2x POWER9 (architected), altivec supported")
|
|
|
|
# correct CPU count on overview
|
|
b.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 : 2
|
|
bogomips per cpu: 3241.00
|
|
max thread id : 0
|
|
features : esan3 zarch stfle msa ldisp eimm dfp edat etf3eh highgprs te vx vxd vxe gs vxe2 vxp sort dflt sie
|
|
processor 0: version = FF, identification = 2EB428, machine = 8561
|
|
processor 1: version = FF, identification = 2EB428, machine = 8561
|
|
|
|
cpu number : 0
|
|
cpu cores : 1
|
|
version : FF
|
|
identification : 2EB428
|
|
machine : 8561
|
|
|
|
cpu number : 1
|
|
cpu cores : 1
|
|
version : FF
|
|
identification : 2EB428
|
|
machine : 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-v5-c-description-list__group:nth-of-type(1) dd', "2x IBM/S390")
|
|
|
|
# umount mocked /sys/class/dmi/id
|
|
m.execute("umount /sys/class/dmi/id")
|
|
m.execute("udevadm trigger --verbose /sys/devices/virtual/dmi/id")
|
|
b.reload()
|
|
b.go("/system/hwinfo")
|
|
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')
|
|
distros_without_systemd_memory_dmi = ['rhel-8-7', 'rhel-8-8', 'rhel-8-9', 'centos-8-stream', 'debian-stable']
|
|
|
|
# Test more specific memory data with a fake dmidecode
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=ID]', "BANK 0: ChannelA-DIMM0")
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Type]', "DDR4")
|
|
if m.image in distros_without_systemd_memory_dmi:
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Size]', "4 GB")
|
|
else:
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Size]', "4 GiB")
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=State]', "Present")
|
|
b.wait_text('#memory-listing tr:nth-of-type(1) td[data-label="Memory technology"]', "Unknown")
|
|
b.wait_text('#memory-listing tr:nth-of-type(1) td[data-label=Rank]', "Single rank")
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Speed]', "2400 MT/s")
|
|
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(2) td[data-label=ID]', "BANK 2: ChannelB-DIMM0")
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(2) td[data-label=Type]', "DDR4")
|
|
if m.image in distros_without_systemd_memory_dmi:
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Size]', "4 GB")
|
|
else:
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Size]', "4 GiB")
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(2) td[data-label=State]', "Present")
|
|
b.wait_text('#memory-listing tr:nth-of-type(2) td[data-label="Memory technology"]', "Unknown")
|
|
b.wait_text('#memory-listing tr:nth-of-type(2) td[data-label=Rank]', "Single rank")
|
|
b.wait_in_text('#memory-listing tr:nth-of-type(2) td[data-label=Speed]', "2400 MT/s")
|
|
|
|
@ nondestructive
|
|
def testCPUSecurityMitigationsDetect(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.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_visible('#cpu-mitigations-dialog .nosmt-heading:contains(nosmt)')
|
|
b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
|
|
(expect_smt_state and ":checked" or ":not(:checked)"))
|
|
|
|
b.logout()
|
|
finally:
|
|
if cmdline:
|
|
m.execute('while ! umount /proc/cmdline; do sleep 1; done')
|
|
|
|
spoof_threads(1, False)
|
|
spoof_threads(2, True, True, 'param1 param2 nosmt param3=value3')
|
|
spoof_threads(2, True, True, 'param1 param2 nosmt=force param3=value3')
|
|
spoof_threads(2, True, True, 'param1 mitigations=auto,nosmt param3=value3')
|
|
spoof_threads(2, True, True, 'param1 mitigations=nosmt,something param3=value3')
|
|
spoof_threads(2, True, False, 'param1 mitigations=something param3=value3')
|
|
spoof_threads(2, False, cmdline='param1 nosmt=someunknown param3=value3')
|
|
spoof_threads(2, True, self.expect_smt_default, None)
|
|
|
|
@skipImage("TODO: add Arch Linux grub entry support", "arch")
|
|
def testCPUSecurityMitigationsEnable(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# spoof SMT
|
|
m.write('/usr/local/bin/lscpu', lscpu.format(2))
|
|
m.execute('chmod +x /usr/local/bin/lscpu')
|
|
|
|
# Switch nosmt option
|
|
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' +
|
|
(self.expect_smt_default and ':not(:checked)' or ':checked'))
|
|
b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
|
|
|
|
m.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"""set -e
|
|
touch /boot/vmlinuz-42.0.0; mkdir -p /lib/modules/42.0.0/
|
|
if type update-grub >/dev/null 2>&1; then
|
|
update-grub # Debian/Ubuntu
|
|
grep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfg
|
|
else
|
|
cp -a /boot/grub2/grubenv /boot/grub2/grubenv.prev
|
|
kernel-install add 42.0.0 /boot/vmlinuz-42.0.0 2>/dev/null
|
|
grep -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 works
|
|
m.execute(r"""set -e
|
|
rm /boot/vmlinuz-42.0.0
|
|
if type update-grub >/dev/null 2>&1; then
|
|
update-grub # Debian/Ubuntu
|
|
else
|
|
kernel-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=2078379
|
|
mv /boot/grub2/grubenv.prev /boot/grub2/grubenv
|
|
fi
|
|
""")
|
|
|
|
# Switch back nosmt option
|
|
self.login_and_go('/system/hwinfo')
|
|
b.click('#hwinfo button:contains(Mitigations)')
|
|
b.wait_visible('#cpu-mitigations-dialog .nosmt-heading:contains(nosmt)')
|
|
b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
|
|
(self.expect_smt_default and ':not(:checked)' or ':checked'))
|
|
b.click('#cpu-mitigations-dialog #nosmt-switch input')
|
|
b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
|
|
(self.expect_smt_default and ':checked' or ':not(:checked)'))
|
|
b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
|
|
m.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 present
|
|
m.upload(["../pkg/systemd/kernelopt.sh"], "/tmp/")
|
|
m.execute("/tmp/kernelopt.sh remove nosmt; /tmp/kernelopt.sh set mitigations=auto,nosmt")
|
|
m.reboot()
|
|
self.login_and_go('/system/hwinfo')
|
|
b.click('#hwinfo button:contains(Mitigations)')
|
|
b.wait_visible('#cpu-mitigations-dialog .nosmt-heading:contains(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)')
|
|
m.wait_reboot()
|
|
self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))
|
|
self.assertIn('mitigations=auto', m.execute('cat /proc/cmdline'))
|
|
|
|
# Behaviour for non-admins
|
|
self.login_and_go('/system/hwinfo', superuser=False)
|
|
b.wait_visible('#cpu_mitigations[disabled]')
|
|
b.mouse('#tip-cpu-security', 'mouseenter')
|
|
b.wait_text('.pf-v5-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-v5-c-tooltip")
|
|
|
|
# Behaviour if grub update tools are missing
|
|
b.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.execute('systemctl stop rpm-ostreed.service || true; systemctl mask 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_visible('#cpu-mitigations-dialog .pf-v5-c-alert__title:contains(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')
|
|
|
|
@onlyImage("insights-client is only on RHEL", "rhel*")
|
|
@nondestructive
|
|
def testInsightsStatus(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# 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")
|
|
|
|
self.restore_dir("/etc/insights-client")
|
|
self.restore_dir("/etc/yum")
|
|
self.restore_dir("/var/lib/insights")
|
|
|
|
# Pretend that the Subscriptions page can do Insights stuff
|
|
self.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 certificate
|
|
m.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=False
|
|
auto_update=False
|
|
base_url={hostname}:8443/r/insights
|
|
cert_verify=/var/lib/insights/mock-certs/ca.crt
|
|
username=admin
|
|
password=foobar
|
|
""")
|
|
|
|
# Initially we are not registered
|
|
self.login_and_go('/system')
|
|
b.wait_text(".system-health-insights a", "Not connected to Insights")
|
|
|
|
# Enable insights, results should appear automatically
|
|
m.execute("insights-client --register")
|
|
self.addCleanup(m.execute, "insights-client --unregister")
|
|
with b.wait_timeout(60):
|
|
b.wait_in_text(".system-health-insights a", "3 hits, including important")
|
|
self.assertIn("123-nice-id", b.attr(".system-health-insights a", "href"))
|
|
|
|
# Switch to limited access, insights status will disappear completely
|
|
b.drop_superuser()
|
|
b.wait_not_present(".system-health-insights")
|
|
|
|
# Switch to admin access, insights status will re-appear
|
|
b.become_superuser()
|
|
b.wait_in_text(".system-health-insights a", "3 hits, including important")
|
|
self.assertIn("123-nice-id", b.attr(".system-health-insights a", "href"))
|
|
|
|
def testOverview(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# packagekit often eats a lot of CPU; silence it to not screw up the "system is idle" test
|
|
m.execute("systemctl mask packagekit")
|
|
|
|
def progressValue(number):
|
|
sel = ".system-usage tr:nth-child(%i) .pf-v5-c-progress__indicator" % number
|
|
b.wait_visible(sel)
|
|
b.wait_attr_contains(sel, "style", "width:")
|
|
style = b.attr(sel, "style")
|
|
m = re.search(r"width: (\d+)%;", style)
|
|
return int(m.group(1))
|
|
|
|
self.login_and_go("/system")
|
|
|
|
# CPU
|
|
# first wait until system settles down
|
|
wait(lambda: progressValue(1) < 20)
|
|
m.spawn("for i in $(seq $(nproc)); do cat /dev/urandom > /dev/null & done", "cpu_hog.log")
|
|
wait(lambda: progressValue(1) > 75)
|
|
m.execute("pkill -e -f [c]at.*urandom")
|
|
# should go back to idle usage
|
|
# HACK: work around pmie CPU usage https://bugzilla.redhat.com/show_bug.cgi?id=2140572
|
|
wait(lambda: progressValue(1) < 20, tries=200)
|
|
|
|
# memory: our test machines should use a reasonable chunk of available memory; MiB or GiB
|
|
b.wait_in_text(".system-usage tr:nth-child(2)", "iB")
|
|
initial_usage = progressValue(2)
|
|
self.assertGreater(initial_usage, 10)
|
|
self.assertLess(initial_usage, 80)
|
|
# 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("MEMBLOB=$(yes | dd bs=1M count=200 iflag=fullblock); touch /tmp/hogged; sleep infinity; true", "mem_hog.log")
|
|
m.execute("while [ ! -e /tmp/hogged ]; do sleep 1; done")
|
|
# bars update every 5s
|
|
time.sleep(8)
|
|
hog_usage = progressValue(2)
|
|
self.assertGreater(hog_usage, initial_usage + 10)
|
|
|
|
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 significantly
|
|
wait(lambda: progressValue(2) <= hog_usage - 15)
|
|
self.assertGreater(progressValue(2), 10)
|
|
|
|
@nondestructive
|
|
def testShutdownStatus(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
self.login_and_go("/system")
|
|
b.wait_not_present("#system-health-shutdown-status")
|
|
|
|
# Schedule a reboot
|
|
m.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 text
|
|
b.reload()
|
|
b.enter_page("/system")
|
|
b.wait_in_text('#system-health-shutdown-status-text', "Scheduled reboot")
|
|
|
|
# Cancel
|
|
b.click("#system-health-shutdown-status-cancel-btn")
|
|
b.wait_not_present('#system-health-shutdown-status')
|
|
|
|
# Schedule a poweroff
|
|
m.execute("shutdown --poweroff +10")
|
|
b.wait_in_text('#system-health-shutdown-status-text', "Scheduled poweroff")
|
|
|
|
# Cancel
|
|
b.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())
|
|
|
|
@skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch")
|
|
@skipOstree("crypto-policies not available")
|
|
def testCryptoPolicies(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
self.allow_restart_journal_messages()
|
|
|
|
def shown_profile_text(profile):
|
|
return profile if profile == "FIPS" else profile[0] + profile[1:].lower()
|
|
|
|
def change_profile(profile, new_profile):
|
|
b.click("#crypto-policy-button")
|
|
b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", shown_profile_text(profile))
|
|
profile_button_name = shown_profile_text(new_profile)
|
|
b.click(f".pf-v5-c-menu__item-main .pf-v5-c-menu__item-text:contains('{profile_button_name}')")
|
|
b.click("#crypto-policy-save-reboot")
|
|
# Initramfs re-generation takes a while
|
|
m.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 has no SHA1 policy, so do not show it.
|
|
b.click("#crypto-policy-button")
|
|
func = b.wait_not_present if m.image.startswith('rhel-8') or m.image.startswith('centos-8') else b.wait_visible
|
|
func(".pf-v5-c-menu__item-main .pf-v5-c-menu__item-text:contains('Default:sha1')")
|
|
|
|
# Test if a new subpolicy can be set
|
|
new_profile = "LEGACY:AD-SUPPORT"
|
|
change_profile(profile, new_profile)
|
|
|
|
profile = m.execute(cmd + " --show").strip()
|
|
self.assertEquals(profile, new_profile)
|
|
b.wait_text("#crypto-policy-button", shown_profile_text(profile))
|
|
new_profile = "FIPS"
|
|
change_profile(profile, new_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-v5-c-menu__item.pf-m-selected", shown_profile_text(profile))
|
|
b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", "Custom crypto policy")
|
|
b.click("#crypto-policy-dialog button.pf-v5-c-button.pf-m-link")
|
|
|
|
@skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch")
|
|
@skipOstree("crypto-policies not available")
|
|
def testInconsistentCryptoPolicy(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
self.allow_restart_journal_messages()
|
|
cmd = "update-crypto-policies"
|
|
|
|
# Admin sets FIPS crypto policy in terminal, but FIPS mode is disabled
|
|
m.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-v5-c-button.pf-m-link")
|
|
b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected .pf-v5-c-label.pf-m-orange", "inconsistent")
|
|
b.click("#crypto-policy-save-reboot")
|
|
# Initramfs re-generation takes a while
|
|
m.wait_reboot(timeout_sec=600)
|
|
m.start_cockpit()
|
|
self.login_and_go("/system")
|
|
b.wait_text("#crypto-policy-button", "FIPS")
|
|
self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "1")
|
|
|
|
m.execute(cmd + " --set DEFAULT")
|
|
b.wait_text("#inconsistent_crypto_policy", "Crypto 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 dialog
|
|
m.execute(cmd + " --set DEFAULT")
|
|
b.wait_text("#inconsistent_crypto_policy", "Crypto policy is inconsistent")
|
|
b.click(".system-health-crypto-policies button.pf-v5-c-button.pf-m-link")
|
|
b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected .pf-v5-c-label.pf-m-orange", "inconsistent")
|
|
b.click("#crypto-policy-save-reboot")
|
|
m.wait_reboot()
|
|
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")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_main()
|