1561 lines
66 KiB
Python
Executable File
1561 lines
66 KiB
Python
Executable File
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
|
|
|
|
# This file is part of Cockpit.
|
|
#
|
|
# Copyright (C) 2013 Red Hat, Inc.
|
|
#
|
|
# Cockpit is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation; either version 2.1 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Cockpit is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import time
|
|
|
|
from testlib import MachineCase, nondestructive, skipDistroPackage, test_main, wait
|
|
|
|
import testvm
|
|
|
|
|
|
@nondestructive
|
|
@skipDistroPackage()
|
|
class TestServices(MachineCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Make sure the system finishes "booting" so that
|
|
# when we add additional services to this target below
|
|
# we don't race with the boot process
|
|
self.machine.execute("while ! systemctl is-active default.target; do sleep 1; done")
|
|
|
|
# We manipulate with `/etc/systemd/system` so `daemon-reload` to keep it in proper state
|
|
# Also `reset-failed` as failed units can be still listed with `systemctl` even when all files
|
|
# were removed and daemon reloaded.
|
|
self.restore_dir("/etc/systemd/system", "systemctl daemon-reload; systemctl reset-failed", True)
|
|
user_post_restore = "su - admin -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u admin); systemctl --user daemon-reload; systemctl --user reset-failed'"
|
|
self.restore_dir("/etc/systemd/user", user_post_restore, True)
|
|
self.restore_dir("/etc/xdg/systemd/user", user_post_restore, True)
|
|
self.restore_dir("/home/admin/.config", user_post_restore, True)
|
|
|
|
def run_systemctl(self, user, cmd):
|
|
if user:
|
|
self.machine.execute(f"su - admin -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u admin); systemctl --user {cmd}'")
|
|
else:
|
|
self.machine.execute(f"systemctl {cmd}")
|
|
|
|
def pick_tab(self, n):
|
|
self.browser.click(f'#services-filter li:nth-child({n}) a')
|
|
|
|
def wait_onoff(self, val):
|
|
self.browser.wait_visible(".service-top-panel .pf-v5-c-switch__input" + (":checked" if val else ":not(:checked)"))
|
|
|
|
def toggle_onoff(self):
|
|
self.browser.click(".service-top-panel .pf-v5-c-switch__input")
|
|
|
|
def do_action(self, action):
|
|
self.browser.click(".service-top-panel .pf-v5-c-dropdown button")
|
|
self.browser.click(f".service-top-panel .pf-v5-c-dropdown__menu a:contains('{action}')")
|
|
|
|
def check_service_details(self, statuses, actions, enabled, onoff=True, kebab=True):
|
|
self.browser.wait_collected_text("#statuses .status", "".join(statuses))
|
|
if onoff:
|
|
self.wait_onoff(enabled)
|
|
else:
|
|
self.browser.wait_not_present(".service-top-panel .pf-v5-c-switch")
|
|
|
|
if kebab:
|
|
self.browser.click(".service-top-panel .pf-v5-c-dropdown button")
|
|
self.browser.wait_text(".service-top-panel .pf-v5-c-dropdown__menu", "".join(actions))
|
|
# Click away to close the pf-v5-c-dropdown__menu
|
|
self.browser.click(".service-top-panel .pf-v5-c-dropdown button")
|
|
else:
|
|
self.browser.wait_not_present(".service-top-panel .pf-v5-c-dropdown")
|
|
|
|
def svc_sel(self, service):
|
|
return f'tr[data-goto-unit="{service}"]'
|
|
|
|
def goto_service(self, service):
|
|
return self.browser.click(self.svc_sel(service) + ' a')
|
|
|
|
def wait_service_state(self, service, state):
|
|
state_new = state
|
|
if 'inactive' in state:
|
|
state_new = 'Not running'
|
|
elif 'active' in state:
|
|
state_new = 'Running'
|
|
elif 'failed' in state:
|
|
state_new = 'Failed to'
|
|
|
|
self.browser.wait_in_text(self.svc_sel(service), state_new)
|
|
|
|
def select_file_state(self, states):
|
|
if not isinstance(states, list):
|
|
states = [states]
|
|
|
|
self.browser.click("#services-dropdown-file-state")
|
|
for state in states:
|
|
self.browser.set_checked(f".pf-v5-c-select__menu label:contains('{state}') input", True)
|
|
self.browser.click("#services-dropdown-file-state")
|
|
for state in states:
|
|
self.browser.wait_visible(f".pf-v5-c-chip-group__label:contains('File state') + .pf-v5-c-chip-group__list li:contains('{state}')")
|
|
|
|
def select_active_state(self, states):
|
|
if not isinstance(states, list):
|
|
states = [states]
|
|
|
|
self.browser.click("#services-dropdown-active-state")
|
|
for state in states:
|
|
self.browser.set_checked(f".pf-v5-c-select__menu label:contains('{state}') input", True)
|
|
self.browser.click("#services-dropdown-active-state")
|
|
for state in states:
|
|
self.browser.wait_visible(f".pf-v5-c-chip-group__label:contains('Active state') + .pf-v5-c-chip-group__list li:contains('{state}')")
|
|
|
|
def wait_page_load(self):
|
|
self.browser.wait_not_present(".pf-v5-c-empty-state .pf-v5-c-spinner[aria-valuetext='Loading...']")
|
|
|
|
def wait_service_in_panel(self, service, title):
|
|
self.wait_service_state(service, title)
|
|
|
|
def wait_service_present(self, service):
|
|
service_name = service[:-8] if service.endswith(".service") else service
|
|
self.browser.wait_text(f'tr[data-goto-unit="{service}"] .service-unit-id', service_name)
|
|
|
|
def wait_service_not_present(self, service):
|
|
self.browser.wait_not_present(self.svc_sel(service))
|
|
|
|
def make_test_service(self, path="/etc/systemd/system/"):
|
|
self.write_file(f"{path}/test.service",
|
|
"""
|
|
[Unit]
|
|
Description=Test Service
|
|
|
|
[Service]
|
|
ExecStart=/usr/local/bin/test-service
|
|
ExecReload=/bin/true
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
""")
|
|
self.write_file("/usr/local/bin/test-service",
|
|
"""#!/bin/sh
|
|
set -eu
|
|
trap "echo STOP" 0
|
|
|
|
if [ $(id -u) -eq 0 ]; then
|
|
journalctl --sync
|
|
else
|
|
# increase the chance for journal to catch up
|
|
sleep 5
|
|
fi
|
|
echo START
|
|
while true; do
|
|
sleep 5
|
|
echo WORKING
|
|
done
|
|
""", perm="755")
|
|
# After writing files out tell systemd about them
|
|
self.run_systemctl(False, "daemon-reload")
|
|
self.run_systemctl(True, "daemon-reload || true")
|
|
|
|
# When the test fails while `test.service` is active, the process keeps running and
|
|
# `systemctl status test.service` still shows it as active until this process dies
|
|
self.addCleanup(
|
|
self.machine.execute,
|
|
"for op in stop reset-failed disable; do systemctl $op test test-fail || true; done")
|
|
self.addCleanup(
|
|
self.machine.execute,
|
|
"su - admin -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u admin); "
|
|
"for op in stop reset-failed disable; do systemctl --user $op test test-fail || true; done'")
|
|
|
|
def testBasic(self):
|
|
self._testBasic()
|
|
|
|
def testBasicSession(self):
|
|
self._testBasic(True)
|
|
|
|
def _testBasic(self, user=False):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
path = "/etc/systemd/user" if user else "/etc/systemd/system"
|
|
|
|
self.write_file(f"{path}/test-fail.service",
|
|
"""
|
|
[Unit]
|
|
Description=Failing Test Service
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/false
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
""")
|
|
self.write_file(f"{path}/test.timer",
|
|
"""
|
|
[Unit]
|
|
Description=Test Timer
|
|
|
|
[Timer]
|
|
OnCalendar=*:1/2
|
|
""")
|
|
self.write_file(f"{path}/test-onboot.timer",
|
|
"""
|
|
[Unit]
|
|
Description=Test OnBoot Timer
|
|
|
|
[Timer]
|
|
OnBootSec=200min
|
|
Unit=test.service
|
|
""")
|
|
self.write_file("/usr/local/lib/systemd/system/cockpit-system.timer",
|
|
"""
|
|
[Unit]
|
|
Description=Not deleteable Timer
|
|
|
|
[Timer]
|
|
OnCalendar=*:1/2
|
|
""")
|
|
self.write_file(f"{path}/deleteme.service",
|
|
"""
|
|
[Unit]
|
|
Description=Delete Me Service
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/true
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
""")
|
|
self.write_file(f"{path}/deleteme.timer",
|
|
"""
|
|
[Unit]
|
|
Description=Delete Me Timer
|
|
|
|
[Timer]
|
|
OnCalendar=*:1/2
|
|
|
|
# just for user mode
|
|
[Install]
|
|
WantedBy=default.target
|
|
""")
|
|
self.write_file(f"{path}/special@:-characters.service",
|
|
"""
|
|
[Unit]
|
|
Description=Service With Special Characters in Id
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/true
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
""")
|
|
|
|
self.make_test_service(path)
|
|
# ensure we have one running timer; in user mode we do not have
|
|
# a running logind session at this point yet, so enable instead
|
|
self.run_systemctl(False, ("enable --global --runtime" if user else "start") + " deleteme.timer")
|
|
self.addCleanup(self.run_systemctl, user, "stop deleteme.timer || true")
|
|
|
|
url = "/system/services#/?owner=user" if user else "/system/services"
|
|
self.login_and_go(url, user="admin")
|
|
self.wait_page_load()
|
|
|
|
self.wait_service_present("test.service")
|
|
self.wait_service_in_panel("test.service", "Disabled")
|
|
self.wait_service_state("test.service", "inactive")
|
|
|
|
self.run_systemctl(user, "start test.service")
|
|
self.wait_service_state("test.service", "active")
|
|
self.wait_service_in_panel("test.service", "Disabled")
|
|
|
|
self.run_systemctl(user, "stop test.service")
|
|
self.wait_service_state("test.service", "inactive")
|
|
self.wait_service_in_panel("test.service", "Disabled")
|
|
|
|
self.run_systemctl(user, "enable test.service")
|
|
self.wait_service_state("test.service", "inactive")
|
|
self.wait_service_in_panel("test.service", "Enabled")
|
|
|
|
self.run_systemctl(user, "start test.service")
|
|
|
|
b.wait_attr("#services-toolbar", "data-loading", "false")
|
|
|
|
self.wait_service_present("special@:-characters.service")
|
|
self.wait_service_in_panel("special@:-characters.service", "Disabled")
|
|
self.wait_service_state("special@:-characters.service", "inactive")
|
|
|
|
# Test breadcrumb link when service id contains special characters
|
|
self.goto_service("special@:-characters.service")
|
|
b.wait_in_text(".pf-v5-c-breadcrumb", "special@:-characters.service")
|
|
b.click(".pf-v5-c-breadcrumb a:contains('Services')")
|
|
b.wait_visible("#services-list")
|
|
|
|
# Selects Targets tab
|
|
self.pick_tab(2)
|
|
b.wait_not_in_text("#services-list", "test")
|
|
self.wait_service_state("basic.target", "active")
|
|
|
|
if not user:
|
|
# Make sure that symlinked services also appear in the list
|
|
self.wait_service_state("reboot.target", "inactive")
|
|
self.wait_service_present("ctrl-alt-del.target")
|
|
self.goto_service("ctrl-alt-del.target")
|
|
b.wait_in_text(".service-name", "Reboot")
|
|
b.click("#services-page .pf-v5-c-breadcrumb__link")
|
|
|
|
# Selects Sockets tab
|
|
self.pick_tab(3)
|
|
b.wait_not_in_text("#services-list", "test")
|
|
b.wait_not_in_text("#services-list", ".target")
|
|
self.wait_service_state("dbus.socket", "active (running)")
|
|
|
|
# Selects Timer tab
|
|
self.pick_tab(4)
|
|
b.wait_not_in_text("#services-list", "test-fail")
|
|
b.wait_not_in_text("#services-list", ".target")
|
|
b.wait_not_in_text("#services-list", ".socket")
|
|
b.wait_visible(self.svc_sel('test.timer'))
|
|
b.wait_text(self.svc_sel('test.timer') + ' .service-unit-triggers', '')
|
|
today = b.eval_js("Intl.DateTimeFormat('en', { dateStyle: 'medium' }).format()")
|
|
# timer from initial page/units load
|
|
b.wait_in_text(self.svc_sel('deleteme.timer') + ' .service-unit-next-trigger', today)
|
|
# timer gets updated on PropertiesChanged event
|
|
self.run_systemctl(user, "start test.timer")
|
|
b.wait_in_text(self.svc_sel('test.timer') + ' .service-unit-next-trigger', today) # next run
|
|
b.wait_in_text(self.svc_sel('test.timer') + ' .service-unit-last-trigger', "unknown") # last trigger
|
|
|
|
# Timer details should also show information about next and last trigger
|
|
self.goto_service("test.timer")
|
|
b.wait_in_text('.service-unit-next-trigger', today) # next run
|
|
b.wait_in_text('.service-unit-last-trigger', "unknown") # last trigger
|
|
b.click(".pf-v5-c-breadcrumb a:contains('Services')")
|
|
|
|
self.run_systemctl(user, "stop test.timer")
|
|
|
|
b.wait_visible(self.svc_sel('test-onboot.timer'))
|
|
b.wait_text(self.svc_sel('test-onboot.timer') + ' .service-unit-triggers', '')
|
|
self.run_systemctl(user, "start test-onboot.timer")
|
|
# Check the next run. Since it triggers 200mins after the boot, it might be today or tomorrow (after 20:40)
|
|
today_stamp = int(m.execute("date +%s").strip())
|
|
time_zone = b.eval_js("Intl.DateTimeFormat().resolvedOptions().timeZone") # get browser's time zone
|
|
today_plus_200min = m.execute(f"TZ='{time_zone}' date --date=@{today_stamp + 200 * 60} '+%b %-d, %Y'").strip()
|
|
b.wait_in_text(self.svc_sel('test-onboot.timer') + ' .service-unit-next-trigger', today_plus_200min)
|
|
b.wait_in_text(self.svc_sel('test-onboot.timer') + ' .service-unit-last-trigger', "unknown") # last trigger
|
|
self.run_systemctl(user, "stop test-onboot.timer")
|
|
|
|
if not user:
|
|
self.goto_service("deleteme.timer")
|
|
self.do_action("Delete")
|
|
b.click("#delete-timer-modal-btn")
|
|
b.wait_not_present(".pf-v5-c-modal-box")
|
|
self.wait_page_load()
|
|
self.assertNotIn("deleteme", m.execute("systemctl list-timers"))
|
|
m.execute(f"! test -f {path}/deleteme.service")
|
|
m.execute(f"! test -f {path}/deleteme.timer")
|
|
|
|
# system timers are not allowed to be deleted
|
|
self.goto_service("cockpit-system.timer")
|
|
b.click(".service-top-panel .pf-v5-c-dropdown button")
|
|
b.wait_in_text(".service-top-panel .pf-v5-c-dropdown__menu", "Disallow running")
|
|
b.wait_not_in_text(".service-top-panel .pf-v5-c-dropdown__menu", "Delete")
|
|
b.click(".pf-v5-c-breadcrumb a:contains('Services')")
|
|
|
|
# Selects Paths tab - the test VM does not have any user path units
|
|
self.pick_tab(5)
|
|
b.wait_not_in_text("#services-list", "test")
|
|
b.wait_not_in_text("#services-list", ".target")
|
|
b.wait_not_in_text("#services-list", ".socket")
|
|
b.wait_not_in_text("#services-list", ".timer")
|
|
b.wait_visible(self.svc_sel("systemd-ask-password-console.path"))
|
|
|
|
# Selects Services tab
|
|
self.pick_tab(1)
|
|
|
|
# Survives a burst of events
|
|
suffix = ''
|
|
if user:
|
|
suffix = "?owner=user"
|
|
b.go(f"#/{suffix}")
|
|
self.wait_service_present("test.service")
|
|
m.execute("udevadm trigger; udevadm settle")
|
|
self.goto_service("test.service")
|
|
self.check_service_details(["Running", "Automatically starts"], ["Reload", "Restart", "Stop", "Disallow running (mask)", "Pin unit"], True)
|
|
|
|
# Stop and disable and back again
|
|
self.toggle_onoff()
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], False)
|
|
self.toggle_onoff()
|
|
self.check_service_details(["Running", "Automatically starts"], ["Reload", "Restart", "Stop", "Disallow running (mask)", "Pin unit"], True)
|
|
self.toggle_onoff() # later on we need some disabled test
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], False)
|
|
|
|
# Check service that fails to start
|
|
b.go(f'/system/services#/{suffix}')
|
|
self.goto_service("test-fail.service")
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], False)
|
|
self.toggle_onoff()
|
|
self.check_service_details(["Failed to start", "Automatically starts"], ["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], True)
|
|
if not user:
|
|
b.assert_pixels("#service-details-unit", "details-test-fail",
|
|
# in medium layout we sometimes get a scrollbar depending on how many test-fail logs exist
|
|
skip_layouts=["medium"],
|
|
# ignore the switcher, it causes a tiny flake around the sides.
|
|
ignore=[".pf-v5-c-switch__toggle"])
|
|
b.click(".action-button:contains('Start service')")
|
|
b.go(f'/system/services#/{suffix}')
|
|
self.wait_service_present("test-fail.service")
|
|
self.wait_service_state("test-fail.service", "failed")
|
|
|
|
# Check static service
|
|
self.goto_service("systemd-exit.service")
|
|
self.check_service_details(["Static", "Not running"], ["Start", "Disallow running (mask)", "Pin unit"], True, onoff=False)
|
|
|
|
# Mask and unmask
|
|
self.do_action("Disallow running (mask)")
|
|
b.click("#mask-service button.pf-m-danger")
|
|
self.check_service_details(["Masked"], ["Allow running (unmask)"], False, onoff=False)
|
|
|
|
# Masked services have no relationships and therefore the expandable section should not be present
|
|
b.wait_not_present("#service-details-show-relationships")
|
|
|
|
self.do_action("Allow running (unmask)")
|
|
self.check_service_details(["Static", "Not running"], ["Start", "Disallow running (mask)", "Pin unit"], True, onoff=False)
|
|
|
|
# Pin unit
|
|
b.go(f'/system/services#/{suffix}')
|
|
b.wait_not_present('#test.service > svg.service-thumbtack-icon-color')
|
|
|
|
self.goto_service("test.service")
|
|
b.wait_not_present('#service-details-unit > article > div > svg.service-thumbtack-icon')
|
|
self.do_action("Pin unit")
|
|
b.is_present('#service-details-unit > article > div > svg.service-thumbtack-icon')
|
|
b.go(f'/system/services#/{suffix}')
|
|
|
|
# returns index of first unit that isn't failed
|
|
b.inject_js("""
|
|
function firstWorkingUnitPos() {
|
|
const arr = document.getElementById('services-list').firstChild;
|
|
for (var i = 0; i < arr.childElementCount; i++) {
|
|
const child = arr.children[i];
|
|
if (!child.classList.contains('service-unit-failed')) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
pos = b.eval_js('firstWorkingUnitPos();')
|
|
b.wait_text(f'#services-list > tbody > tr:nth-child({pos}) > th > div > div > a', 'test')
|
|
b.is_present('#test.service > svg.service-thumbtack-icon-color')
|
|
|
|
# Unpin unit
|
|
self.goto_service("test.service")
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Unpin unit"], False)
|
|
self.do_action("Unpin unit")
|
|
b.wait_not_present('#service-details-unit > article > div > svg.service-thumbtack-icon')
|
|
b.go(f'/system/services#/{suffix}')
|
|
b.wait_not_present('#test.service > svg.service-thumbtack-icon-color')
|
|
|
|
def init_filter_state():
|
|
if not b.is_visible("#services-text-filter"):
|
|
b.click(".pf-v5-c-toolbar__toggle button")
|
|
|
|
if b.is_present(".pf-v5-c-toolbar__expandable-content.pf-m-expanded button:contains('Clear all filters')"):
|
|
b.click(".pf-v5-c-toolbar__expandable-content.pf-m-expanded button:contains('Clear all filters')")
|
|
elif b.is_present(".pf-v5-c-toolbar__content > .pf-v5-c-toolbar__item > button:contains('Clear all filters')"):
|
|
b.click(".pf-v5-c-toolbar__content > .pf-v5-c-toolbar__item > button:contains('Clear all filters')")
|
|
else:
|
|
b.set_input_text("#services-text-filter input", "")
|
|
|
|
self.wait_service_present("test.service")
|
|
self.wait_service_present("test-fail.service")
|
|
|
|
b.go(f'/system/services#/{suffix}')
|
|
|
|
# Check that `Listen` and `Triggers` properties are shown for socket units
|
|
self.pick_tab(3)
|
|
self.goto_service("dbus.socket")
|
|
if not user:
|
|
self.assertIn("/run/dbus/system_bus_socket (Stream)", b.text("#listen"))
|
|
else:
|
|
self.assertIn("/run/user/", b.text("#listen"))
|
|
self.assertIn(b.text("#Triggers"), ["dbus.service", "dbus-broker.service"])
|
|
b.click(".pf-v5-c-breadcrumb a:contains('Services')")
|
|
|
|
# Select services tab
|
|
self.pick_tab(1)
|
|
|
|
# Filter by id
|
|
init_filter_state()
|
|
b.set_input_text("#services-text-filter input", "fail.ser")
|
|
self.wait_service_not_present("test.service")
|
|
self.wait_service_present("test-fail.service")
|
|
|
|
# Filter by description capital letters included
|
|
init_filter_state()
|
|
b.set_input_text("#services-text-filter input", "Test Service")
|
|
# test.service is not loaded, thus description search does not find it
|
|
self.wait_service_present("test-fail.service")
|
|
if not user:
|
|
b.assert_pixels("#services-page", "text-filter-test", skip_layouts=["mobile"])
|
|
b.set_layout("mobile")
|
|
# Waiting a bit for the layout to stabilize. The scrolling of
|
|
# the header happens asynchronously with an animation.
|
|
time.sleep(2)
|
|
# Now scroll the header all the way to the left to get a conistent pixel test result
|
|
nav_scroll_btn = ".services-header button[aria-label='Scroll left']"
|
|
while b.call_js_func("ph_attr", nav_scroll_btn, "disabled") is None:
|
|
b.click(nav_scroll_btn)
|
|
time.sleep(0.5)
|
|
if not user:
|
|
b.assert_pixels_in_current_layout("#services-page", "text-filter-test")
|
|
b.set_layout("desktop")
|
|
|
|
# Filter by description capitalization not matching the unit description
|
|
init_filter_state()
|
|
b.set_input_text("#services-text-filter input", "failing test service")
|
|
self.wait_service_not_present("test.service")
|
|
self.wait_service_present("test-fail.service")
|
|
|
|
# Filter by Id capitalization not matching the unit Id
|
|
# NetworkManager exists only as a system service, skip user test
|
|
if not user:
|
|
init_filter_state()
|
|
b.set_input_text("#services-text-filter input", "networkmanager")
|
|
self.wait_service_present("NetworkManager.service")
|
|
|
|
# Select only static services
|
|
init_filter_state()
|
|
self.select_file_state("Static")
|
|
self.wait_service_not_present("test.service")
|
|
self.wait_service_not_present("test-fail.service")
|
|
self.wait_service_present("systemd-exit.service")
|
|
|
|
# Select only disabled services
|
|
init_filter_state()
|
|
self.select_file_state("Disabled")
|
|
self.wait_service_present("test.service")
|
|
self.wait_service_not_present("test-fail.service")
|
|
self.wait_service_not_present("systemd-exit.service")
|
|
|
|
# Select only stopped services
|
|
init_filter_state()
|
|
self.select_active_state("Not running")
|
|
self.wait_service_present("test.service")
|
|
self.wait_service_not_present("test-fail.service")
|
|
|
|
# Select only failed services
|
|
init_filter_state()
|
|
self.select_active_state("Failed to start")
|
|
self.wait_service_not_present("test.service")
|
|
self.wait_service_present("test-fail.service")
|
|
|
|
# Select Alias and Masked services
|
|
# No indirect user services exist, skip user test
|
|
if not user:
|
|
init_filter_state()
|
|
self.select_file_state(["Indirect", "Masked"])
|
|
self.wait_service_not_present("test.service")
|
|
self.wait_service_present("getty@tty1.service")
|
|
|
|
# Check filtering and selecting together - empty state
|
|
init_filter_state()
|
|
b.set_input_text("#services-text-filter input", "failing")
|
|
self.select_active_state("Not running")
|
|
self.wait_service_not_present("test.service")
|
|
b.wait_visible("#services-page .pf-v5-c-empty-state")
|
|
|
|
# Check resetting filter
|
|
b.click("#clear-all-filters")
|
|
self.wait_service_present("test.service")
|
|
self.wait_service_present("test-fail.service")
|
|
self.wait_service_present("systemd-exit.service")
|
|
self.assertEqual(b.val("#services-text-filter input"), "")
|
|
|
|
# Check that closing filter chip groups or single chips works
|
|
init_filter_state()
|
|
self.select_active_state("Not running")
|
|
self.select_active_state("Running")
|
|
self.wait_service_present("test.service")
|
|
b.click(".pf-v5-c-chip-group__label:contains('Active state') + .pf-v5-c-chip-group__list li:contains('Not running') button")
|
|
b.wait_not_present(".pf-v5-c-chip-group__label:contains('Active state') + .pf-v5-c-chip-group__list li:contains('Not running')")
|
|
self.wait_service_not_present("test.service")
|
|
b.click(".pf-v5-c-chip-group:contains('Active state') .pf-v5-c-chip-group__close > button")
|
|
b.wait_not_present(".pf-v5-c-chip-group")
|
|
|
|
# Check that journalbox shows empty state
|
|
self.goto_service("systemd-exit.service")
|
|
b.wait_text('.cockpit-log-panel .pf-v5-c-card__body', "No log entries")
|
|
b.wait_not_present("button:contains('View all logs')")
|
|
|
|
b.go(f'/system/services#/{suffix}')
|
|
|
|
params = "-user" if user else ""
|
|
|
|
self.write_file(f"/tmp/mem-hog{params}.awk", f'BEGIN {{ system("while [ ! -f /tmp/continue{params} ]; do sleep 1; done"); y = sprintf("%50000000s",""); system("sleep infinity") }}')
|
|
self.write_file(f"{path}/mem-hog.service",
|
|
f"""
|
|
[Unit]
|
|
Description=Mem Hog Service
|
|
|
|
[Service]
|
|
ExecStart=/bin/awk -f /tmp/mem-hog{params}.awk
|
|
""")
|
|
|
|
self.run_systemctl(user, "daemon-reload")
|
|
|
|
# Check that MemoryCurrent is shown
|
|
self.goto_service("mem-hog.service")
|
|
b.wait_not_present("#memory")
|
|
# In some distros systemctl is showing memory current as [Not Set] for user units
|
|
if not user or m.image not in ["rhel-8-7", "rhel-8-8", "rhel-8-9", "centos-8-stream"]:
|
|
self.run_systemctl(user, "start mem-hog.service")
|
|
# If the test fails before we stop the mem-hog the next test run will not get the correct memory usage here
|
|
self.addCleanup(self.run_systemctl, user, "stop mem-hog.service || true")
|
|
# initial memory detection takes very long especially on RHEL 8
|
|
with b.wait_timeout(60):
|
|
b.wait_visible("#memory")
|
|
initial_memory = float(b.text("#memory").split(" ")[0])
|
|
self.assertGreater(initial_memory, 0.5)
|
|
m.execute(f"touch /tmp/continue{params}")
|
|
self.addCleanup(m.execute, f"rm /tmp/continue{params}")
|
|
# MemoryCurrent auto-updates every 30s - when it updates the memory used should be ~50MiB
|
|
with b.wait_timeout(60):
|
|
b.wait(lambda: float(b.text("#memory").split(" ")[0]) > 40)
|
|
self.run_systemctl(user, "stop mem-hog.service")
|
|
b.wait_not_present("#memory")
|
|
# Check that listen is not shown for .service units
|
|
b.wait_not_present("#listen")
|
|
|
|
# Check empty state error for nonexisent unit with valid name
|
|
b.go(f'/system/services#/nonexistent.service{suffix}')
|
|
b.wait_in_text(".pf-v5-c-empty-state", "Unit nonexistent.service not found")
|
|
b.click("a:contains('View all services')")
|
|
b.switch_to_top()
|
|
b.wait_js_cond('window.location.pathname == "/system/services"')
|
|
if user:
|
|
b.wait_js_cond(f"window.location.hash === '#/{suffix}'")
|
|
|
|
# Check empty state error for invalid unit name
|
|
b.go(f'/system/services#/nonexistent{suffix}')
|
|
b.enter_page("/system/services")
|
|
b.wait_in_text(".pf-v5-c-empty-state", "Loading unit failed")
|
|
b.wait_in_text(".pf-v5-c-empty-state", "Unit name nonexistent is not valid")
|
|
|
|
def testLogs(self):
|
|
self._testLogs(False)
|
|
|
|
def testLogsUser(self):
|
|
self._testLogs(True)
|
|
|
|
def _testLogs(self, user=False):
|
|
b = self.browser
|
|
service_type = "user-service" if user else "service"
|
|
|
|
def append_user(url):
|
|
if user:
|
|
return url + ("" if "#/" in url else "#/") + "?owner=user"
|
|
return url
|
|
|
|
self.make_test_service("/etc/systemd/user" if user else "/etc/systemd/system")
|
|
|
|
self.login_and_go(append_user("/system/services"))
|
|
self.wait_page_load()
|
|
|
|
# Check test.service and then start it
|
|
self.goto_service("test.service")
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], False)
|
|
b.wait_text('.cockpit-log-panel .pf-v5-c-card__body', "No log entries")
|
|
self.toggle_onoff()
|
|
self.check_service_details(["Running", "Automatically starts"], ["Reload", "Restart", "Stop", "Disallow running (mask)", "Pin unit"], True)
|
|
b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "START")
|
|
b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "WORKING")
|
|
b.click(".cockpit-log-panel .pf-v5-c-card__header button")
|
|
|
|
b.enter_page("/system/logs")
|
|
b.wait_in_text("#journal-box .cockpit-log-panel > div:nth-child(2)", "WORKING")
|
|
t = b.text("#journal-box .cockpit-log-panel > div:nth-child(3)") + b.text("#journal-box .cockpit-log-panel > div:nth-child(4)")
|
|
self.assertIn("START", t)
|
|
b.wait_val("#journal-grep input", f"priority:debug {service_type}:test.service ")
|
|
b.go(append_user("/system/services#/test.service"))
|
|
b.enter_page("/system/services")
|
|
|
|
b.wait_in_text(".cockpit-logline:nth-child(2)", "WORKING")
|
|
b.click(".cockpit-logline:nth-child(2)")
|
|
b.enter_page("/system/logs")
|
|
b.wait_text(".pf-v5-c-card__title", "WORKING")
|
|
b.click(".pf-v5-c-breadcrumb li:contains('Logs')")
|
|
b.wait_val("#journal-grep input", f"priority:debug {service_type}:test.service ")
|
|
b.go(append_user("/system/services#/test.service"))
|
|
b.enter_page("/system/services")
|
|
|
|
b.wait_in_text(".cockpit-logline:nth-child(2)", "WORKING")
|
|
b.click(".cockpit-logline:nth-child(2)")
|
|
b.enter_page("/system/logs")
|
|
b.click("button:contains('Go to test.service')")
|
|
b.enter_page("/system/services")
|
|
b.wait_js_cond('window.location.hash === "' + append_user("#/test.service") + '"')
|
|
|
|
def testApi(self):
|
|
b = self.browser
|
|
|
|
self.make_test_service()
|
|
|
|
self.login_and_go("/playground/service#/test")
|
|
|
|
b.wait_text('#exists', 'true')
|
|
b.wait_text('#state', '"stopped"')
|
|
b.wait_text('#enabled', 'false')
|
|
|
|
b.click('#start')
|
|
b.wait_text('#state', '"running"')
|
|
b.click('#stop')
|
|
b.wait_text('#state', '"stopped"')
|
|
|
|
b.click('#enable')
|
|
b.wait_text('#enabled', 'true')
|
|
b.click('#disable')
|
|
b.wait_text('#enabled', 'false')
|
|
|
|
b.go('#/foo')
|
|
b.wait_text('#exists', 'false')
|
|
|
|
def testConditions(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
self.write_file("/etc/systemd/system/condtest.service",
|
|
"""
|
|
[Unit]
|
|
Description=Test Service
|
|
ConditionDirectoryNotEmpty=/var/tmp/empty
|
|
|
|
[Service]
|
|
ExecStart=/bin/sh -c "while true; do sleep 5; done"
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
""")
|
|
|
|
m.execute("mkdir -p /var/tmp/empty")
|
|
m.execute("rm -rf /var/tmp/empty/*")
|
|
self.addCleanup(m.execute, "systemctl stop condtest")
|
|
|
|
# After writing files out tell systemd about them
|
|
m.execute("systemctl daemon-reload")
|
|
self.machine.execute("systemctl start multi-user.target")
|
|
|
|
# This does not work for not enabled services. See:
|
|
# https://github.com/systemd/systemd/issues/2234
|
|
self.machine.execute("systemctl enable condtest")
|
|
|
|
self.login_and_go("/system/services")
|
|
|
|
# Selects Services tab
|
|
self.pick_tab(1)
|
|
|
|
self.wait_service_in_panel("condtest.service", "Enabled")
|
|
self.goto_service("condtest.service")
|
|
self.check_service_details(["Not running", "Automatically starts"], ["Start", "Disallow running (mask)", "Pin unit"], True)
|
|
self.do_action("Start")
|
|
b.wait_text("#condition", "Condition ConditionDirectoryNotEmpty=/var/tmp/empty was not met")
|
|
|
|
# If the condition succeeds the message disappears
|
|
m.execute("touch /var/tmp/empty/non-empty")
|
|
self.addCleanup(m.execute, "rm /var/tmp/empty/non-empty")
|
|
self.do_action("Start")
|
|
self.check_service_details(["Running", "Automatically starts"], ["Restart", "Stop", "Disallow running (mask)", "Pin unit"], True)
|
|
b.wait_not_present("#condition")
|
|
|
|
def testRelationships(self):
|
|
self._testRelationships()
|
|
|
|
def testRelationshipsUser(self):
|
|
self._testRelationships(True)
|
|
|
|
def _testRelationships(self, user=False):
|
|
b = self.browser
|
|
|
|
systemd_path = "/etc/systemd/user" if user else "/etc/systemd/system"
|
|
|
|
self.write_file(f"{systemd_path}/test-a.service",
|
|
"""
|
|
[Unit]
|
|
Description=A Service
|
|
Before=test-b.service
|
|
Conflicts=test-c.service
|
|
|
|
[Service]
|
|
ExecStart=/bin/sh -c "while true; do sleep 5; done"
|
|
""")
|
|
|
|
self.write_file(f"{systemd_path}/test-b.service",
|
|
"""
|
|
[Unit]
|
|
Description=B Service
|
|
After=test-a.service
|
|
|
|
[Service]
|
|
ExecStart=/bin/sh -c "while true; do sleep 5; done"
|
|
""")
|
|
|
|
self.write_file(f"{systemd_path}/test-c.service",
|
|
"""
|
|
[Unit]
|
|
Description=C Service
|
|
Conflicts=test-a.service
|
|
PartOf=test-b.service
|
|
|
|
[Service]
|
|
ExecStart=/bin/sh -c "while true; do sleep 5; done"
|
|
""")
|
|
|
|
# After writing files out tell systemd about them
|
|
self.run_systemctl(user, "daemon-reload || true")
|
|
|
|
def rel_sel(reltype, service):
|
|
return f"#{''.join(reltype.split(' '))} a:contains('{service}')"
|
|
|
|
# services page
|
|
url = "/system/services#/?owner=user" if user else "/system/services"
|
|
self.login_and_go(url, user="admin")
|
|
self.wait_page_load()
|
|
|
|
self.wait_service_present("test-a.service")
|
|
self.goto_service("test-a.service")
|
|
|
|
usr_query_str = "?owner=user" if user else ""
|
|
|
|
# service a
|
|
b.wait_js_cond(f'window.location.hash === "#/test-a.service{usr_query_str}"')
|
|
b.click("#service-details-show-relationships button")
|
|
b.wait_visible(rel_sel("Before", "test-b.service"))
|
|
b.wait_visible(rel_sel("Conflicts", "test-c.service"))
|
|
b.click(rel_sel("Before", "test-b.service"))
|
|
|
|
# service b
|
|
b.wait_js_cond(f'window.location.hash === "#/test-b.service{usr_query_str}"')
|
|
b.click("#service-details-show-relationships button")
|
|
b.wait_visible(rel_sel("After", "test-a.service"))
|
|
b.click(rel_sel("After", "test-a.service"))
|
|
|
|
# service a
|
|
b.wait_js_cond(f'window.location.hash === "#/test-a.service{usr_query_str}"')
|
|
b.click("#service-details-show-relationships button")
|
|
b.wait_visible(rel_sel("Conflicts", "test-c.service"))
|
|
b.click(rel_sel("Conflicts", "test-c.service"))
|
|
|
|
# service c
|
|
b.wait_js_cond(f'window.location.hash === "#/test-c.service{usr_query_str}"')
|
|
b.click("#service-details-show-relationships button")
|
|
b.wait_visible(rel_sel("Conflicts", "test-a.service"))
|
|
b.wait_visible(rel_sel("Part of", "test-b.service"))
|
|
b.click(rel_sel("Part of", "test-b.service"))
|
|
|
|
# service b
|
|
b.wait_js_cond(f'window.location.hash === "#/test-b.service{usr_query_str}"')
|
|
b.click("#service-details-show-relationships button")
|
|
b.wait_visible(rel_sel("After", "test-a.service"))
|
|
|
|
def testNotFound(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
self.write_file("/etc/systemd/system/test.service",
|
|
"""
|
|
[Unit]
|
|
Description=Test Service
|
|
Requires=not-found.service
|
|
|
|
[Service]
|
|
ExecStart=/bin/true
|
|
""")
|
|
m.execute("systemctl daemon-reload")
|
|
self.machine.execute("systemctl start default.target")
|
|
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
|
|
b.wait_visible(self.svc_sel("test.service"))
|
|
b.wait_not_present(self.svc_sel("not-found.service"))
|
|
|
|
self.goto_service("test.service")
|
|
b.wait_js_cond('window.location.hash === "#/test.service"')
|
|
|
|
b.click("#service-details-show-relationships button")
|
|
b.wait_visible("#Requires a.pf-m-disabled:contains('not-found.service')")
|
|
|
|
def testResetFailed(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# Put it in /run instead of in /etc so that we can mask it,
|
|
# which needs to put a symlink into /etc.
|
|
self.write_file("/run/systemd/system/test-fail.service",
|
|
"""
|
|
[Unit]
|
|
Description=Failing Test Service
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/false
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
""")
|
|
|
|
m.execute("systemctl daemon-reload")
|
|
self.machine.execute("systemctl start default.target")
|
|
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
|
|
self.goto_service("test-fail.service")
|
|
b.wait_js_cond('window.location.hash === "#/test-fail.service"')
|
|
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], False)
|
|
|
|
self.wait_onoff(False)
|
|
self.toggle_onoff()
|
|
|
|
self.check_service_details(["Failed to start", "Automatically starts"],
|
|
["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], True)
|
|
|
|
# Disabling should reset the "Failed to start" status
|
|
self.wait_onoff(True)
|
|
self.toggle_onoff()
|
|
|
|
self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], False)
|
|
|
|
self.wait_onoff(False)
|
|
self.toggle_onoff()
|
|
|
|
self.check_service_details(["Failed to start", "Automatically starts"],
|
|
["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], True)
|
|
|
|
# Just reset the failed status, but leave it enabled
|
|
self.do_action("Clear")
|
|
|
|
self.check_service_details(["Not running", "Automatically starts"],
|
|
["Start", "Disallow running (mask)", "Pin unit"], True)
|
|
|
|
self.do_action("Start")
|
|
self.check_service_details(["Failed to start", "Automatically starts"],
|
|
["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], True)
|
|
|
|
# Masking should also clear the failed status
|
|
self.do_action("Disallow running (mask)")
|
|
b.click("#mask-service button.pf-m-danger")
|
|
self.check_service_details(["Masked"], ["Allow running (unmask)"], False, onoff=False)
|
|
|
|
def count_failures(self):
|
|
return int(self.machine.execute("systemctl --failed --plain --no-legend | wc -l"))
|
|
|
|
def check_system_menu_services_error(self, expected, pixel_label=None):
|
|
b = self.browser
|
|
b.switch_to_top()
|
|
if expected:
|
|
b.wait_visible("#services-error")
|
|
else:
|
|
b.wait_not_present("#services-error")
|
|
|
|
if pixel_label:
|
|
b.assert_pixels("#nav-system", pixel_label, skip_layouts=["mobile"])
|
|
b.set_layout("mobile")
|
|
b.click("#nav-system-item")
|
|
b.assert_pixels_in_current_layout("#nav-system", pixel_label)
|
|
b.click("#nav-system-item")
|
|
b.set_layout("desktop")
|
|
|
|
def testNotifyFailed(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
# We use two services with prefix "aaa" and "aab" so that
|
|
# they always occupy the first two rows and we can check their
|
|
# relative order depending on whether "aab" has failed or not.
|
|
|
|
self.write_file("/etc/systemd/system/aaa.service",
|
|
"""
|
|
[Unit]
|
|
Description=First Row Service
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/true
|
|
""")
|
|
self.write_file("/etc/systemd/system/aab-fail.service",
|
|
"""
|
|
[Unit]
|
|
Description=Failing Test Service
|
|
|
|
[Service]
|
|
ExecStart=/usr/bin/false
|
|
""")
|
|
|
|
m.execute("systemctl daemon-reload")
|
|
self.machine.execute("systemctl start default.target")
|
|
self.machine.execute("systemctl reset-failed")
|
|
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
|
|
# Start test-fail
|
|
m.execute("systemctl start aab-fail")
|
|
|
|
# Services tab should have icon
|
|
# Aab-fail should be at the top, and have failed
|
|
b.wait_visible('a:contains("Services") .ct-exclamation-circle')
|
|
b.wait_visible('tr[data-goto-unit="aab-fail.service"]')
|
|
b.wait_visible('tr[data-goto-unit="aab-fail.service"] .service-unit-status:contains("Failed")')
|
|
|
|
# Nav link should have icon
|
|
self.check_system_menu_services_error(True, pixel_label="menu_error")
|
|
|
|
n_failures = self.count_failures()
|
|
health_message = "1 service has failed" if n_failures == 1 else f"{n_failures} services have failed"
|
|
|
|
# System page should have notification
|
|
b.click_system_menu("/system")
|
|
b.wait_in_text(".system-health-events", health_message)
|
|
|
|
# Clear aab-fail
|
|
m.execute("systemctl reset-failed aab-fail")
|
|
|
|
# aab-fail should not be at the top
|
|
b.switch_to_top()
|
|
b.click_system_menu("/system/services")
|
|
b.wait_not_present('li:nth-child(1)[data-goto-unit="aab-fail.service"]')
|
|
|
|
if self.count_failures() == 0:
|
|
# Services tab should not have icon
|
|
b.wait_not_present('a:contains("Services") .ct-exclamation-circle')
|
|
|
|
# Nav link should not have icon
|
|
self.check_system_menu_services_error(False)
|
|
|
|
# System page should not have notification
|
|
b.click_system_menu('/system')
|
|
b.wait_not_in_text(".system-health-events", "service has failed")
|
|
b.wait_not_in_text(".system-health-events", "services have failed")
|
|
|
|
def testHiddenFailure(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
self.write_file("/etc/systemd/system/fail.mount",
|
|
"""
|
|
[Unit]
|
|
Description=Failing Mount
|
|
|
|
[Mount]
|
|
What=wrong
|
|
Where=/fail
|
|
""")
|
|
|
|
m.execute("systemctl daemon-reload")
|
|
self.machine.execute("systemctl start default.target")
|
|
self.machine.execute("systemctl reset-failed")
|
|
m.execute("systemctl start fail.mount || true")
|
|
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
|
|
if self.count_failures() == 1:
|
|
# Nav link should not have icon
|
|
self.check_system_menu_services_error(False)
|
|
|
|
# System page should not have notification
|
|
b.click_system_menu("/system")
|
|
b.wait_not_in_text(".system-health-events", "service has failed")
|
|
b.wait_not_in_text(".system-health-events", "services have failed")
|
|
|
|
self.allow_journal_messages(".*type=1400 audit(.*): avc: denied { create } .* comm=\"systemd\" name=\"fail\".*")
|
|
|
|
def testTransientUnits(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
|
|
m.execute("systemd-run --collect --unit test-autocollect@1.service sh -c 'sleep 5; false'")
|
|
m.execute("systemd-run --unit test-manual-collect@1.service sh -c 'sleep 5; false'")
|
|
|
|
self.wait_service_present("test-autocollect@1.service")
|
|
self.wait_service_present("test-manual-collect@1.service")
|
|
|
|
self.wait_service_in_panel("test-manual-collect@1.service", "Failed to start")
|
|
|
|
self.wait_service_not_present("test-autocollect@1.service")
|
|
self.wait_service_present("test-manual-collect@1.service")
|
|
|
|
m.execute("systemctl reset-failed")
|
|
self.wait_service_not_present("test-manual-collect@1.service")
|
|
|
|
# details page handles units going away
|
|
m.execute("systemd-run --collect --unit test-transient.service sleep infinity")
|
|
self.addCleanup(m.execute, "systemctl stop test-transient.service || true")
|
|
self.wait_service_present("test-transient.service")
|
|
self.goto_service("test-transient.service")
|
|
self.check_service_details(["Static", "Running"], ["Restart", "Stop", "Disallow running (mask)", "Pin unit"], False, onoff=False)
|
|
|
|
self.do_action("Stop")
|
|
|
|
b.wait_in_text(".pf-v5-c-empty-state", "Unit test-transient.service not found")
|
|
b.click("a:contains('View all services')")
|
|
b.switch_to_top()
|
|
b.wait_js_cond('window.location.pathname == "/system/services"')
|
|
|
|
def testServicesThemeConsistency(self):
|
|
b = self.browser
|
|
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
|
|
b.wait_not_present("html.pf-v5-theme-dark")
|
|
b.switch_to_top()
|
|
b.open_superuser_dialog()
|
|
b.click(".pf-v5-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
|
|
b.check_superuser_indicator("Limited access")
|
|
b.enter_page("/system/services")
|
|
b.wait_not_present("html.pf-v5-theme-dark")
|
|
|
|
def testServicesFiltersURLConsistency(self):
|
|
b = self.browser
|
|
|
|
# this filter matches nothing
|
|
self.login_and_go("/system/services#/?activestate=[\"Running\"]&filestate=[\"Disabled\"%2C\"Enabled\"]&name=test&type=service&owner=user")
|
|
self.wait_page_load()
|
|
|
|
b.wait_visible(".pf-v5-c-chip-group:contains(Active state) li:contains(Running)")
|
|
b.wait_visible(".pf-v5-c-chip-group:contains(File state) li:contains(Disabled)")
|
|
b.wait_visible(".pf-v5-c-chip-group:contains(File state) li:contains(Enabled)")
|
|
b.wait_visible(".services-text-filter input[value=test]")
|
|
b.wait_visible(".pf-v5-c-nav__list a.pf-m-current:contains(Services)")
|
|
b.wait_visible("#user.pf-m-selected")
|
|
# there is no running "test" service
|
|
self.browser.wait_in_text(".pf-v5-c-empty-state", "No matching results")
|
|
# filter survives a reload, same URL
|
|
b.reload()
|
|
b.enter_page("/system/services")
|
|
self.browser.wait_in_text(".pf-v5-c-empty-state", "No matching results")
|
|
|
|
# Changing the UI filters should also affect the URL
|
|
b.click("#system")
|
|
url = b.eval_js("window.location.hash")
|
|
self.assertIn("owner=system", url)
|
|
|
|
# Mass remove active state filters and check the URL
|
|
b.click(".pf-v5-c-chip-group:contains(Active state) .pf-v5-c-chip-group__close button")
|
|
b.wait_js_cond('window.location.hash == "#/?filestate=%5B%22Disabled%22%2C%22Enabled%22%5D&name=test&type=service&owner=system"')
|
|
|
|
b.click(".pf-v5-c-toolbar__content > .pf-v5-c-toolbar__item > button:contains('Clear all filters')")
|
|
b.wait_js_cond('window.location.hash == "#/?type=service&owner=system"')
|
|
|
|
# Test text input clear button
|
|
b.go('/system/services#/?name=test')
|
|
b.wait_visible(".services-text-filter input[value=test]")
|
|
b.click(".services-text-filter button")
|
|
b.wait_js_cond('window.location.hash == "#/"')
|
|
|
|
# ensure that there is at least one running service
|
|
self.make_test_service("/etc/systemd/user")
|
|
self.run_systemctl(True, "enable --now test.service")
|
|
b.click("#user")
|
|
self.wait_service_state("test.service", "active")
|
|
self.wait_service_state("test.service", "Enabled")
|
|
|
|
# drop the name search; all our images have at least stopped and static unit, which should not appear
|
|
b.go("/system/services#/?activestate=[\"Running\"]&filestate=[\"Disabled\"%2C\"Enabled\"]&type=service&owner=user")
|
|
self.wait_page_load()
|
|
b.wait_in_text(".services-list", "Running")
|
|
# filters out non-matching units
|
|
self.assertNotIn("Not running", b.text(".services-list"))
|
|
self.assertNotIn("Static", b.text(".services-list"))
|
|
# survives a page reload
|
|
b.reload()
|
|
b.enter_page("/system/services")
|
|
b.wait_in_text(".services-list", "Running")
|
|
self.assertNotIn("Not running", b.text(".services-list"))
|
|
self.assertNotIn("Static", b.text(".services-list"))
|
|
|
|
def testAlias(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
self.write_file("/etc/systemd/system/flower-rose.service",
|
|
"""
|
|
[Unit]
|
|
Description=Smell sweet
|
|
|
|
[Service]
|
|
ExecStart=/bin/echo Perfume
|
|
RemainAfterExit=yes
|
|
|
|
[Install]
|
|
Alias=flower-byanyothername.service
|
|
WantedBy=multi-user.target
|
|
""")
|
|
m.execute("systemctl enable flower-rose.service")
|
|
self.addCleanup(m.execute, "systemctl disable --now flower-rose.service")
|
|
|
|
self.login_and_go("/system/services#/?name=flower")
|
|
|
|
# overview: primary unit
|
|
self.wait_service_present("flower-rose.service")
|
|
self.wait_service_in_panel("flower-rose.service", "Enabled")
|
|
self.wait_service_state("flower-rose.service", "inactive")
|
|
|
|
# overview: alias
|
|
self.wait_service_present("flower-byanyothername.service")
|
|
if m.image.startswith("rhel-8") or m.image.startswith("centos-8"):
|
|
# old systemd does not have unit file state "alias" yet
|
|
self.wait_service_in_panel("flower-byanyothername.service", "Enabled")
|
|
else:
|
|
self.wait_service_in_panel("flower-byanyothername.service", "Alias")
|
|
# runtime status of aliases is unknown in list view
|
|
self.wait_service_state("flower-byanyothername.service", "")
|
|
|
|
# both react to state changes
|
|
m.execute("systemctl start flower-byanyothername.service")
|
|
self.wait_service_state("flower-rose.service", "active")
|
|
self.wait_service_state("flower-byanyothername.service", "")
|
|
|
|
# details: primary unit
|
|
self.goto_service("flower-rose.service")
|
|
b.wait_in_text("#statuses", "Running")
|
|
b.wait_in_text("#statuses", "Automatically starts")
|
|
b.wait_in_text("#service-details-unit", "/etc/systemd/system/flower-rose.service")
|
|
b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "Perfume")
|
|
|
|
# details: alias
|
|
b.click(".pf-v5-c-breadcrumb a:contains('Services')")
|
|
self.goto_service("flower-byanyothername.service")
|
|
b.wait_in_text("#statuses", "Running")
|
|
b.wait_in_text("#statuses", "Automatically starts")
|
|
b.wait_in_text("#service-details-unit", "/etc/systemd/system/flower-rose.service")
|
|
b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "Perfume")
|
|
|
|
def testUnprivileged(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.make_test_service()
|
|
|
|
self.login_and_go("/system/services", superuser=False)
|
|
self.wait_page_load()
|
|
|
|
# service list
|
|
self.wait_service_in_panel("NetworkManager.service", "Enabled")
|
|
self.wait_service_state("NetworkManager.service", "active")
|
|
self.wait_service_in_panel("test.service", "Disabled")
|
|
self.wait_service_state("test.service", "inactive")
|
|
# reacts to changes
|
|
m.execute("systemctl start test.service")
|
|
self.wait_service_state("test.service", "active")
|
|
|
|
# details page
|
|
self.goto_service("test.service")
|
|
self.check_service_details(["Read-only", "Running"], [], False, onoff=False, kebab=False)
|
|
b.wait_in_text("#service-details-unit", "/etc/systemd/system/test.service")
|
|
# unprivileged users cannot read the system journal on all OSes; only check it on Fedora, where we know it's allowed
|
|
if m.image.startswith("fedora"):
|
|
b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "WORKING")
|
|
# reacts to changes
|
|
m.execute("systemctl stop test.service")
|
|
self.check_service_details(["Read-only", "Disabled"], [], False, onoff=False, kebab=False)
|
|
|
|
|
|
@skipDistroPackage()
|
|
class TestTimers(MachineCase):
|
|
def svc_sel(self, service):
|
|
return f'tr[data-goto-unit="{service}"]'
|
|
|
|
def wait_page_load(self):
|
|
self.browser.wait_not_present(".pf-v5-c-empty-state .pf-v5-c-spinner[aria-valuetext='Loading...']")
|
|
|
|
def testCreate(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
def wait_systemctl_timer(time):
|
|
with testvm.Timeout(seconds=20, machine=m, error_message="Timeout while waiting for systemctl list-timers"):
|
|
m.execute(f"cmd='systemctl list-timers'; until $cmd | grep -m 1 '{time}'; do sleep 1; done")
|
|
|
|
# HACK: pmie and pmlogger take a looooong time to shutdown (https://bugzilla.redhat.com/show_bug.cgi?id=1703348)
|
|
# so disable them for this test, we don't test PCP here
|
|
m.execute("systemctl disable --now pmie pmlogger || true")
|
|
|
|
# set an initial baseline date/time, to ensure that we never jump backwards in subsequent tests
|
|
m.execute("timedatectl set-timezone UTC")
|
|
m.execute("""ntp=`timedatectl show --property NTP --value`
|
|
if [ $ntp == "yes" ]; then
|
|
timedatectl set-ntp off
|
|
fi""")
|
|
wait(lambda: "false" in m.execute(
|
|
"busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 org.freedesktop.timedate1 NTP"))
|
|
m.execute("timedatectl set-time '2036-01-01 12:00:00'")
|
|
m.reboot()
|
|
|
|
m.execute("timedatectl set-time '2036-01-01 15:30:00'")
|
|
self.login_and_go("/system/services")
|
|
self.wait_page_load()
|
|
# Select "Timers" tab
|
|
self.browser.click('#services-filter li:nth-child(4) a')
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "testing timer")
|
|
m.execute("rm -f /tmp/date")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.click("input[value=specific-time]")
|
|
b.set_input_text(".create-timer-time-picker input", "24:6s")
|
|
b.click("#timer-save-button")
|
|
|
|
# checks for invalid input messages
|
|
b.wait_text("#servicename-helper", "Only alphabets, numbers, : , _ , . , @ , - are allowed")
|
|
b.wait_text("#description-helper", "This field cannot be empty")
|
|
b.wait_text(".pf-v5-c-date-picker__helper-text", "Invalid time format")
|
|
|
|
# checks for command not found
|
|
b.set_input_text("#servicename", "testing")
|
|
b.set_input_text("#description", "desc")
|
|
b.set_input_text("#command", "this is not found")
|
|
b.set_input_text(".create-timer-time-picker input", "14:12")
|
|
b.click("#timer-save-button")
|
|
|
|
b.wait_text("#command-helper", "Command not found")
|
|
|
|
# creates a new yearly timer at 2036-01-01 16:00 and at 2037-01-01 01:22
|
|
b.set_input_text("#servicename", "yearly_timer")
|
|
b.set_input_text("#description", "Yearly timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.select_from_dropdown("#drop-repeat", "yearly")
|
|
b.click("[data-index=0] [aria-label=Add]")
|
|
b.set_input_text("[data-index=0] .pf-v5-c-date-picker:nth-child(1) input", "2036-01-01")
|
|
b.set_input_text("[data-index=0] .pf-v5-c-date-picker:nth-child(2) input", "16:00")
|
|
b.set_input_text("[data-index=1] .pf-v5-c-date-picker:nth-child(1) input", "2037-01-01")
|
|
b.set_input_text("[data-index=1] .pf-v5-c-date-picker:nth-child(2) input", "01:22")
|
|
|
|
# shows creation errors
|
|
m.execute("chattr +i /etc/systemd/system")
|
|
try:
|
|
b.click("#timer-save-button")
|
|
b.wait_in_text("#timer-dialog .pf-v5-c-alert", "Timer creation failed")
|
|
b.wait_in_text("#timer-dialog .pf-v5-c-alert", "Not permitted")
|
|
finally:
|
|
m.execute("chattr -i /etc/systemd/system")
|
|
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('yearly_timer.timer'))
|
|
|
|
m.execute("timedatectl set-time '2036-01-01 15:30:00'")
|
|
b.wait_visible(self.svc_sel('yearly_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('yearly_timer.timer'), "Yearly timer")
|
|
wait_systemctl_timer("2036-01-01 16:00")
|
|
self.assertIn("2036-01-01 16:00", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-01-01 16:10:00'")
|
|
# checks if yearly timer repeats yearly on 2037-01-01 01:22
|
|
wait_systemctl_timer("2037-01-01 01:22")
|
|
self.assertIn("2037-01-01 01:22", m.execute("systemctl list-timers"))
|
|
# creates a new monthly timer that runs on 6th at 14:12 and 8th at 21:12 of every month
|
|
b.wait_visible("#create-timer")
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "monthly_timer")
|
|
b.set_input_text("#description", "Monthly timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.click("input[value=specific-time]")
|
|
b.select_from_dropdown("#drop-repeat", "monthly")
|
|
b.select_from_dropdown("[data-index=0] .month-days", "6")
|
|
b.set_input_text("[data-index=0] .create-timer-time-picker input", "14:12")
|
|
b.click("[data-index=0] [aria-label=Add]")
|
|
b.select_from_dropdown("[data-index=1] .month-days", "8")
|
|
b.set_input_text("[data-index=1] .create-timer-time-picker input", "21:12")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('monthly_timer.timer'))
|
|
|
|
m.execute("timedatectl set-time '2036-01-01 16:15:00'")
|
|
b.wait_visible(self.svc_sel('monthly_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('monthly_timer.timer'), "Monthly timer")
|
|
wait_systemctl_timer("2036-01-06 14:12")
|
|
self.assertIn("2036-01-06 14:12", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-01-07 00:00:00'")
|
|
wait_systemctl_timer("2036-01-08 21:12")
|
|
self.assertIn("2036-01-08 21:12", m.execute("systemctl list-timers"))
|
|
# checks if timer runs on next month February 2036 on same dates
|
|
m.execute("timedatectl set-time '2036-01-08 21:23'")
|
|
wait_systemctl_timer("2036-02-06 14:12")
|
|
self.assertIn("2036-02-06 14:12", m.execute("systemctl list-timers"))
|
|
# checks if timer runs on 8th March 2036 at 21:12
|
|
m.execute("timedatectl set-time '2036-03-07 00:00:00'")
|
|
wait_systemctl_timer("2036-03-08 21:12")
|
|
self.assertIn("2036-03-08 21:12", m.execute("systemctl list-timers"))
|
|
# creates a new weekly timer that runs on Fri at 12:45 and Sun at 20:12 every week
|
|
b.wait_visible("#create-timer")
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "weekly_timer")
|
|
b.set_input_text("#description", "Weekly timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.click("input[value=specific-time]")
|
|
b.select_from_dropdown("#drop-repeat", "weekly")
|
|
b.wait_visible("[data-index=0] .week-days")
|
|
b.click("[data-index=0] [aria-label=Add]")
|
|
b.select_from_dropdown("[data-index=0] .week-days", "fri")
|
|
b.set_input_text("[data-index=0] .create-timer-time-picker input", "12:45")
|
|
b.select_from_dropdown("[data-index=1] .week-days", "sun")
|
|
b.set_input_text("[data-index=1] .create-timer-time-picker input", "20:12")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('weekly_timer.timer'))
|
|
|
|
m.execute("timedatectl set-time '2036-03-08 00:00:00'")
|
|
b.wait_visible(self.svc_sel('weekly_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('weekly_timer.timer'), "Weekly timer")
|
|
wait_systemctl_timer("Sun 2036-03-09 20:12")
|
|
self.assertIn("Sun 2036-03-09 20:12", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-03-10 00:00:00'")
|
|
wait_systemctl_timer("Fri 2036-03-14 12:45")
|
|
self.assertIn("Fri 2036-03-14 12:45", m.execute("systemctl list-timers"))
|
|
# checks if timer runs on next week's Friday and Sunday
|
|
m.execute("timedatectl set-time '2036-03-15 00:00:00'")
|
|
wait_systemctl_timer("Sun 2036-03-16 20:12")
|
|
self.assertIn("Sun 2036-03-16 20:12", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-03-17 00:00:00'")
|
|
wait_systemctl_timer("Fri 2036-03-21 12:45")
|
|
self.assertIn("Fri 2036-03-21 12:45", m.execute("systemctl list-timers"))
|
|
# creates a new daily timer that runs at 2:40 and at 21:15 every day
|
|
b.wait_visible("#create-timer")
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "daily_timer")
|
|
b.set_input_text("#description", "Daily timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.click("input[value=specific-time]")
|
|
b.select_from_dropdown("#drop-repeat", "daily")
|
|
b.click("[data-index=0] [aria-label=Add]")
|
|
b.set_input_text("[data-index=0] .create-timer-time-picker input", "2:40")
|
|
b.set_input_text("[data-index=1] .create-timer-time-picker input", "21:15")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('daily_timer.timer'))
|
|
|
|
m.execute("timedatectl set-time '2036-03-17 00:00:00'")
|
|
b.wait_visible(self.svc_sel('daily_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('daily_timer.timer'), "Daily timer")
|
|
wait_systemctl_timer("2036-03-17 02:40")
|
|
self.assertIn("2036-03-17 02:40", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-03-17 03:00:00'")
|
|
wait_systemctl_timer("2036-03-17 21:15")
|
|
self.assertIn("2036-03-17 21:15", m.execute("systemctl list-timers"))
|
|
# checks if timer runs on 2036-04-10 at 02:40 and 21:15
|
|
m.execute("timedatectl set-time '2036-04-10 00:00:00'")
|
|
wait_systemctl_timer("2036-04-10 02:40")
|
|
self.assertIn("2036-04-10 02:40", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-04-10 03:00:00'")
|
|
wait_systemctl_timer("2036-04-10 21:15")
|
|
self.assertIn("2036-04-10 21:15", m.execute("systemctl list-timers"))
|
|
# creates a new houry timer that runs at *:05 and at *:26
|
|
b.wait_visible("#create-timer")
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "hourly_timer")
|
|
b.set_input_text("#description", "Hourly timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.click("input[value=specific-time]")
|
|
b.select_from_dropdown("#drop-repeat", "hourly")
|
|
b.click("[data-index=0] [aria-label=Add]")
|
|
b.set_input_text("[data-index=0] input", "05")
|
|
b.set_input_text("[data-index=1] input", "26")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('hourly_timer.timer'))
|
|
|
|
m.execute("timedatectl set-time '2036-04-10 03:00:00'")
|
|
b.wait_visible(self.svc_sel('hourly_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('hourly_timer.timer'), "Hourly timer")
|
|
wait_systemctl_timer("2036-04-10 03:05")
|
|
self.assertIn("2036-04-10 03:05", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-04-10 03:07:00'")
|
|
wait_systemctl_timer("2036-04-10 03:26")
|
|
self.assertIn("2036-04-10 03:26", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-04-10 04:00:00'")
|
|
wait_systemctl_timer("2036-04-10 04:05")
|
|
# checks if timer runs on next hour at 5 min and 26 min
|
|
self.assertIn("2036-04-10 04:05", m.execute("systemctl list-timers"))
|
|
m.execute("timedatectl set-time '2036-04-10 04:10:00'")
|
|
wait_systemctl_timer("2036-04-10 04:26")
|
|
self.assertIn("2036-04-10 04:26", m.execute("systemctl list-timers"))
|
|
|
|
# creates a new minutely timer that runs at *:*:05 and at *:*:20
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "minutely_timer")
|
|
b.set_input_text("#description", "Minutely timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.select_from_dropdown("#drop-repeat", "minutely")
|
|
b.click("[data-index=0] [aria-label=Add]")
|
|
b.set_input_text("[data-index=0] input", "05")
|
|
b.set_input_text("[data-index=1] input", "20")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('minutely_timer.timer'))
|
|
|
|
m.execute("timedatectl set-time '2036-04-10 04:15:07'")
|
|
b.wait_visible(self.svc_sel('minutely_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('minutely_timer.timer'), "Minutely timer")
|
|
wait_systemctl_timer("2036-04-10 04:15:20")
|
|
self.assertIn("2036-04-10 04:15:20", m.execute("systemctl list-timers | grep minutely_timer.timer"))
|
|
m.execute("timedatectl set-time '2036-04-10 04:15:55'")
|
|
wait_systemctl_timer("2036-04-10 04:16:05")
|
|
self.assertIn("2036-04-10 04:16:05", m.execute("systemctl list-timers | grep minutely_timer.timer"))
|
|
|
|
# creates a new timer that runs at today at 23:59
|
|
b.wait_visible("#create-timer")
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "no_repeat_timer")
|
|
b.set_input_text("#description", "No repeat timer")
|
|
b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
|
|
b.click("input[value=specific-time]")
|
|
b.set_input_text(".create-timer-time-picker input", "23:59")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('no_repeat_timer.timer'))
|
|
b.wait_in_text(self.svc_sel('no_repeat_timer.timer'), "No repeat timer")
|
|
|
|
m.execute("timedatectl set-time '2036-04-10 04:10:00'")
|
|
b.wait_visible(self.svc_sel('no_repeat_timer.timer'))
|
|
wait_systemctl_timer("2036-04-10 23:59")
|
|
self.assertIn("2036-04-10 23:59", m.execute("systemctl list-timers"))
|
|
|
|
# creates a boot timer that runs after 10 sec from boot
|
|
b.wait_visible("#create-timer")
|
|
b.click('#create-timer')
|
|
b.wait_visible("#timer-dialog")
|
|
b.set_input_text("#servicename", "boot_timer")
|
|
b.set_input_text("#description", "Boot timer")
|
|
b.set_input_text("#command", "/bin/sh -c 'echo hello >> /tmp/hello'")
|
|
b.click("input[value=system-boot]")
|
|
b.set_input_text(".delay-group input", "2")
|
|
b.click("#timer-save-button")
|
|
b.wait_not_present("#timer-dialog")
|
|
b.wait_visible(self.svc_sel('boot_timer.timer'))
|
|
m.reboot()
|
|
m.start_cockpit()
|
|
with testvm.Timeout(seconds=15, machine=m, error_message="Timeout while waiting for boot timer to run"):
|
|
m.execute("while [ ! -f /tmp/hello ] ; do sleep 0.5; done")
|
|
self.assertIn("hello", m.execute("cat /tmp/hello"))
|
|
|
|
self.allow_restart_journal_messages()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_main()
|