#!/usr/bin/python3

# 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 subprocess
import time

import parent
import packagelib
from testlib import *


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'
"""

dmidecode = """#!/bin/sh
[ "$1" = "-t" -a "$2" = "memory" ] || exit 1
cat <<EOF
# dmidecode 3.2
Getting SMBIOS data from sysfs.
SMBIOS 2.7 present.

Handle 0x0008, DMI type 17, 34 bytes
Memory Device
	Total Width: 64 bits
	Size: 8192 MB
	Form Factor: SODIMM
	Locator: ChannelA-DIMM0
	Bank Locator: BANK 0
	Type: DDR3
	Speed: 1600 MT/s
	Manufacturer: Samsung
	Asset Tag: None
	Rank: Unknown
	Configured Memory Speed: 1600 MT/s

Handle 0x0009, DMI type 17, 34 bytes
Memory Device
	Total Width: 64 bits
	Size: 8192 MB
	Form Factor: SODIMM
	Locator: ChannelB-DIMM0
	Bank Locator: BANK 2
	Memory Technology: Awesome
	Type: DDR3
	Speed: 1600 MT/s
	Manufacturer: Samsung
	Asset Tag: None
	Rank: 1
	Configured Memory Speed: 1600 MT/s
EOF
"""

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


class TestSystemInfo(MachineCase):
    def setUp(self):
        super().setUp()

        # Debian will not allow timesyncd to start if any other NTP packge is installed We only
        # support timesyncd for NTP configuration, so make sure chrony is not installed in the older
        # debian/ubuntu images which don't yet have systemd-timesyncd
        if self.machine.image in ["debian-stable", "ubuntu-stable"]:
            self.machine.execute("dpkg --remove chrony");

    @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()

        # 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")

        supports_alt = False
        try:
            m.execute(command="ssh-keygen -l -f /etc/ssh/weirdname -E md5", quiet=True)
            supports_alt = True
        except subprocess.CalledProcessError:
            pass

        if supports_alt:
            new_default = m.execute("ssh-keygen -l -f /etc/ssh/weirdname -E md5 | cut -d' ' -f2")
            new_alt = m.execute("ssh-keygen -l -f /etc/ssh/weirdname -E sha256 | cut -d' ' -f2")
            old_default = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key -E md5 | cut -d' ' -f2")
            old_alt = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key -E sha256 | cut -d' ' -f2")
        else:
            new_default = m.execute("ssh-keygen -l -f /etc/ssh/weirdname | cut -d' ' -f2")
            new_alt = None
            old_default = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key | cut -d' ' -f2")
            old_alt = None

        b.click("#system-ssh-keys-link")
        b.wait_popup("system_information_ssh_keys")
        b.wait_not_visible("#system_information_ssh_keys .spinner")
        b.wait_visible("#system_information_ssh_keys .content")
        b.wait_in_text("#system_information_ssh_keys .list-group", "ED25519")
        b.wait_in_text("#system_information_ssh_keys .list-group", "RSA")
        b.wait_in_text("#system_information_ssh_keys .list-group", "ECDSA")
        b.wait_not_in_text("#system_information_ssh_keys .list-group", new_default)
        b.wait_in_text("#system_information_ssh_keys .list-group", old_default)
        if supports_alt:
            b.wait_not_in_text("#system_information_ssh_keys .list-group", new_alt)
            b.wait_in_text("#system_information_ssh_keys .list-group", old_alt)

        b.click("#system_information_ssh_keys button")
        b.wait_popdown("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_popup("system_information_ssh_keys")
        b.wait_not_visible("#system_information_ssh_keys .spinner")
        b.wait_visible("#system_information_ssh_keys .content")
        b.wait_not_in_text("#system_information_ssh_keys .list-group", "ED25519")
        b.wait_in_text("#system_information_ssh_keys .list-group", "RSA")
        b.wait_not_in_text("#system_information_ssh_keys .list-group", "ECDSA")
        b.wait_in_text("#system_information_ssh_keys .list-group", new_default)
        b.wait_not_in_text("#system_information_ssh_keys .list-group", old_default)
        if supports_alt:
            b.wait_not_in_text("#system_information_ssh_keys .list-group", old_alt)
            b.wait_in_text("#system_information_ssh_keys .list-group", new_alt)

        b.wait_in_text('#system_information_os_text',
                    "Foobar Adventure Linux Server 2.0 (Day of Doom)")

        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_popup("system_information_change_hostname")
        b.wait_val("#sich-pretty-hostname", "Adventure Box")
        b.set_val("#sich-hostname", "host1.cockpit.lan")
        b.click("#system_information_change_hostname button:contains('Change')")
        b.wait_popdown("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")

        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)
        if m.image not in ["rhel-8-2-distropkg"]:
            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, moment humanize() 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: .*")

    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: Unknown property")

        self.login_and_go("/system")

        # Make sure the date loads
        b.wait_text_not("#system_information_systime_button", "")

        # Change the date
        b.click("#system_information_systime_button")
        b.wait_popup("system_information_change_systime")
        b.click("#change_systime button")
        b.click("#change_systime li[value=manual_time] a")
        b.wait_in_text("#change_systime button", "Manually")
        b.set_val("#systime-date-input", "2020-01-24")
        b.set_val("#systime-time-hours", "08")
        b.set_val("#systime-time-minutes", "03")
        b.click("#systime-apply-button")
        b.wait_popdown("system_information_change_systime")

        if m.image == "rhel-8-2-distropkg": # Changed in #13489
            b.wait_text("#system_information_systime_button", "2020-01-24 08:03")
        else:
            b.wait_text("#system_information_systime_button", "Jan 24, 2020 8:03 AM")

        self.assertFalse(ntp_enabled())
        self.assertIn("Fri Jan 24 08:03:", m.execute("date"))
        self.assertIn("EET 2020\n", m.execute("date"))

        # Set to NTP
        b.click("#system_information_systime_button")
        b.wait_popup("system_information_change_systime")
        b.click("#change_systime button")
        b.click("#change_systime li[value=ntp_time] a")
        b.wait_in_text("#change_systime button", "Automatically using NTP")
        b.click("#systime-apply-button")
        b.wait_popdown("system_information_change_systime")

        wait(ntp_enabled)

        # Change the date
        b.click("#system_information_systime_button")
        b.wait_popup("system_information_change_systime")
        b.click("#change_systime button")
        b.click("#change_systime li[value=manual_time] a")
        b.wait_in_text("#change_systime button", "Manually")
        b.set_val("#systime-date-input", "2018-06-04")
        b.set_val("#systime-time-hours", "06")
        b.set_val("#systime-time-minutes", "34")
        b.click("#systime-apply-button")
        b.wait_popdown("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"))

        self.allow_authorize_journal_messages()
        b.relogin('/system', authorized=False)
        tooltip_sel = '.tooltip-inner'
        b.wait_present('#system_information_systime_button[disabled]')
        b.eval_js('window.cockpit_permission = cockpit.permission({ admin: true })')
        b.wait_js_cond('window.cockpit_permission.allowed !== null')
        b.mouse('#systime-tooltip', 'mouseover')
        b.wait_text(tooltip_sel, 'The user admin is not permitted to change the system time')
        b.mouse('#systime-tooltip', 'mouseout')
        b.wait_not_present(tooltip_sel)

    @skipImage("Uses timedatex, no NTP servers config", "rhel-8-2", "rhel-8-2-distropkg", "rhel-8-3", "centos-8-stream")
    def testTimeServers(self):
        m = self.machine
        b = self.browser

        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")

        # Add two NTP servers.  We can't expect the servers to be used, so
        # we only test that they get added.
        b.click("#system_information_systime_button")
        b.wait_popup("system_information_change_systime")
        b.click("#change_systime button")
        b.click("#change_systime li[value=ntp_time_custom] a")
        b.wait_in_text("#change_systime button", "Automatically using specific NTP servers")
        b.set_val("#systime-ntp-servers form:nth-child(1) input", "0.pool.ntp.org")
        b.click('#systime-ntp-servers form:nth-child(1) button[data-action="add"]')
        b.set_val("#systime-ntp-servers form:nth-child(2) input", "1.pool.ntp.org")
        b.click("#systime-apply-button")
        b.wait_popdown("system_information_change_systime")

        self.assertIn("0.pool.ntp.org", m.execute("grep '^NTP=' %s" % conf))
        self.assertIn("1.pool.ntp.org", m.execute("grep '^NTP=' %s" % conf))

        # 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.click("#system_information_systime_button")
        b.wait_popup("system_information_change_systime")
        b.wait_val("#systime-ntp-servers form:nth-child(1) input", "2.pool.ntp.org")
        b.click("#change_systime button")
        b.click("#change_systime li[value=ntp_time] a")
        b.wait_in_text("#change_systime button", "Automatically using NTP")
        b.wait_not_visible("#systime-ntp-servers")
        b.click("#systime-apply-button")
        b.wait_popdown("system_information_change_systime")

        self.assertIn("2.pool.ntp.org", m.execute("grep '^#NTP=' %s" % conf))

    def testMotd(self):
        m = self.machine
        b = self.browser

        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; fixed in PR #13951
        if m.image in ["rhel-8-2-distropkg"]:
            b.wait_text('#motd', "\n  \n  Hello\n  World")
        else:
            b.wait_text('#motd', "  Hello\n  World")

        b.click('#motd-box button')
        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")

        # because of the reload
        self.allow_restart_journal_messages()

    @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 .info-table-ct', "CPU")
        # QEMU VM type
        b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(1) tr:nth-of-type(1) td', "Other")
        # Name
        b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(1) tr:nth-of-type(2) td', "Standard PC")
        # BIOS
        b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(2) tr:nth-of-type(1) td', "SeaBIOS")
        # BIOS date; just ensure it's from this century
        b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(2) tr:nth-of-type(3) td', "/20")

        pci_selector = '#hwinfo #pci-listing'
        heading_selector = ' .listing-ct-heading'
        # 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[data-label=Model] button')
        b.wait_in_text(pci_selector + ' tbody tr:first-of-type td[data-label=Model]', "440FX")
        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('.breadcrumb button')
        b.enter_page("/system")

        # now pretend this is a system without DMI
        b.logout()
        m.execute("mount -t tmpfs none /sys/class/dmi/id")
        self.addCleanup(m.execute, "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 .info-table-ct', "CPU")
        self.assertNotIn('Type', b.text('#hwinfo .info-table-ct'))
        self.assertNotIn('BIOS', b.text('#hwinfo .info-table-ct'))

        # 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 .info-table-ct tbody:nth-of-type(1) tr:nth-of-type(2) td', "NAME")
        b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(1) tr:nth-of-type(3) td', "VENDOR")

        # Memory details should be shown for generic QEMU memory
        # demidecode not available on certain images
        if not m.ostree_image:
            b.wait_in_text('#hwinfo #memory-listing' + heading_selector, "Memory")
            b.wait_in_text('#hwinfo #memory-listing table', "DIMM 0")
            b.wait_in_text('#hwinfo #memory-listing table', "RAM")

            # Test more specific memory data with a fake dmidecode
            m.write('/tmp/dmidecode', dmidecode)
            m.execute('chmod +x /tmp/dmidecode; mount -o bind /tmp/dmidecode $(which dmidecode)')
            self.addCleanup(m.execute, 'umount $(which dmidecode)')
            b.reload()
            b.enter_page('/system/hwinfo')
            # first DIMM has unknown technology and rank
            b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=ID]', "ChannelA-DIMM0")
            b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Type]', "DDR3")
            b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Size]', "8 GB")
            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]', "Unknown")
            b.wait_in_text('#memory-listing tr:nth-of-type(1) td[data-label=Speed]', "1600 MT/s")
            # second DIMM has known technology and rank
            b.wait_in_text('#memory-listing tr:nth-of-type(2) td[data-label=ID]', "ChannelB-DIMM0")
            b.wait_text('#memory-listing tr:nth-of-type(2) td[data-label="Memory Technology"]', "Awesome")
            b.wait_text('#memory-listing tr:nth-of-type(2) td[data-label=Rank]', "Single Rank")

    def testCPUSecurityMitigations(self):
        b = self.browser
        m = self.machine

        self.login_and_go('/system/hwinfo')
        b.wait_present('#hwinfo th:contains(CPU)')

        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('mount --bind /run/cmdline /proc/cmdline')

            b.reload()
            b.enter_page('/system/hwinfo')

            if not expect_link_present:
                b.wait_present('#hwinfo th:contains(CPU)')
                b.wait_not_present('#hwinfo th:contains(CPU Security)')
            else:
                b.click('#hwinfo button:contains(Mitigations)')

            if expect_smt_state is not None:
                b.wait_present('#cpu-mitigations-dialog span:contains(nosmt)')
                b.wait_present('#cpu-mitigations-dialog #nosmt-switch input' +
                               (expect_smt_state and ":checked" or ":not(:checked)"))

            if cmdline:
                m.execute('while ! umount /proc/cmdline; do sleep 1; done')
                m.execute('rm /run/cmdline')

        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')
        # Most OSes don't set nosmt by default, but there are some exceptions
        expect_smt_default = m.image in ["fedora-coreos"]
        spoof_threads(2, True, expect_smt_default, None)

        # Switch nosmt option
        b.reload()
        b.enter_page('/system/hwinfo')
        b.click('#hwinfo button:contains(Mitigations)')


        b.click('#cpu-mitigations-dialog #nosmt-switch input')
        b.wait_present('#cpu-mitigations-dialog #nosmt-switch input' +
                       (expect_smt_default and ':not(:checked)' or ':checked'))
        b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')

        m.wait_reboot()
        if 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; various
        # Fedora and RHEL version are in different stages of the BLS
        # transition, so there are three cases:
        # - no BLS, options go into /etc/default/grub and grub.cfg (oldest)
        # - BLS, options go directly into entries (RHEL 8.0)
        # - BLS, entries use $kernelopt, that is defined in grubenv (newest)
        if not m.ostree_image:
            m.execute(r"""set -e
