1778 lines
76 KiB
Python
Executable File
1778 lines
76 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) 2017 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 os
|
|
import re
|
|
import time
|
|
|
|
from packagelib import PackageCase
|
|
from testlib import nondestructive, onlyImage, skipDistroPackage, skipImage, skipOstree, test_main
|
|
|
|
WAIT_SCRIPT = """
|
|
set -ex
|
|
for x in $(seq 1 200); do
|
|
if curl --insecure -s https://%(addr)s:8443/candlepin; then
|
|
break
|
|
else
|
|
sleep 1
|
|
fi
|
|
done
|
|
"""
|
|
|
|
OSesWithoutTracer = ["debian-stable", "debian-testing", "ubuntu-2204", "ubuntu-stable", "fedora-coreos", "rhel4edge"]
|
|
OSesWithoutKpatch = ["debian-*", "ubuntu-*", "arch", "fedora-*", "rhel4edge", "centos-*"]
|
|
|
|
|
|
class NoSubManCase(PackageCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Disable Subscription Manager on RHEL for these tests; subscriptions are tested in a separate class
|
|
# On other OSes (Fedora/CentOS) we expect sub-man to be disabled in yum, so it should not get in the way there
|
|
if self.machine.image.startswith("rhel") or self.machine.image.startswith("centos"):
|
|
self.machine.execute("systemctl stop rhsm.service; systemctl mask rhsm.service")
|
|
self.addCleanup(self.machine.execute, "systemctl unmask rhsm.service")
|
|
|
|
# expected journal messages from enabling/disabling auto upgrade services
|
|
self.allow_journal_messages("(Created symlink|Removed).*dnf-automatic-install.timer.*")
|
|
|
|
|
|
@skipImage("TODO: Fails with 401 on our test runners", "arch")
|
|
@skipOstree("Image uses OSTree")
|
|
class TestUpdates(NoSubManCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
pkcon_version = self.machine.execute("pkcon --version").strip()
|
|
|
|
# only the yum and PackageKit ≥ 1.2.4 dnf (https://github.com/PackageKit/PackageKit/issues/268) backends
|
|
# properly recognize "enhancement" severity; apt does not have that metadata
|
|
self.supports_severity = not (self.backend == "apt" or pkcon_version < "1.2.4")
|
|
|
|
if self.supports_severity:
|
|
self.enhancement_severity = "enhancement"
|
|
else:
|
|
self.enhancement_severity = "bug"
|
|
|
|
self.update_icon = "#page_status_notification_updates svg"
|
|
self.update_text = "#page_status_notification_updates"
|
|
self.update_text_action = "#page_status_notification_updates a"
|
|
|
|
def assertHistory(self, path, updates):
|
|
selector = path + " li:nth-child({0})"
|
|
for index, pkg in enumerate(updates, start=1):
|
|
self.browser.wait_in_text(selector.format(index), pkg)
|
|
# make sure we don't have any extra ones
|
|
self.assertFalse(self.browser.is_present(selector.format(len(updates) + 1)))
|
|
|
|
def check_nth_update(self, index, pkgname, version, severity="bug",
|
|
num_issues=None, desc_matches=[], cves=[], bugs=[], arch=None):
|
|
"""Check the contents of the package update table row at index
|
|
|
|
None properties will not be tested.
|
|
"""
|
|
b = self.browser
|
|
if arch is None:
|
|
arch = self.primary_arch
|
|
row = "#available-updates table[aria-label='Available updates'] > tbody:nth-of-type(%i) " % index
|
|
|
|
if isinstance(pkgname, list):
|
|
for idx, pkg in enumerate(pkgname, 1):
|
|
self.assertEqual(b.text(row + "tr:first-child [data-label=Name] > div:nth-of-type(%i) span" % idx).split(', ', 1)[0], pkg)
|
|
else:
|
|
self.assertEqual(b.text(row + " tr:first-child [data-label=Name]"), pkgname)
|
|
b.mouse(row + "tr:first-child [data-label=Name] span", "mouseenter")
|
|
b.wait_text(".pf-v5-c-tooltip", "dummy " + pkgname + " (" + arch + ")")
|
|
b.mouse(row + "tr:first-child [data-label=Name] span", "mouseleave")
|
|
b.wait_not_present(".pf-v5-c-tooltip")
|
|
self.assertEqual(b.text(row + "[data-label=Version]"), version)
|
|
# verify type
|
|
severity_to_aria = {"bug": "bug fix", "enhancement": "enhancement", "security": "security"}
|
|
b.wait_visible(f"{row} [data-label=Severity] .severity-icon[aria-label='{severity_to_aria[severity]}']")
|
|
self.assertEqual(b.text(row + "[data-label=Severity]").strip(),
|
|
num_issues is not None and str(num_issues) or "")
|
|
|
|
# should not be expanded by default
|
|
self.assertNotIn("pf-m-expanded", b.attr(row, "class"))
|
|
# expand
|
|
b.click(row + "td.pf-v5-c-table__toggle button")
|
|
self.assertIn("pf-m-expanded", b.attr(row, "class"))
|
|
b.wait_in_text(row + "> tr.pf-m-expanded", "Packages")
|
|
desc = b.text(row + "> tr.pf-m-expanded")
|
|
|
|
# details should contain all description bits, CVEs and bug numbers
|
|
for m in desc_matches + cves + bugs:
|
|
self.assertIn(m, desc)
|
|
|
|
return row
|
|
|
|
def wait_checking_updates(self):
|
|
'''Wait until spinner is gone from updates icon for 3 s'''
|
|
|
|
good_count = 0
|
|
for retry in range(60):
|
|
classes = self.browser.attr(self.update_icon, "class")
|
|
if classes is not None and "pf-v5-c-spinner" in classes:
|
|
good_count = 0
|
|
else:
|
|
good_count += 1
|
|
if good_count >= 3:
|
|
return
|
|
time.sleep(1)
|
|
|
|
self.fail("Timed out waiting for updates spinner to go away")
|
|
|
|
@nondestructive
|
|
@skipImage("kpatch is not available", *OSesWithoutKpatch)
|
|
def testKpatch(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
kernel_ver_arch = m.execute("uname -r").strip()
|
|
fields = kernel_ver_arch.split("-")
|
|
kpp_kernel_version = fields[0].replace(".", "_")
|
|
release = fields[1].split(".")[:-2] # remove el8.x86_64
|
|
kpp_kernel_release = "_".join(release)
|
|
|
|
sanitized_kernel_ver = "_".join(kernel_ver_arch.split(".")[:-2])
|
|
self.createPackage("-".join(["kpatch-patch", kpp_kernel_version, kpp_kernel_release]), "0", "0", arch=self.secondary_arch,
|
|
provides=f"kpatch-patch = {kernel_ver_arch}")
|
|
self.enableRepo()
|
|
|
|
self.restore_file("/etc/dnf/plugins/kpatch.conf")
|
|
m.execute("systemctl disable --now kpatch")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
# Enable and install kpatch
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("#kpatch-settings", "Disabled")
|
|
b.click("#kpatch-settings button:contains('Enable')")
|
|
|
|
# Apply kernel live patches for current and future kernels
|
|
b.click("#apply-kpatch")
|
|
b.wait_visible("#apply-kpatch:checked")
|
|
b.wait_visible("#current-future:checked")
|
|
|
|
b.click("button:contains('Save')")
|
|
b.wait_not_present("#kpatch-setup")
|
|
|
|
self.assertIn("True", m.execute("grep autoupdate /etc/dnf/plugins/kpatch.conf"))
|
|
self.assertEqual(m.execute("systemctl is-enabled kpatch").strip(), "enabled")
|
|
self.assertEqual(m.execute("systemctl is-active kpatch").strip(), "active")
|
|
# Patches should be installed
|
|
m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
|
|
|
|
# Faking real patches is really hard, so fake `kpatch list` command
|
|
# To make sure it has the same structure as we expect it to be first check the real command
|
|
self.assertEqual(m.execute("kpatch list"), "Loaded patch modules:\n\nInstalled patch modules:\n")
|
|
|
|
kpatch = """
|
|
#!/bin/bash
|
|
echo -e "Loaded patch modules:\nkpatch_3_10_0_1062_1_1 [enabled]\n\nInstalled patch modules:\nkpatch_3_10_0_1062_1_1 (3.10.0-1062.el7.x86_64)"
|
|
"""
|
|
|
|
self.createPackage("kpatch-patch-" + sanitized_kernel_ver, "1", "0", arch=self.secondary_arch,
|
|
provides=f"kpatch-patch = {kernel_ver_arch}",
|
|
content={"/usr/local/bin/kpatch": kpatch},
|
|
postinst="chmod +x /usr/local/bin/kpatch; mount -o bind /usr/local/bin/kpatch /usr/sbin/kpatch")
|
|
# Mix in an normal update and ensure it's not updated
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "1.0", "2")
|
|
self.createPackage("secdeclare", "3", "4.a1", install=True)
|
|
self.createPackage("secdeclare", "3", "4.b1", severity="security",
|
|
changes="Will crash your data center", cves=["CVE-2014-123456"])
|
|
self.enableRepo()
|
|
self.addCleanup(m.execute, "umount /usr/sbin/kpatch")
|
|
|
|
b.click("#status .pf-v5-c-card__actions button")
|
|
b.wait_visible(".pf-v5-c-badge:contains('patches')")
|
|
b.click("button:contains('Install kpatch updates')")
|
|
b.click("button:contains('Continue')")
|
|
b.wait_in_text("#status", "Kernel live patch kpatch_3_10_0_1062_1_1 is active")
|
|
# Other updates are not installed
|
|
self.check_nth_update(1, "secdeclare", "3-4.b1", "security", 1,
|
|
desc_matches=["Will crash your data center"], cves=["CVE-2014-123456"])
|
|
self.check_nth_update(2, "vanilla", "1.0-2")
|
|
self.assertEqual(m.execute("rpm -q vanilla").strip(), "vanilla-1.0-1.noarch")
|
|
|
|
# Switch to 'for current kernel only'
|
|
b.click("#kpatch-settings button:contains('Edit')")
|
|
b.click("#current-only")
|
|
b.wait_visible("#current-future:not(:checked)")
|
|
b.wait_visible("#current-only:checked")
|
|
|
|
b.click("button:contains('Save')")
|
|
b.wait_not_present("#kpatch-setup")
|
|
|
|
self.assertIn("False", m.execute("grep autoupdate /etc/dnf/plugins/kpatch.conf"))
|
|
self.assertEqual(m.execute("systemctl is-enabled kpatch").strip(), "enabled")
|
|
self.assertEqual(m.execute("systemctl is-active kpatch").strip(), "active")
|
|
# Patches should be installed
|
|
m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
|
|
|
|
# Test disabling applying patches
|
|
b.click("#kpatch-settings button:contains('Edit')")
|
|
b.wait_visible("#apply-kpatch:checked")
|
|
b.click("#apply-kpatch")
|
|
b.wait_visible("#apply-kpatch:not(:checked)")
|
|
|
|
b.click("button:contains('Save')")
|
|
b.wait_not_present("#kpatch-setup")
|
|
|
|
b.wait_in_text("#kpatch-settings", "Disabled")
|
|
b.click("#kpatch-settings button:contains('Enable')")
|
|
|
|
# Test closing of the dialog and resetting of changes
|
|
b.wait_visible("#apply-kpatch:not(:checked)")
|
|
b.click("#apply-kpatch")
|
|
b.wait_visible("#apply-kpatch:checked")
|
|
b.click("button:contains('Cancel')")
|
|
b.wait_not_present("#kpatch-setup")
|
|
b.click("#kpatch-settings button:contains('Enable')")
|
|
b.wait_visible("#apply-kpatch:not(:checked)")
|
|
|
|
# Test 'for current kernel only' from clean state
|
|
m.execute("umount /usr/sbin/kpatch")
|
|
m.execute("systemctl restart kpatch")
|
|
m.execute("dnf -y remove kpatch-patch-" + sanitized_kernel_ver)
|
|
b.reload() # Not listening on patches being removed
|
|
b.enter_page("/updates")
|
|
|
|
b.wait_in_text("#kpatch-settings", "Disabled")
|
|
b.click("#kpatch-settings button:contains('Enable')")
|
|
|
|
# Apply kernel live patches for current and future kernels
|
|
b.click("#apply-kpatch")
|
|
b.wait_visible("#apply-kpatch:checked")
|
|
b.click("#current-only")
|
|
b.wait_visible("#current-future:not(:checked)")
|
|
b.wait_visible("#current-only:checked")
|
|
|
|
b.click("button:contains('Save')")
|
|
b.wait_not_present("#kpatch-setup")
|
|
|
|
self.assertIn("False", m.execute("grep autoupdate /etc/dnf/plugins/kpatch.conf"))
|
|
self.assertEqual(m.execute("systemctl is-enabled kpatch").strip(), "enabled")
|
|
self.assertEqual(m.execute("systemctl is-active kpatch").strip(), "active")
|
|
# Patches should be installed
|
|
m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
|
|
|
|
@nondestructive
|
|
def testBasic(self):
|
|
# no security updates, no changelogs
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
def check_status():
|
|
if m.image in OSesWithoutTracer:
|
|
b.wait_in_text("#status p", "System is up to date")
|
|
# PK starts from a blank state, thus should force refresh and set the "time since" to 0
|
|
b.wait_in_text("#last-checked", "Last checked: less than a minute ago")
|
|
else:
|
|
code = m.execute("tracer > /dev/null 2>&1; echo $?").rstrip()
|
|
if code == "104":
|
|
b.wait_in_text("#status p", "a system reboot")
|
|
elif code == "102" or code == "101":
|
|
b.wait_in_text("#status p", "to be restarted")
|
|
else:
|
|
b.wait_in_text("#status p", "System is up to date")
|
|
# PK starts from a blank state, thus should force refresh and set the "time since" to 0
|
|
b.wait_in_text("#last-checked", "Last checked: less than a minute ago")
|
|
|
|
self.enable_preload("packagekit", "index")
|
|
|
|
# Refresh cache so cockpit does not try to reload page
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/system")
|
|
# status on /system front page: no repos at all, thus no updates
|
|
self.wait_checking_updates()
|
|
b.wait_text(self.update_text, "System is up to date")
|
|
self.assertEqual(b.attr(self.update_icon, "data-pficon"), "check")
|
|
|
|
# no updates on the Software Updates page
|
|
b.go("/updates")
|
|
b.enter_page("/updates")
|
|
|
|
# refresh
|
|
b.click("#status .pf-v5-c-card__header button")
|
|
check_status()
|
|
|
|
install_lockfile = "/tmp/finish-pk"
|
|
# create two updates; force installing chocolate before vanilla
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "1.0", "2", depends="chocolate",
|
|
postinst="while [ ! -e {0} ]; do sleep 1; done; rm -f {0}".format(install_lockfile))
|
|
self.createPackage("chocolate", "2.0", "1", install=True, arch=self.secondary_arch)
|
|
self.createPackage("chocolate", "2.0", "2", arch=self.secondary_arch)
|
|
self.enableRepo()
|
|
|
|
# check again
|
|
b.click("#status .pf-v5-c-card__header button")
|
|
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.wait_in_text("#status", "2 updates available")
|
|
|
|
b.wait_in_text("table[aria-label='Available updates']", "vanilla")
|
|
self.check_nth_update(1, "chocolate", "2.0-2", arch=self.secondary_arch)
|
|
self.check_nth_update(2, "vanilla", "1.0-2")
|
|
|
|
# updates are shown on system page
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
self.wait_checking_updates()
|
|
b.wait_text(self.update_text, "Bug fix updates available")
|
|
self.assertEqual(b.attr(self.update_icon, "data-pficon"), "bug")
|
|
# should be a link, click on it to go to /updates
|
|
b.click(self.update_text_action)
|
|
b.enter_page("/updates")
|
|
|
|
# old versions are still installed
|
|
m.execute("test -f /stamp-vanilla-1.0-1; test -f /stamp-chocolate-2.0-1")
|
|
|
|
# no update history yet
|
|
self.assertFalse(b.is_present("table.updates-history"))
|
|
|
|
# should only have one button (no security updates)
|
|
self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
|
|
|
|
# stall the download of chocolate by replacing the package with a pipe, so that we can test cancelling
|
|
chocolate = m.execute(f"""set -eux;
|
|
p=$(ls {self.vm_tmpdir}/repo/chocolate*2.0*2*)
|
|
f={self.vm_tmpdir}/fifo
|
|
mkfifo $f
|
|
mount -o bind $f $p
|
|
echo $p""").strip()
|
|
try:
|
|
b.click("#available-updates button#install-all")
|
|
|
|
# applying updates panel present
|
|
b.wait_visible("#app div.pf-v5-c-progress__bar")
|
|
|
|
# cancel the installation
|
|
b.wait_in_text(".progress-main-view button.pf-m-secondary", "Cancel")
|
|
b.click(".progress-main-view button.pf-m-secondary")
|
|
# abort the current download, so that read calls don't hang indefinitely
|
|
m.spawn(f"echo > {self.vm_tmpdir}/fifo", "fifo")
|
|
|
|
# going back to overview, nothing happened just yet
|
|
b.wait_not_present(".progress-main-view")
|
|
self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
|
|
self.assertFalse(b.is_present("table.updates-history"))
|
|
m.execute("test -f /stamp-vanilla-1.0-1; test -f /stamp-chocolate-2.0-1")
|
|
finally:
|
|
# avoid keeping the rpm file busy
|
|
m.execute("systemctl stop packagekit")
|
|
m.execute(f"umount {chocolate}")
|
|
|
|
# update again; Cancel button should eventually disappear
|
|
b.click("#available-updates button#install-all")
|
|
b.wait_visible(".progress-main-view")
|
|
b.wait_not_present(".progress-main-view button.pf-m-secondary")
|
|
# gets stuck at vanilla, which needs install_lockfile
|
|
b.wait_in_text("#app div.progress-description", "vanilla 1.0-2")
|
|
|
|
# update log only exists in the expander, collapsed by default
|
|
self.assertFalse(b.is_visible("#update-log"))
|
|
# expand it
|
|
b.click(".pf-v5-c-expandable-section__toggle")
|
|
# should eventually show chocolate when vanilla starts installing
|
|
b.wait_in_text("#update-log", "chocolate")
|
|
|
|
# finish the package installation
|
|
m.execute(f"touch {install_lockfile}")
|
|
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
# if tracer is not present, reboot is recommended
|
|
if m.image in OSesWithoutTracer:
|
|
b.wait_in_text("#app .pf-v5-c-empty-state button.pf-m-primary", "Reboot system...")
|
|
b.click("#ignore")
|
|
|
|
# should go back to updates overview, nothing pending any more
|
|
check_status()
|
|
|
|
# TODO make Packagekit GetUpdates work for tests properly
|
|
b.wait_not_present("#available-updates")
|
|
|
|
# new versions are now installed
|
|
m.execute("test -f /stamp-vanilla-1.0-2; test -f /stamp-chocolate-2.0-2")
|
|
|
|
# history shows the two packages, expanded by default
|
|
b.wait_text("table.updates-history tbody.pf-m-expanded td.history-pkgcount", "2 packages")
|
|
b.wait_in_text("table.updates-history tr.pf-m-expanded", "chocolate")
|
|
b.wait_in_text("table.updates-history tr.pf-m-expanded", "vanilla")
|
|
|
|
# system page has current state as well
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
self.wait_checking_updates()
|
|
self.assertEqual(b.attr(self.update_icon, "data-pficon"), "check")
|
|
b.wait_text(self.update_text, "System is up to date")
|
|
|
|
@skipImage("TODO: Packagekit on Arch does not detect the pear update", "arch")
|
|
@skipImage("tracer not available", *OSesWithoutTracer)
|
|
def testTracer(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
class Tracer(object):
|
|
def __init__(self, testObj, rebootRequired=False, testStatusCard=False, packageName="vanilla"):
|
|
self.testObj = testObj
|
|
self.rebootRequired = rebootRequired
|
|
self.testStatusCard = testStatusCard
|
|
self.packageName = packageName
|
|
|
|
def execute(self):
|
|
self.prepare()
|
|
self.test_frontend()
|
|
self.verify_backend()
|
|
self.cleanup()
|
|
|
|
def prepare(self):
|
|
if self.rebootRequired:
|
|
# setting app as static in tracer's helper file will cause it to require a reboot
|
|
self.testObj.write_file("/etc/tracer/applications.xml",
|
|
"""
|
|
<applications>
|
|
<app name="{0}" type="static" />
|
|
</applications>
|
|
""".format(self.packageName))
|
|
|
|
scriptContent = "#!/bin/sh\nsleep infinity"
|
|
unitContent = f"""
|
|
[Service]
|
|
ExecStart=/usr/local/bin/{self.packageName}
|
|
"""
|
|
self.testObj.createPackage(self.packageName, "1", "1", install=True, changes="initial package with service and run script",
|
|
content={f"/usr/local/bin/{self.packageName}": scriptContent, f"/etc/systemd/system/{self.packageName}.service": unitContent},
|
|
postinst="chmod a+x /usr/local/bin/{0}; systemctl daemon-reload; systemctl start {0}.service".format(self.packageName))
|
|
self.testObj.createPackage(self.packageName, "1", "2",
|
|
content={f"/usr/local/bin/{self.packageName}": scriptContent, f"/etc/systemd/system/{self.packageName}.service": unitContent},
|
|
postinst=f"chmod a+x /usr/local/bin/{self.packageName}")
|
|
|
|
self.serviceStartTime = m.execute(f"systemctl show {self.packageName}.service --property=ExecMainStartTimestamp")
|
|
|
|
self.testObj.enableRepo()
|
|
|
|
def test_frontend(self):
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
# check update is present
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("#status", "1 update available")
|
|
b.wait_in_text("#available-updates table", self.packageName)
|
|
|
|
# install updates
|
|
b.wait_visible("#install-all")
|
|
b.wait_in_text("#install-all", "Install all updates")
|
|
b.click("#install-all")
|
|
with b.wait_timeout(60):
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
|
|
b.wait_visible("#ignore")
|
|
b.wait_visible(".updates-success-table")
|
|
|
|
if self.rebootRequired:
|
|
rowId = "#reboot-row"
|
|
else:
|
|
rowId = "#service-row"
|
|
|
|
b.wait_visible(f"{rowId}")
|
|
b.click(f"{rowId} button")
|
|
b.wait_visible(f"{rowId} + tr")
|
|
b.wait_in_text(f"{rowId} + tr", self.packageName)
|
|
|
|
# test the tracer functionality works also in the Status Card
|
|
if self.testStatusCard:
|
|
b.click("#ignore")
|
|
|
|
if self.rebootRequired:
|
|
b.wait_in_text("#status", "1 package needs a system reboot")
|
|
b.click("#packages-need-reboot button")
|
|
b.wait_visible("#shutdown-dialog")
|
|
b.click("#delay")
|
|
b.click("button:contains('No delay')")
|
|
b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
|
|
b.click("#shutdown-dialog button:contains('Reboot')")
|
|
b.switch_to_top()
|
|
b.wait_in_text(".curtains-ct h1", "Disconnected")
|
|
|
|
# ensure that rebooting actually worked
|
|
m.wait_reboot()
|
|
m.start_cockpit()
|
|
b.reload()
|
|
b.login_and_go("/updates")
|
|
else:
|
|
b.wait_in_text("#status", "1 service needs to be restarted")
|
|
b.click("#services-need-restart button")
|
|
b.wait_visible("#restart-services-modal")
|
|
b.wait_in_text(".restart-services-modal-body", self.packageName)
|
|
b.click("#restart-services-modal button:contains('Restart services')")
|
|
b.wait_not_present("#restart-services-modal")
|
|
|
|
# test the tracer functionality works in-page after successful update
|
|
else:
|
|
if self.rebootRequired:
|
|
# update required a reboot
|
|
b.wait_not_present("#choose-service")
|
|
b.click("#reboot-system")
|
|
b.wait_visible("#shutdown-dialog")
|
|
b.click("#delay")
|
|
b.click("button:contains('No delay')")
|
|
b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
|
|
b.click("#shutdown-dialog button:contains('Reboot')")
|
|
b.switch_to_top()
|
|
b.wait_in_text(".curtains-ct h1", "Disconnected")
|
|
|
|
# ensure that rebooting actually worked
|
|
m.wait_reboot()
|
|
m.start_cockpit()
|
|
b.reload()
|
|
b.login_and_go("/updates")
|
|
else:
|
|
# update required a service restart
|
|
b.wait_not_present("#reboot-system")
|
|
b.click("#choose-service")
|
|
b.wait_visible("#restart-services-modal")
|
|
b.wait_in_text(".restart-services-modal-body", self.packageName)
|
|
b.click("#restart-services-modal button:contains('Restart services')")
|
|
b.wait_not_present("#restart-services-modal")
|
|
|
|
# check no updates are present
|
|
b.wait_in_text("#status", "System is up to date")
|
|
|
|
# history on "up to date" page should show the recent update (expanded by default)
|
|
self.testObj.assertHistory("#expanded-content0 > td > div > ul", [self.packageName])
|
|
|
|
def verify_backend(self):
|
|
if not self.rebootRequired:
|
|
# Check the service was actually restarted
|
|
newServiceStartTime = m.execute(f"systemctl show {self.packageName}.service --property=ExecMainStartTimestamp")
|
|
self.testObj.assertGreater(newServiceStartTime, self.serviceStartTime)
|
|
|
|
# check no services/processes need reboot or service restart
|
|
# tracer returns non-zero exit code if any services/processes are affected
|
|
m.execute("tracer --all --root")
|
|
|
|
def cleanup(self):
|
|
if not self.rebootRequired:
|
|
m.execute(f"systemctl stop {self.packageName}")
|
|
if m.image == "arch":
|
|
m.execute(f"pacman -R --noconfirm {self.packageName}")
|
|
else:
|
|
m.execute(f"rpm -e {self.packageName}")
|
|
if self.rebootRequired:
|
|
m.execute("rm /etc/tracer/applications.xml")
|
|
|
|
Tracer(
|
|
testObj=self,
|
|
packageName="apple",
|
|
rebootRequired=True,
|
|
testStatusCard=False,
|
|
).execute()
|
|
|
|
Tracer(
|
|
testObj=self,
|
|
packageName="cherry",
|
|
rebootRequired=False,
|
|
testStatusCard=False,
|
|
).execute()
|
|
|
|
Tracer(
|
|
testObj=self,
|
|
packageName="pear",
|
|
rebootRequired=True,
|
|
testStatusCard=True,
|
|
).execute()
|
|
|
|
Tracer(
|
|
testObj=self,
|
|
packageName="banana",
|
|
rebootRequired=False,
|
|
testStatusCard=True,
|
|
).execute()
|
|
|
|
@skipImage("Arch Linux does not start services by default", "arch")
|
|
@skipImage("tracer not available", *OSesWithoutTracer)
|
|
@nondestructive
|
|
def testFailServiceRestart(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
packageName = "apple"
|
|
scriptContent = "#!/bin/sh\nsleep infinity"
|
|
unitContent = f"""
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/local/bin/{packageName}
|
|
"""
|
|
self.createPackage(packageName, "1", "1", install=True, changes="initial package with service and run script",
|
|
content={f"/usr/local/bin/{packageName}": scriptContent, f"/etc/systemd/system/{packageName}.service": unitContent},
|
|
postinst=f"chmod a+x /usr/local/bin/{packageName}; systemctl daemon-reload; systemctl start {packageName}.service")
|
|
|
|
scriptContent = "#!/bin/sh\nfalse"
|
|
unitContent = f"""
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=/usr/local/bin/{packageName}
|
|
"""
|
|
self.createPackage(packageName, "1", "2",
|
|
content={f"/usr/local/bin/{packageName}": scriptContent, f"/etc/systemd/system/{packageName}.service": unitContent})
|
|
|
|
self.enableRepo()
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
# check update is present
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("#status", "1 update available")
|
|
b.wait_in_text("#available-updates table", packageName)
|
|
|
|
# install updates
|
|
b.wait_visible("#install-all")
|
|
b.wait_in_text("#install-all", "Install all updates")
|
|
b.click("#install-all")
|
|
with b.wait_timeout(60):
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
|
|
b.wait_visible("#ignore")
|
|
b.wait_visible(".updates-success-table")
|
|
|
|
b.wait_visible("#service-row")
|
|
b.click("#service-row button")
|
|
b.wait_visible("#service-row + tr")
|
|
b.wait_in_text("#service-row + tr", packageName)
|
|
|
|
# update required a service restart
|
|
b.wait_not_present("#reboot-system")
|
|
b.click("#choose-service")
|
|
b.wait_visible("#restart-services-modal")
|
|
b.wait_in_text(".restart-services-modal-body", packageName)
|
|
b.click("#restart-services-modal button:contains('Restart services')")
|
|
b.wait_visible("#restart-services-modal")
|
|
|
|
# tracer updated the list of services which need a restart, so our service is no longer present
|
|
b.wait_not_in_text("#restart-services-modal .pf-v5-l-stack__item", packageName)
|
|
b.wait_visible("#restart-services-modal .pf-v5-c-alert")
|
|
|
|
@skipImage("No security changelog support in packagekit", "arch")
|
|
def testInfoSecurity(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# just changelog
|
|
self.createPackage("norefs-bin", "1", "1", install=True)
|
|
self.createPackage("norefs-bin", "2", "1", severity="enhancement",
|
|
changes="Now 10% *more* [unicorns](http://unicorn.example.com)")
|
|
# binary from same source
|
|
self.createPackage("norefs-doc", "1", "1", install=True)
|
|
self.createPackage("norefs-doc", "2", "1", severity="enhancement",
|
|
changes="Now 10% *more* [unicorns](http://unicorn.example.com)")
|
|
# bug fixes
|
|
self.createPackage("buggy", "2", "1", install=True)
|
|
self.createPackage("buggy", "2", "2", changes="* Fixit", bugs=[123, 456])
|
|
# security fix with proper CVE list and severity
|
|
self.createPackage("secdeclare", "3", "4.a1", install=True)
|
|
self.createPackage("secdeclare", "3", "4.b1", severity="security",
|
|
changes="Will crash your data center", cves=["CVE-2014-123456"])
|
|
# security fix with parsing from changes
|
|
self.createPackage("secparse", "4", "1", install=True)
|
|
self.createPackage("secparse", "4", "2", changes="Fix CVE-2014-54321 and CVE-2017-9999.")
|
|
# security fix with RHEL severity and errata
|
|
self.createPackage("sevcritical", "5", "1", install=True)
|
|
self.createPackage("sevcritical", "5", "2", cves=["CVE-2014-54321"], securitySeverity="critical",
|
|
errata=["RHSA-2000:0001", "RHSA-2000:0002"], changes="More broken stuff")
|
|
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.wait_in_text("#status", "6 updates available, including 3 security fixes")
|
|
|
|
b.wait_in_text("table[aria-label='Available updates']", "sevcritical")
|
|
|
|
# security updates should get sorted on top and then alphabetically, so start with "secdeclare"
|
|
sel = self.check_nth_update(1, "secdeclare", "3-4.b1", "security", 1,
|
|
desc_matches=["Will crash your data center"], cves=["CVE-2014-123456"])
|
|
# should not have erratum label in details
|
|
self.assertNotIn("Errat", b.text(sel))
|
|
|
|
# secparse should also be considered a security update as the changelog mentions CVEs
|
|
self.check_nth_update(2, "secparse", "4-2", "security", 2,
|
|
desc_matches=["Fix CVE-2014-54321 and CVE-2017-9999."],
|
|
cves=["CVE-2014-54321", "CVE-2017-9999"])
|
|
sel = self.check_nth_update(3, "sevcritical", "5-2", "security", 1,
|
|
desc_matches=["More broken stuff"])
|
|
|
|
if self.backend == 'yum':
|
|
# sevcritical has a severity and errata
|
|
details = b.text(sel + " .pf-m-expanded")
|
|
self.assertIn("Severity", details)
|
|
self.assertIn("critical", details)
|
|
self.assertIn("Errata", details)
|
|
self.assertIn("RHSA-2000:0001", details)
|
|
self.assertIn("RHSA-2000:0002", details)
|
|
# icon has critical class
|
|
self.assertIn("severity-critical", b.attr(sel + " .severity-icon", "class"))
|
|
b.mouse(sel + " .severity-icon", "mouseenter")
|
|
b.wait_text(".pf-v5-c-tooltip", "critical")
|
|
b.mouse(sel + " .severity-icon", "mouseleave")
|
|
b.wait_not_present(".pf-v5-c-tooltip")
|
|
# details has link to severity definition
|
|
self.assertIn("access.redhat.com", b.attr(sel + " .pf-m-expanded dd.severity a:first-of-type", "href"))
|
|
|
|
# buggy: bug refs, no security
|
|
sel = self.check_nth_update(4, "buggy", "2-2", "bug", 2, bugs=["123", "456"], desc_matches=["Fixit"])
|
|
# should filter out enumeration in overview
|
|
ch = b.eval_js(f"document.querySelector(\"{sel + ' td.changelog'}\").innerHTML")
|
|
self.assertNotIn("<li>", ch)
|
|
self.assertNotIn("*", ch)
|
|
# should show bug fix icon and pf-v5-c-tooltip
|
|
self.assertEqual(b.attr(sel + " .severity-icon", "aria-label"), "bug fix")
|
|
b.mouse(sel + " .severity-icon", "mouseenter")
|
|
b.wait_text(".pf-v5-c-tooltip", "bug fix")
|
|
b.mouse(sel + " .severity-icon", "mouseleave")
|
|
b.wait_not_present(".pf-v5-c-tooltip")
|
|
|
|
# norefs: just changelog, show both binary packages
|
|
sel = self.check_nth_update(5, ["norefs-bin", "norefs-doc"], "2-1", self.enhancement_severity,
|
|
desc_matches=["Now 10% more unicorns"])
|
|
# verify Markdown formatting in table cell
|
|
self.assertEqual(b.text(sel + " td.changelog em"), "more") # *more*
|
|
self.assertEqual(b.attr(sel + " td.changelog a", "href"), "http://unicorn.example.com")
|
|
self.assertEqual(b.attr(sel + " td.changelog a", "target"), "_blank")
|
|
# verify Markdown formatting in details
|
|
self.assertEqual(b.text(sel + " .pf-m-expanded em"), "more") # *more*
|
|
self.assertEqual(b.attr(sel + " .pf-m-expanded a:first-of-type", "href"), "http://unicorn.example.com")
|
|
self.assertEqual(b.attr(sel + " .pf-m-expanded a:first-of-type", "target"), "_blank")
|
|
|
|
# verify that changelog is absent in mobile
|
|
b.set_layout("mobile")
|
|
b.wait_not_visible(sel + " td.changelog em")
|
|
b.set_layout("desktop")
|
|
b.wait_visible(sel + " td.changelog em")
|
|
|
|
# updates are shown on system page
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
self.wait_checking_updates()
|
|
b.wait_text(self.update_text, "Security updates available")
|
|
b.wait_attr(self.update_icon, "data-pficon", "security")
|
|
|
|
# should be a link, click on it to go to back to /updates
|
|
b.click(self.update_text_action)
|
|
b.enter_page("/updates")
|
|
|
|
# install only security updates
|
|
self.assertEqual(b.text("#available-updates button#install-security"), "Install security updates")
|
|
b.wait_not_present("#available-updates button#install-kpatches")
|
|
b.click("#available-updates button#install-security")
|
|
with b.wait_timeout(60):
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
|
|
# history on restart page should show the three security updates
|
|
b.click(".pf-v5-c-expandable-section__toggle")
|
|
self.assertHistory(".pf-v5-c-expandable-section ul", ["secdeclare", "secparse", "sevcritical"])
|
|
|
|
# ignore restarting
|
|
b.click("#ignore")
|
|
|
|
# should have succeeded; 3 non-security updates left
|
|
b.wait_in_text("#status", "3 updates available")
|
|
b.wait_in_text("#available-updates h2", "Available updates")
|
|
|
|
b.wait_in_text("#available-updates table", "norefs-doc")
|
|
self.assertIn("buggy", b.text("#available-updates table"))
|
|
self.assertNotIn("secdeclare", b.text("#available-updates table"))
|
|
self.assertNotIn("secparse", b.text("#available-updates table"))
|
|
|
|
# history should show the security updates
|
|
self.assertHistory("table.updates-history #expanded-content0 ul", ["secdeclare", "secparse", "sevcritical"])
|
|
|
|
# stop PackageKit (e. g. idle timeout) to make sure the page survives that
|
|
m.execute("systemctl stop packagekit; systemctl reset-failed packagekit || true")
|
|
|
|
# new security versions are now installed
|
|
m.execute("test -f /stamp-secdeclare-3-4.b1; test -f /stamp-secparse-4-2; test -f /stamp-sevcritical-5-2")
|
|
# but the three others are untouched
|
|
m.execute("test -f /stamp-buggy-2-1; test -f /stamp-norefs-bin-1-1; test -f /stamp-norefs-doc-1-1")
|
|
|
|
# should now only have one button (no security updates left)
|
|
self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
|
|
b.click("#available-updates button#install-all")
|
|
|
|
# should have succeeded and show restart
|
|
with b.wait_timeout(60):
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
b.wait_visible("#ignore")
|
|
|
|
# history on restart page should show the three non-security updates
|
|
b.click(".pf-v5-c-expandable-section__toggle")
|
|
self.assertHistory(".pf-v5-c-expandable-section ul", ["buggy", "norefs-bin", "norefs-doc"])
|
|
|
|
if m.image in OSesWithoutTracer:
|
|
# do the reboot; this will disconnect the web UI
|
|
b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
|
|
b.wait_visible("#shutdown-dialog")
|
|
b.click("#delay")
|
|
b.click("button:contains('No delay')")
|
|
b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
|
|
b.click("#shutdown-dialog button:contains('Reboot')")
|
|
b.switch_to_top()
|
|
b.wait_in_text(".curtains-ct h1", "Disconnected")
|
|
|
|
# ensure that rebooting actually worked
|
|
m.wait_reboot()
|
|
m.start_cockpit()
|
|
b.reload()
|
|
b.login_and_go("/updates")
|
|
else:
|
|
b.click("#ignore")
|
|
|
|
# new versions are now installed
|
|
m.execute("test -f /stamp-norefs-bin-2-1; test -f /stamp-norefs-doc-2-1")
|
|
|
|
# no further updates
|
|
b.wait_in_text("#status", "System is up to date")
|
|
|
|
# history on "up to date" page should show the recent update (expanded by default)
|
|
self.assertHistory("table.updates-history tbody.pf-m-expanded ul", ["buggy", "norefs-bin", "norefs-doc"])
|
|
# and the previous one, not expaned
|
|
b.wait_visible("table.updates-history tbody:not(.pf-m-expanded)")
|
|
|
|
@skipImage("No security changelog support in packagekit", "arch")
|
|
@nondestructive
|
|
def testSecurityOnly(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# security fix with proper CVE list and severity
|
|
self.createPackage("secdeclare", "3", "4.a1", install=True)
|
|
self.createPackage("secdeclare", "3", "4.b1", severity="security",
|
|
changes="Will crash your data center", cves=['CVE-2014-123456'])
|
|
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.wait_in_text("#status", "1 security fix available")
|
|
|
|
# should only have one button (only security updates)
|
|
b.wait_not_present("#available-updates button#install-all")
|
|
b.wait_not_present("#available-updates button#install-kpatches")
|
|
self.assertEqual(b.text("#available-updates button#install-security"), "Install security updates")
|
|
|
|
# security fix without CVE URLs
|
|
if self.supports_severity:
|
|
self.createPackage("secnocve", "1", "1", install=True)
|
|
self.createPackage("secnocve", "1", "2", severity="security", changes="Fix leak")
|
|
self.enableRepo()
|
|
# check for updates
|
|
b.click("#status .pf-v5-c-card__header button")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.wait_in_text("#status", "2")
|
|
|
|
b.wait_in_text("table[aria-label='Available updates']", "secnocve")
|
|
|
|
# secnocve should be displayed properly
|
|
self.check_nth_update(2, "secnocve", "1-2", "security", desc_matches=["Fix leak"])
|
|
|
|
@skipImage("No changelog support in Arch Linux", "arch")
|
|
@nondestructive
|
|
def testInfoTruncation(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# update with not too many binary packages
|
|
for i in range(4):
|
|
self.createPackage(f"coarse{i:02}", "1", "1", install=True)
|
|
self.createPackage(f"coarse{i:02}", "1", "2", changes="make it greener")
|
|
|
|
# update with lots of binary packages
|
|
for i in range(10):
|
|
self.createPackage(f"fine{i:02}", "1", "1", install=True)
|
|
self.createPackage(f"fine{i:02}", "1", "2", changes="make it better")
|
|
|
|
# update with long changelog
|
|
long_changelog = ""
|
|
for i in range(30):
|
|
long_changelog += f" - Things change #{i:02}\n"
|
|
self.createPackage("verbose", "1", "1", install=True)
|
|
self.createPackage("verbose", "1", "2", changes=long_changelog)
|
|
|
|
self.enableRepo()
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("table[aria-label='Available updates']", "Things change")
|
|
|
|
# "coarse" package list should be complete
|
|
t = b.text("#app .ct-table tbody:nth-of-type(1) tr:first-child [data-label=Name]")
|
|
self.assertIn("coarse00", t)
|
|
self.assertIn("coarse03", t)
|
|
self.assertNotIn(u"…", t)
|
|
|
|
# "fine" package list should be truncated
|
|
t = b.text("#app .ct-table tbody:nth-of-type(2) tr:first-child [data-label=Name]")
|
|
self.assertIn("fine00", t)
|
|
self.assertIn("fine03", t)
|
|
self.assertNotIn("fine09", t)
|
|
self.assertIn(u"…", t)
|
|
# but complete in the details
|
|
self.check_nth_update(2, ["fine00", "fine01", "fine02", "fine03"], "1-2",
|
|
desc_matches=["fine07", "fine09"])
|
|
|
|
# changelog should be truncated
|
|
desc = b.text("#app .ct-table tbody:nth-of-type(3) [data-label=Details]")
|
|
self.assertIn("Things change #00", desc)
|
|
self.assertNotIn("#01", desc)
|
|
|
|
# and not visible on mobile
|
|
b.set_layout("mobile")
|
|
b.wait_not_visible("#app .ct-table tbody:nth-of-type(3) [data-label=Details]")
|
|
# but complete in the details
|
|
self.check_nth_update(3, "verbose", "1-2",
|
|
desc_matches=["Things change #00", "Things change #29"])
|
|
|
|
# also on desktop
|
|
b.set_layout("desktop")
|
|
b.wait_visible("#app .ct-table tbody:nth-of-type(3) [data-label=Details]")
|
|
# collapse it so that check_nth_update is happy
|
|
b.click("#available-updates table[aria-label='Available updates'] > tbody:nth-of-type(3) td.pf-v5-c-table__toggle button")
|
|
self.check_nth_update(3, "verbose", "1-2",
|
|
desc_matches=["Things change #00", "Things change #29"])
|
|
|
|
# seems we can't verify that the description has a scrollbar
|
|
|
|
def testRebootAfterSuccess(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
install_lockfile = "/tmp/finish-pk"
|
|
# create two updates; force installing chocolate before vanilla
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "1.0", "2",
|
|
postinst="while [ ! -e {0} ]; do sleep 1; done; rm -f {0}".format(install_lockfile))
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.click("#available-updates button#install-all")
|
|
|
|
b.wait_visible("#app div.pf-v5-c-progress__bar")
|
|
# auto-reboot is off by default
|
|
b.wait_visible("#reboot-after:not(:checked)")
|
|
b.click("#reboot-after")
|
|
b.wait_visible("#reboot-after:checked")
|
|
|
|
m.execute(f"touch {install_lockfile}")
|
|
# reboots automatically
|
|
b.switch_to_top()
|
|
b.wait_in_text(".curtains-ct h1", "Disconnected")
|
|
m.wait_reboot()
|
|
self.allow_restart_journal_messages()
|
|
|
|
@nondestructive
|
|
def testUpdateError(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.createPackage("vapor", "1", "1", install=True)
|
|
self.createPackage("vapor", "1", "2")
|
|
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
# break the upgrade by removing the generated packages from the repo
|
|
m.execute("rm -f {0}/vapor*.deb {0}/vapor*.rpm {0}/vapor*.pkg*".format(self.repo_dir))
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.wait_in_text("#status", "1 update available")
|
|
|
|
b.click("#available-updates button#install-all")
|
|
|
|
# error message visible
|
|
b.wait_visible("#app .pf-v5-c-page .pf-v5-c-empty-state__body")
|
|
|
|
self.assertRegex(b.text("#app .pf-v5-c-page .pf-v5-l-stack .pf-v5-c-code-block .pf-v5-c-code-block__content .pf-v5-c-code-block__pre .pf-v5-c-code-block__code span:first-of-type"),
|
|
"missing|downloading|not.*available|No such file or directory|download library error")
|
|
|
|
# not expecting any buttons
|
|
self.assertFalse(b.is_present("#app button"))
|
|
|
|
@nondestructive
|
|
def testPackageKitCrash(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# this tends to corrupt the rpm database, so do a backup/restore
|
|
if self.backend == 'dnf':
|
|
# https://www.fedoraproject.org/wiki/Changes/RelocateRPMToUsr
|
|
exists = self.machine.execute("if test -e %s; then echo yes; fi" % "/usr/lib/sysimage/rpm").strip() != ""
|
|
if exists:
|
|
self.restore_dir("/usr/lib/sysimage/rpm")
|
|
else:
|
|
self.restore_dir("/var/lib/rpm")
|
|
|
|
# make sure we have enough time to crash PK
|
|
self.createPackage("slow", "1", "1", install=True)
|
|
# we don't want this installation to finish
|
|
self.createPackage("slow", "1", "2", postinst="sleep infinity")
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
with b.wait_timeout(30):
|
|
b.click("#available-updates button#install-all")
|
|
|
|
# let updates start and zap PackageKit
|
|
b.wait_visible("#app div.pf-v5-c-progress__bar")
|
|
m.execute("systemctl kill --signal=SEGV packagekit.service")
|
|
# this crash creates so many messages from systemd-coredump, debug metadata etc. that
|
|
# trying to keep up with specific patterns is too brittle
|
|
self.allow_journal_messages(".*")
|
|
|
|
# error message visible
|
|
b.wait_in_text("#app .pf-v5-c-page .pf-v5-l-stack .pf-v5-c-code-block__content", "PackageKit crashed")
|
|
|
|
@nondestructive
|
|
def testNoPackageKit(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.execute("systemctl stop packagekit")
|
|
system_service = m.execute("systemctl show -p FragmentPath packagekit.service | cut -f2 -d=").strip()
|
|
m.execute('''mv {0} {0}.disabled
|
|
mv /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service.disabled
|
|
systemctl daemon-reload'''.format(system_service))
|
|
self.addCleanup(m.execute,
|
|
'''mv {0}.disabled {0}
|
|
mv /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service.disabled /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service
|
|
systemctl daemon-reload'''.format(system_service))
|
|
|
|
m.start_cockpit()
|
|
if not self.is_pybridge():
|
|
# TODO: conditions not implemented on C bridge; once we drop it, drop this special case and
|
|
# enable the manifest documentation for conditions in doc/guide/packages.xml
|
|
self.assertIn("\nupdates", m.execute("cockpit-bridge --packages"))
|
|
b.login_and_go("/updates")
|
|
|
|
# error message present
|
|
b.wait_in_text(".pf-v5-c-page__main-section .pf-v5-l-stack .pf-v5-c-code-block__content", "PackageKit is not installed")
|
|
|
|
# update status on front page should show error
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
b.wait_text(self.update_text, "Loading available updates failed")
|
|
self.assertIn("exclamation", b.attr(self.update_icon, "class"))
|
|
else:
|
|
self.assertNotIn("\nupdates", m.execute("cockpit-bridge --packages"))
|
|
# should not appear in the menu at all
|
|
self.login_and_go(None)
|
|
b.wait_in_text("#host-apps .pf-m-current", "Overview")
|
|
self.assertNotIn("Updates", b.text("#host-apps .pf-m-current"))
|
|
|
|
# update status on front page should be invisible
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
b.wait_visible(".system-health")
|
|
self.assertFalse(b.is_present(self.update_text))
|
|
|
|
@skipDistroPackage()
|
|
@nondestructive
|
|
def testUnprivileged(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "2.0", "2")
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
# getting update info is allowed to all users
|
|
self.login_and_go("/updates", superuser=False)
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("#status", "1 update available")
|
|
b.wait_visible("#available-updates")
|
|
|
|
# but applying updates is not; FIXME: this is a crappy UX
|
|
b.click("#available-updates button#install-all")
|
|
# error message visible
|
|
b.wait_in_text("#app .pf-v5-c-code-block__content", "authentication")
|
|
|
|
# page adjusts automatically to privilege change
|
|
b.become_superuser()
|
|
b.wait_in_text("#status", "1 update available")
|
|
b.wait_visible("#available-updates")
|
|
|
|
# applying updates works now
|
|
b.click("#available-updates button#install-all")
|
|
|
|
with b.wait_timeout(60):
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
b.click("#ignore")
|
|
|
|
# should go back to updates overview, nothing pending any more
|
|
# TODO make Packagekit GetUpdates work for tests properly
|
|
b.wait_not_present("#available-updates")
|
|
|
|
|
|
@skipImage("TODO: Arch Linux has no cockpit-ws package, it's in cockpit", "arch")
|
|
@skipOstree("Image uses OSTree")
|
|
class TestWsUpdate(NoSubManCase):
|
|
def testBasic(self):
|
|
# The main case for this is that cockpit-ws itself gets upgraded, which
|
|
# restarts the service and terminates the connection. As we can't
|
|
# (efficiently) build a newer working cockpit-ws package, test the two
|
|
# parts (reconnect and warning about disconnect) separately.
|
|
|
|
# no security updates, no changelogs
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
install_lockfile = "/tmp/finish-pk"
|
|
# updating this package takes longer than a cockpit start and building the page
|
|
self.createPackage("slow", "1", "1", install=True)
|
|
self.createPackage(
|
|
"slow", "1", "2", postinst="while [ ! -e {0} ]; do sleep 1; done; rm -f {0}".format(install_lockfile))
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
with b.wait_timeout(30):
|
|
b.click("#available-updates button#install-all")
|
|
|
|
# applying updates panel present
|
|
b.wait_in_text("#app div.progress-description", "slow")
|
|
|
|
# restarting should pick up that install progress
|
|
m.restart_cockpit()
|
|
b.login_and_go("/updates")
|
|
|
|
b.wait_in_text("#app div.progress-description", "slow 1-2")
|
|
# progress bar has some reasonable value
|
|
b.wait_attr_contains(".progress-main-view .pf-v5-c-progress__indicator", "style", "width:")
|
|
rm = re.search(r"width: (\d+)%;", b.attr(".progress-main-view .pf-v5-c-progress__indicator", "style"))
|
|
progress = int(rm.group(1))
|
|
self.assertGreater(progress, 20)
|
|
self.assertLess(progress, 80)
|
|
|
|
# finish the package installation
|
|
m.execute(f"touch {install_lockfile}")
|
|
|
|
# should have succeeded and show restart page; cancel
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
b.click("#ignore")
|
|
b.wait_in_text("#status", "System is up to date")
|
|
|
|
# now pretend that there is a newer cockpit-ws available, warn about disconnect
|
|
self.createPackage("cockpit-ws", "999", "1")
|
|
# these have strict version dependencies to cockpit-ws, don't get in the way
|
|
self.createPackage("cockpit", "999", "1")
|
|
m.execute("if type apt; then dpkg -P cockpit-ws-dbgsym; fi")
|
|
self.enableRepo()
|
|
b.click("#status .pf-v5-c-card__header button")
|
|
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
b.wait_in_text("#status", "2 updates available")
|
|
b.wait_in_text("table[aria-label='Available updates']", "cockpit-ws")
|
|
|
|
b.wait_visible(".cockpit-update-warning")
|
|
b.wait_in_text(".cockpit-update-warning-text", "Web Console will restart")
|
|
|
|
self.allow_restart_journal_messages()
|
|
|
|
|
|
@skipImage("kpatch is not available", *OSesWithoutKpatch)
|
|
class TestKpatchInstall(NoSubManCase):
|
|
def testBasic(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.execute("rpm --erase --verbose kpatch kpatch-dnf")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#status")
|
|
b.wait_in_text("#kpatch-settings", "Not available")
|
|
# show unavailable packages in popover
|
|
b.click("#kpatch-settings .ct-info-circle")
|
|
b.wait_in_text(".pf-v5-c-popover", "Unavailable packages")
|
|
b.wait_in_text(".pf-v5-c-popover", "kpatch-dnf")
|
|
b.click("#kpatch-settings .ct-info-circle")
|
|
b.wait_not_present(".pf-v5-c-popover")
|
|
|
|
dummy_service = "[Service]\nExecStart=/bin/sleep infinity\n[Install]\nWantedBy=multi-user.target\n"
|
|
self.createPackage("kpatch", "999", "1", content={"/lib/systemd/system/kpatch.service": dummy_service})
|
|
self.createPackage("kpatch-dnf", "999", "1", content={"/etc/dnf/plugins/kpatch.conf": ""})
|
|
self.enableRepo()
|
|
|
|
b.reload()
|
|
b.enter_page("/updates")
|
|
|
|
b.wait_in_text("#kpatch-settings", "Not installed")
|
|
b.click("#kpatch-settings button:contains('Install')")
|
|
b.wait_in_text("#dialog", "kpatch, kpatch-dnf will be installed")
|
|
b.click("#dialog button:contains('Install')")
|
|
|
|
b.wait_in_text("#kpatch-settings", "Disabled")
|
|
|
|
# kpatch and kpatch-dnf should be installed
|
|
m.execute("rpm -q kpatch kpatch-dnf")
|
|
|
|
|
|
@onlyImage("No subscriptions", "rhel-*")
|
|
class TestUpdatesSubscriptions(PackageCase):
|
|
provision = {
|
|
"0": {"address": "10.111.112.1/20", "dns": "10.111.112.1", "memory_mb": 512},
|
|
"services": {"image": "services", "memory_mb": 1024}
|
|
}
|
|
|
|
def register(self):
|
|
# this fails with "Unable to find available subscriptions for all your installed products", but works anyway
|
|
self.machine.execute(
|
|
"LC_ALL=C.UTF-8 subscription-manager register --insecure --serverurl https://10.111.112.100:8443/candlepin --org=admin --activationkey=awesome_os_pool || true")
|
|
self.machine.execute("LC_ALL=C.UTF-8 subscription-manager attach --auto")
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.candlepin = self.machines['services']
|
|
m = self.machine
|
|
|
|
# wait for candlepin to be active and verify
|
|
self.candlepin.execute("/root/run-candlepin")
|
|
|
|
# remove all existing products (RHEL server), as we can't control them
|
|
m.execute("rm -f /etc/pki/product-default/*.pem /etc/pki/product/*.pem")
|
|
|
|
# download product info from the candlepin machine and install it
|
|
product_file = os.path.join(self.tmpdir, "88888.pem")
|
|
self.candlepin.download("/home/admin/candlepin/generated_certs/88888.pem", product_file)
|
|
|
|
# # upload product info to the test machine
|
|
m.execute("mkdir -p /etc/pki/product")
|
|
m.upload([product_file], "/etc/pki/product")
|
|
|
|
# make sure that rhsm skips certificate checks for the server
|
|
self.sed_file("s/insecure = 0/insecure = 1/g", "/etc/rhsm/rhsm.conf")
|
|
|
|
# Wait for the web service to be accessible
|
|
m.execute(WAIT_SCRIPT % {"addr": "10.111.112.100"}, timeout=360)
|
|
self.update_icon = "#page_status_notification_updates svg"
|
|
self.update_text = "#page_status_notification_updates"
|
|
self.update_text_action = "#page_status_notification_updates a"
|
|
|
|
def testNoUpdates(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# fresh machine, no updates available; by default our rhel-* images are not registered
|
|
m.start_cockpit()
|
|
b.login_and_go("/system")
|
|
# show unregistered status on system front page
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text(self.update_text, "Not registered")
|
|
self.assertIn("triangle", b.attr(self.update_icon, "class"))
|
|
|
|
# software updates page also shows unregistered
|
|
b.go("/updates")
|
|
b.enter_page("/updates")
|
|
# empty state visible in main area
|
|
b.wait_visible(".pf-v5-c-empty-state button")
|
|
b.wait_in_text(".pf-v5-c-empty-state", "This system is not registered")
|
|
|
|
# test the button to switch to Subscriptions
|
|
b.click(".pf-v5-c-empty-state button")
|
|
b.switch_to_top()
|
|
b.wait_js_cond('window.location.pathname === "/subscriptions"')
|
|
|
|
# after registration it should show the usual "system is up to date", through the "status changed" signal
|
|
self.register()
|
|
b.go("/updates")
|
|
b.enter_page("/updates")
|
|
# check updates
|
|
b.wait_visible("#status .pf-v5-c-card__header button")
|
|
b.wait_in_text("#status", "System is up to date")
|
|
|
|
# same on system page
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
self.assertEqual(b.attr(self.update_icon, "data-pficon"), "check")
|
|
b.wait_text(self.update_text, "System is up to date")
|
|
|
|
def testAvailableUpdates(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# one available update
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "1.0", "2")
|
|
self.enableRepo()
|
|
|
|
m.start_cockpit()
|
|
|
|
b.login_and_go("/system")
|
|
# by default our rhel-* images are not registered; show warning on system page
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text(self.update_text, "Not registered")
|
|
self.assertIn("triangle", b.attr(self.update_icon, "class"))
|
|
# should be a link leading to subscriptions page
|
|
b.click(self.update_text_action)
|
|
b.enter_page("/subscriptions")
|
|
|
|
# software updates page also shows unregistered
|
|
b.go("/updates")
|
|
b.enter_page("/updates")
|
|
|
|
# empty state visible in main area
|
|
b.wait_visible(".pf-v5-c-empty-state button")
|
|
b.wait_in_text(".pf-v5-c-empty-state", "This system is not registered")
|
|
|
|
# after registration it should show available updates
|
|
self.register()
|
|
b.go("/updates")
|
|
b.enter_page("/updates")
|
|
b.wait_not_present(".pf-v5-c-empty-state")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
# no update history yet
|
|
self.assertFalse(b.is_present("table.updates-history"))
|
|
|
|
# has action buttons
|
|
b.wait_visible("#status .pf-v5-c-card__header button")
|
|
self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
|
|
|
|
# show available updates on system page too
|
|
b.go("/system")
|
|
b.enter_page("/system")
|
|
b.wait_text(self.update_text, "Bug fix updates available")
|
|
self.assertEqual(b.attr(self.update_icon, "data-pficon"), "bug")
|
|
|
|
def testNoSubOsRepo(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# pretend we have a proper OS repo that does not require subscription
|
|
self.createPackage("coreutils", "999", "1")
|
|
self.enableRepo()
|
|
m.execute("pkcon refresh")
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/system")
|
|
with b.wait_timeout(30):
|
|
b.wait_text(self.update_text, "System is up to date")
|
|
|
|
b.go("/updates")
|
|
b.enter_page("/updates")
|
|
b.wait_visible("#status .pf-v5-c-card__header button")
|
|
b.wait_in_text("#status", "System is up to date")
|
|
|
|
|
|
@skipOstree
|
|
@nondestructive
|
|
class TestAutoUpdates(NoSubManCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
# not implemented for yum and apt yet, only dnf
|
|
self.supported_backend = self.backend in ["dnf"]
|
|
if self.backend == 'dnf':
|
|
self.addCleanup(self.machine.execute, "systemctl disable --now dnf-automatic-install.timer 2>/dev/null; rm -rf /etc/systemd/system/dnf-automatic-*")
|
|
|
|
def closeSettings(self, browser):
|
|
browser.click("#automatic-updates-dialog button:contains('Save changes')")
|
|
with browser.wait_timeout(30):
|
|
browser.wait_not_present("#automatic-updates-dialog")
|
|
|
|
def testBasic(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#status")
|
|
|
|
if not self.supported_backend:
|
|
self.assertFalse(b.is_present("#automatic"))
|
|
self.assertFalse(b.is_present("#auto-update-type"))
|
|
return
|
|
|
|
def assertTimerDnf(hour, dow):
|
|
out = m.execute("systemctl --no-legend list-timers dnf-automatic-install.timer")
|
|
if hour:
|
|
# don't test the minutes, due to RandomizedDelaySec=60m
|
|
self.assertRegex(out, f" {hour}:")
|
|
else:
|
|
self.assertEqual(out, "")
|
|
if dow:
|
|
self.assertRegex(out, r"^%s\s" % dow)
|
|
else:
|
|
# "every day" should not have a "LEFT" time > 1 day
|
|
self.assertNotIn(" day", out)
|
|
|
|
# service should not run right away
|
|
self.assertEqual(m.execute("systemctl is-active dnf-automatic-install.service || true").strip(), "inactive")
|
|
|
|
# automatic reboots should be enabled whenever timer is enabled
|
|
out = m.execute("systemctl cat dnf-automatic-install.service")
|
|
if hour:
|
|
self.assertRegex(out, "ExecStartPost=/.*shutdown")
|
|
else:
|
|
self.assertNotIn("ExecStartPost", out)
|
|
|
|
def assertTimer(hour, dow=None):
|
|
if self.backend == "dnf":
|
|
assertTimerDnf(hour, dow)
|
|
else:
|
|
raise NotImplementedError(self.backend)
|
|
|
|
def assertTypeDnf(_type):
|
|
if _type == "all":
|
|
match = '= default'
|
|
elif _type == "security":
|
|
match = '= security'
|
|
else:
|
|
raise ValueError(_type)
|
|
|
|
self.assertIn(match, m.execute("grep upgrade_type /etc/dnf/automatic.conf"))
|
|
|
|
def assertType(_type):
|
|
if self.backend == "dnf":
|
|
assertTypeDnf(_type)
|
|
else:
|
|
raise NotImplementedError(self.backend)
|
|
|
|
# automatic updates are supported, but off
|
|
b.wait_in_text("#autoupdates-settings", "Disabled")
|
|
assertTimer(None)
|
|
|
|
# enable
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#all-updates")
|
|
b.wait_val("#auto-update-time-input", "06:00")
|
|
b.wait_in_text("#auto-update-day", "every day")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
|
|
assertTimer("06")
|
|
assertType("all")
|
|
|
|
# change type to security
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#security-updates")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Security updates will be applied every day at 06:00")
|
|
assertType("security")
|
|
assertTimer("06")
|
|
|
|
# change it back
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#all-updates")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
|
|
assertType("all")
|
|
assertTimer("06")
|
|
|
|
# change day
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.select_from_dropdown("#auto-update-day", "thu")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every Thursday at 06:00")
|
|
assertType("all")
|
|
assertTimer("06", "Thu")
|
|
|
|
# change time
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.set_input_text("#auto-update-time-input", "21:00")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every Thursday at 21:00")
|
|
assertType("all")
|
|
assertTimer("21", "Thu")
|
|
|
|
# page should parse it correctly from the timer
|
|
b.logout()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every Thursday at 21:00")
|
|
|
|
# change back to daily
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.select_from_dropdown("#auto-update-day", "everyday")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 21:00")
|
|
assertType("all")
|
|
assertTimer("21")
|
|
|
|
# disable
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#no-updates")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Disabled")
|
|
assertTimer(None)
|
|
|
|
if self.backend == "dnf":
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#all-updates")
|
|
self.closeSettings(b)
|
|
# OnCalendar= parsing: only time
|
|
m.execute("mkdir -p /etc/systemd/system/dnf-automatic.timer.d")
|
|
m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=08:00\n" > '
|
|
r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
|
|
b.reload()
|
|
b.enter_page("/updates")
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 8:00")
|
|
b.wait_visible("#autoupdates-settings button")
|
|
|
|
# OnCalendar= parsing: weekday and time
|
|
m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=Tue 20:00\n" > '
|
|
r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
|
|
b.reload()
|
|
b.enter_page("/updates")
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every Tuesday at 20:00")
|
|
b.wait_visible("#autoupdates-settings button")
|
|
|
|
# OnCalendar= parsing: "every day" calendar and time
|
|
m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=*-*-* 07:00\n" > '
|
|
r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
|
|
b.reload()
|
|
b.enter_page("/updates")
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 7:00")
|
|
b.wait_visible("#autoupdates-settings button")
|
|
|
|
# OnCalendar= parsing: unsupported
|
|
m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=*-02-* 11:00\n" > '
|
|
r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
|
|
b.reload()
|
|
b.enter_page("/updates")
|
|
time.sleep(5)
|
|
b.wait_visible("#settings .pf-v5-c-alert")
|
|
# don't allow stomping over unparsable custom settings
|
|
b.wait_not_present("#autoupdates-settings button")
|
|
|
|
def testWithAvailableUpdates(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "1.0", "2")
|
|
self.enableRepo()
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
|
|
if not self.supported_backend:
|
|
return
|
|
|
|
b.wait_in_text("#autoupdates-settings", "Disabled")
|
|
|
|
# enable
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#all-updates")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
|
|
|
|
if self.backend == 'dnf':
|
|
self.checkUpgradeRebootDnf()
|
|
else:
|
|
raise NotImplementedError(self.backend)
|
|
|
|
@skipDistroPackage()
|
|
def testPrivilegeChange(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.execute("pkcon refresh")
|
|
|
|
self.login_and_go("/updates", superuser=False)
|
|
|
|
if not self.supported_backend:
|
|
return
|
|
|
|
# detecting auto updates configuration works unprivileged, but changing does not
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#autoupdates-settings button:disabled")
|
|
|
|
# become superuser, enable auto-updates
|
|
b.become_superuser()
|
|
b.wait_in_text("#autoupdates-settings", "Disabled")
|
|
b.click("#autoupdates-settings button:contains('Edit')")
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
b.click("#all-updates")
|
|
self.closeSettings(b)
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
|
|
|
|
# without superuser, auto-update status still visible, but disabled
|
|
b.drop_superuser()
|
|
b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
|
|
b.wait_visible("#autoupdates-settings button:disabled")
|
|
|
|
def checkUpgradeRebootDnf(self):
|
|
"""part of testWithAvailableUpdates() for dnf backend"""
|
|
|
|
m = self.machine
|
|
|
|
# dial down the random sleep to avoid the test having to wait 5 mins
|
|
self.sed_file("/random_sleep/ s/=.*$/= 3/", "/etc/dnf/automatic.conf")
|
|
# then manually start the upgrade job like the timer would
|
|
m.execute("systemctl start dnf-automatic-install.service")
|
|
# new vanilla package got installed, and triggered reboot; cancel that
|
|
m.execute("test -f /stamp-vanilla-1.0-2")
|
|
m.execute("until test -f /run/nologin; do sleep 1; done")
|
|
m.execute("set -e; shutdown -c; test ! -f /run/nologin")
|
|
# service should show vanilla upgrade and scheduling shutdown
|
|
out = m.execute(
|
|
"if systemctl status dnf-automatic-install.service; then echo 'expected service to be stopped'; exit 1; fi")
|
|
self.assertIn("vanilla", out)
|
|
# systemd 245.7 correctly says "reboot", older version say "shutdown"
|
|
self.assertRegex(out, "(Shutdown|Reboot) scheduled")
|
|
|
|
# run it again, now there are no available updates → no reboot
|
|
m.execute("systemctl start dnf-automatic-install.service")
|
|
m.execute("set -e; test -f /stamp-vanilla-1.0-2; test ! -f /run/nologin")
|
|
# service should not do much
|
|
out = m.execute(
|
|
"if systemctl status dnf-automatic-install.service; then echo 'expected service to be stopped'; exit 1; fi")
|
|
self.assertNotIn("vanilla", out)
|
|
self.assertNotIn("Shutdown", out)
|
|
|
|
|
|
@skipImage("Image uses OSTree", "fedora-coreos", "rhel4edge")
|
|
class TestAutoUpdatesInstall(NoSubManCase):
|
|
def testUnsupported(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.execute("if type dnf; then dnf remove -y dnf-automatic; elif type apt; then dpkg -P unattended-upgrades; fi")
|
|
|
|
# first test with available upgrades
|
|
self.createPackage("vanilla", "1.0", "1", install=True)
|
|
self.createPackage("vanilla", "1.0", "2")
|
|
self.enableRepo()
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go("/updates")
|
|
with b.wait_timeout(30):
|
|
b.wait_visible("#available-updates")
|
|
|
|
# apply updates
|
|
b.click("#available-updates button#install-all")
|
|
# wait until installation is finished
|
|
with b.wait_timeout(60):
|
|
b.wait_visible(".pf-c-empty-state__title:contains('Update was successful')")
|
|
b.click("#ignore")
|
|
|
|
if self.backend == 'dnf':
|
|
b.wait_in_text("#autoupdates-settings", "Not set up")
|
|
b.wait_not_present("#settings .pf-v5-c-alert")
|
|
b.click("#autoupdates-settings button:contains('Enable')")
|
|
else:
|
|
b.wait_not_present("#settings")
|
|
b.wait_not_present("#autoupdates-settings")
|
|
|
|
@skipImage("No supported auto update backend", "debian-*", "ubuntu-*", "arch")
|
|
def testInstall(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.execute('dnf remove -y dnf-automatic')
|
|
|
|
# provide minimal content in order for the backend to be seen as supported
|
|
timerContent = '''
|
|
[Unit]
|
|
Description=dnf-automatic timer
|
|
# See comment in dnf-makecache.service
|
|
ConditionPathExists=!/run/ostree-booted
|
|
|
|
[Timer]
|
|
OnBootSec=1h
|
|
OnUnitInactiveSec=1d
|
|
Unit=-.mount
|
|
|
|
[Install]
|
|
WantedBy=basic.target
|
|
'''
|
|
self.createPackage('dnf-automatic', '1', '1', content={
|
|
'/etc/dnf/automatic.conf': '',
|
|
'/usr/lib/systemd/system/dnf-automatic.timer': timerContent,
|
|
'/usr/lib/systemd/system/dnf-automatic-install.timer': timerContent
|
|
})
|
|
self.enableRepo()
|
|
|
|
m.start_cockpit()
|
|
b.login_and_go('/updates')
|
|
|
|
# click through install dialog
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text("#autoupdates-settings", "Not set up")
|
|
b.wait_not_present("#settings .pf-v5-c-alert")
|
|
b.click("#autoupdates-settings button:contains('Enable')")
|
|
b.wait_popup('dialog')
|
|
b.wait_visible('#dialog button.apply')
|
|
b.wait_not_attr('#dialog button.apply', 'disabled', '')
|
|
b.click('#dialog button.apply')
|
|
|
|
# as dnf-automatic isn't actually installed DnfImpl.setConfig will fail,
|
|
# but we can check that the backend is now enabled
|
|
b.wait_visible("#automatic-updates-dialog")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_main()
|