. /etc/os-release
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
else
    if [ "$ID" = rhel -o "$ID" = centos ] && [ "${VERSION_ID#7}" != "$VERSION_ID" ]; then
        new-kernel-pkg --package kernel --install 42.0.0  # RHEL/CentOS 7
    else
        kernel-install add 42.0.0 /boot/vmlinuz-42.0.0 2>/dev/null # Fedora/RHEL >= 8
    fi
fi
grep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfg ||
  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 )
""")
            # clean up so that next reboot works
            m.execute(r"""set -e
. /etc/os-release
rm /boot/vmlinuz-42.0.0
if type update-grub >/dev/null 2>&1; then
    update-grub  # Debian/Ubuntu
else
    if [ "$ID" = rhel -o "$ID" = centos ] && [ "${VERSION_ID#7}" != "$VERSION_ID" ]; then
        new-kernel-pkg --package kernel --remove 42.0.0  # RHEL/CentOS 7
    else
        kernel-install remove 42.0.0 /boot/vmlinuz-42.0.0  # Fedora/RHEL >= 8
    fi
fi
""")

        # Switch back nosmt option
        self.login_and_go('/system/hwinfo')
        b.click('#hwinfo button:contains(Mitigations)')
        b.wait_present('#cpu-mitigations-dialog span:contains(nosmt)')
        b.wait_present('#cpu-mitigations-dialog #nosmt-switch input' +
                       (expect_smt_default and ':not(:checked)' or ':checked'))
        b.click('#cpu-mitigations-dialog #nosmt-switch input')
        b.wait_present('#cpu-mitigations-dialog #nosmt-switch input' +
                       (expect_smt_default and ':checked' or ':not(:checked)'))
        b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
        m.wait_reboot()
        if 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.spawn("sync && sleep 0.1 && reboot", "reboot")
        m.wait_reboot()
        self.login_and_go('/system/hwinfo')
        b.click('#hwinfo button:contains(Mitigations)')
        b.wait_present('#cpu-mitigations-dialog span:contains(nosmt)')
        b.wait_present('#cpu-mitigations-dialog #nosmt-switch input:checked')
        b.click('#cpu-mitigations-dialog #nosmt-switch input')
        b.wait_present('#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', authorized=False)
        b.wait_present('#hwinfo th:contains(CPU)')
        b.wait_present('#cpu_mitigations[disabled]')
        b.mouse('#tip-cpu-security', 'mouseover')
        b.wait_text('.tooltip-inner', 'The user admin is not permitted to change cpu security mitigations')

        # 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_present('#cpu-mitigations-dialog #nosmt-switch input:checked')
        b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
        b.wait_present('#cpu-mitigations-dialog .pf-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')
        self.allow_authorize_journal_messages()
        self.allow_restart_journal_messages()

    @skipImage("Added in PR #13797", "rhel-8-2-distropkg")
    def testInsightsStatus(self):
        m = self.machine
        b = self.browser

        if not m.image.startswith("rhel"):
            self.skipTest("insights-client is only on RHEL")

        # Pretend that the Subscriptions page can do Insights stuff
        m.write("/usr/share/cockpit/subscription-manager/override.json", '{ "features": { "insights": true } }')

        # Run a mock version of the Insights API locally and configure
        # insights-client to access it.

        m.upload(["verify/files/mock-insights"], "/var/tmp")
        m.spawn("/var/tmp/mock-insights", "mock-insights")
        m.write("/etc/insights-client/insights-client.conf",
"""
[insights-client]
gpg=False
auto_config=False
base_url=127.0.0.1:8888/r/insights
username=admin
password=foobar
insecure_connection=True
""")

        # 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")
        b.wait_in_text(".system-health-insights a", "3 hits, including important")
        self.assertIn("123-nice-id", b.attr(".system-health-insights a", "href"))

class TestPcp(packagelib.PackageCase):

    @skipImage("cockpit-system doesn't have the install-on-demand feature")
    def testEnablePcpLink(self):
        m = self.machine
        b = self.browser

        # the OSTree images don't have pcp and can't install additional software
        if m.ostree_image:
            self.login_and_go("/system")
            b.wait_not_present("#server-pmlogger-switch")
            b.wait_not_present("#system-configuration-enable-pcp-link")
            return

        m.execute("pkcon remove -y pcp")

        pmlogger = {
            "/usr/bin/pmlogger": "",
            "/etc/systemd/system/pmlogger.service": "[Service]\nExecStart=/bin/sh -c \"while [ ! -f /tmp/pmlogger.start ]; do sleep 1; done\"\nType=oneshot\nRemainAfterExit=true"
        }

        self.createPackage("cockpit-pcp", "999", "1", content=pmlogger,
                           postinst="chmod +x /usr/bin/pmlogger; systemctl daemon-reload")
        self.enableRepo()
        m.execute("pkcon refresh")

        # the offer to install it should be visible
        self.login_and_go("/system")
        b.wait_not_present("#server-pmlogger-switch")
        b.wait_in_text("#system-configuration-enable-pcp-link", "Enable stored metrics")
        b.click("#system-configuration-enable-pcp-link")
        b.click(".modal-footer button:contains('Install')")
        b.wait_not_present(".modal-dialog:contains('Install Software')")


        b.wait_visible("#server-pmlogger-switch")
        b.wait_not_present("#system-configuration-enable-pcp-link")

        # Turn stored metrics on
        b.wait_present("#server-pmlogger-switch input:not(:checked)")
        b.click("#server-pmlogger-switch input")
        b.wait_present("#server-pmlogger-switch input:disabled")
        m.execute("touch /tmp/pmlogger.start")
        b.wait_present("#server-pmlogger-switch input:not(:disabled)")
        b.wait_present("#server-pmlogger-switch input:checked")

if __name__ == '__main__':
    test_main()
