diff --git a/.gitignore b/.gitignore
index b6d108ec306e..4bc3a773a00c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,7 @@ Makefile.in
/tags
/test-*
/test_rsa_key
+/Test[A-Z]*
/tmp-dist
/tmp/
/tsconfig.tsbuildinfo
diff --git a/pkg/lib/kernelopt.sh b/pkg/lib/kernelopt.sh
index 7f319ad277aa..65d5c01b8c2e 100755
--- a/pkg/lib/kernelopt.sh
+++ b/pkg/lib/kernelopt.sh
@@ -14,8 +14,8 @@ error() {
grub() {
key="${2%=*}" # split off optional =value
- # For the non-BLS case, or if someone overrides those with grub2-mkconfig
- # or update-grub, change it in /etc/default/grub
+ # For the non-BLS case, or if someone overrides those with grub2-mkconfig,
+ # update-grub or update-bootloader, change it in /etc/default/grub
if [ -e /etc/default/grub ]; then
if [ "$1" = set ]; then
# replace existing argument, otherwise append it
@@ -37,6 +37,11 @@ grub() {
elif [ -e /etc/default/grub ] && type update-grub >/dev/null 2>&1; then
update-grub
+ # on Suse platforms, use update-bootloader
+ # Since earlier we updated /etc/default/grub we can just run update-bootloader
+ elif type update-bootloader >/dev/null 2>&1; then
+ update-bootloader
+
# on OSTree, the kernel config is inside the image
elif cur=$(rpm-ostree kargs 2>&1); then
if [ "$1" = set ]; then
@@ -50,7 +55,7 @@ grub() {
rpm-ostree kargs --delete="$key"
fi
else
- error "No supported grub update mechanism found (grubby, update-grub, or rpm-ostree)"
+ error "No supported grub update mechanism found (grubby, update-grub, update-bootloader or rpm-ostree)"
fi
}
diff --git a/pkg/metrics/manifest.json b/pkg/metrics/manifest.json
index 4a868e38cc08..a22d5e2c79fa 100644
--- a/pkg/metrics/manifest.json
+++ b/pkg/metrics/manifest.json
@@ -12,7 +12,8 @@
"config": {
"redis_package": {
"fedora": "valkey",
- "platform:el10": "valkey"
+ "platform:el10": "valkey",
+ "opensuse-tumbleweed": "valkey"
}
}
}
diff --git a/pkg/metrics/metrics.jsx b/pkg/metrics/metrics.jsx
index dc217c25e070..923c992bc8e1 100644
--- a/pkg/metrics/metrics.jsx
+++ b/pkg/metrics/metrics.jsx
@@ -1323,12 +1323,12 @@ const wait_cond = (cond, objects) => {
const PCPConfigDialog = ({
firewalldRequest,
- s_pmlogger, s_pmproxy, s_redis, s_redis_server, s_valkey,
- packageInstallCallback,
+ s_pmlogger, s_pmproxy, s_redis, s_redis_server, s_valkey, s_valkey_target,
+ packageInstallCallback
}) => {
const Dialogs = useDialogs();
const dialogInitialProxyValue = runningService(s_pmproxy) && (
- runningService(s_redis) || runningService(s_redis_server) || runningService(s_valkey));
+ runningService(s_redis) || runningService(s_redis_server) || runningService(s_valkey) || runningService(s_valkey_target));
const [dialogError, setDialogError] = useState(null);
const [dialogLoggerValue, setDialogLoggerValue] = useState(runningService(s_pmlogger));
const [dialogProxyValue, setDialogProxyValue] = useState(dialogInitialProxyValue);
@@ -1343,7 +1343,7 @@ const PCPConfigDialog = ({
if (dialogLoggerValue && !s_pmlogger.exists) {
missing.push(...await get_pcp_packages());
}
- const redisExists = () => s_redis.exists || s_redis_server.exists || s_valkey.exists;
+ const redisExists = () => s_redis.exists || s_redis_server.exists || s_valkey.exists || s_valkey_target.exists;
if (dialogProxyValue && !redisExists()) {
const os_release = await read_os_release();
missing.push(get_manifest_config_matchlist("metrics", "redis_package", "redis",
@@ -1357,7 +1357,7 @@ const PCPConfigDialog = ({
debug("PCPConfig: package installation successful");
await wait_cond(() => (s_pmlogger.exists &&
(!dialogProxyValue || (s_pmproxy.exists && redisExists()))),
- [s_pmlogger, s_pmproxy, s_redis, s_redis_server, s_valkey]);
+ [s_pmlogger, s_pmproxy, s_redis, s_redis_server, s_valkey, s_valkey_target]);
}
};
@@ -1373,6 +1373,9 @@ const PCPConfigDialog = ({
if (s_valkey.exists && s_valkey.unit?.UnitFileState !== 'masked') {
real_redis = s_valkey;
redis_name = "valkey.service";
+ } else if (s_valkey_target.exists && s_valkey_target.unit?.UnitFileState !== 'masked') {
+ real_redis = s_valkey_target;
+ redis_name = "valkey.target";
} else if (s_redis_server.exists && s_redis_server.unit?.UnitFileState !== 'masked') {
real_redis = s_redis_server;
redis_name = "redis-server.service";
@@ -1498,6 +1501,7 @@ const PCPConfig = ({ buttonVariant, firewalldRequest }) => {
const s_redis = useObject(() => service.proxy("redis.service"), null, []);
const s_redis_server = useObject(() => service.proxy("redis-server.service"), null, []);
const s_valkey = useObject(() => service.proxy("valkey.service"), null, []);
+ const s_valkey_target = useObject(() => service.proxy("valkey.target"), null, []);
useEvent(superuser, "changed");
useEvent(s_pmlogger, "changed");
@@ -1505,12 +1509,14 @@ const PCPConfig = ({ buttonVariant, firewalldRequest }) => {
useEvent(s_redis, "changed");
useEvent(s_redis_server, "changed");
useEvent(s_valkey, "changed");
+ useEvent(s_valkey_target, "changed");
debug("PCPConfig s_pmlogger.state", s_pmlogger.state);
debug("PCPConfig s_pmproxy state", s_pmproxy.state,
"redis exists", s_redis.exists, "state", s_redis.state,
"redis-server exists", s_redis_server.exists, "state", s_redis_server.state,
- "valkey exists", s_valkey.exists, "state", s_valkey.state);
+ "valkey exists", s_valkey.exists, "state", s_valkey.state,
+ "valkey-target exists", s_valkey_target.exists, "state", s_valkey_target.state);
if (!superuser.allowed)
return null;
@@ -1521,13 +1527,14 @@ const PCPConfig = ({ buttonVariant, firewalldRequest }) => {
s_pmlogger={s_pmlogger}
s_pmproxy={s_pmproxy}
s_redis={s_redis} s_redis_server={s_redis_server} s_valkey={s_valkey}
+ s_valkey_target={s_valkey_target}
packageInstallCallback={() => setPackageInstallStatus("done")} />);
}
return (
}
isDisabled={ invalidService(s_pmlogger) || invalidService(s_pmproxy) ||
- invalidService(s_redis) || invalidService(s_redis_server) || invalidService(s_valkey) }
+ invalidService(s_redis) || invalidService(s_redis_server) || invalidService(s_valkey) || invalidService(s_valkey_target) }
onClick={show_dialog}
data-test-install-finished={packageInstallStatus}>
{ _("Metrics settings") }
diff --git a/pkg/networkmanager/firewall.jsx b/pkg/networkmanager/firewall.jsx
index 8ff8945371f1..4ecdab43341f 100644
--- a/pkg/networkmanager/firewall.jsx
+++ b/pkg/networkmanager/firewall.jsx
@@ -556,10 +556,24 @@ class AddEditServicesModal extends React.Component {
componentDidMount() {
firewall.getAvailableServices()
.then(services => this.setState({ services }));
- cockpit.file('/etc/services').read()
- .then(content => this.setState({
- avail_services: this.parseServices(content)
- }));
+ async function fetchServices(component) {
+ try {
+ await cockpit.spawn(["test", "-e", "/usr/etc/services"], { err: "ignore" });
+ cockpit.file('/usr/etc/services').read()
+ .then(content => component.setState({
+ avail_services: component.parseServices(content)
+ }));
+ } catch (err1) {
+ cockpit.file('/etc/services').read()
+ .then(content => component.setState({
+ avail_services: component.parseServices(content)
+ }));
+ }
+ }
+
+ if (this.state.avail_services === null) {
+ fetchServices(this);
+ }
}
onFilterChanged(value) {
diff --git a/pkg/storaged/multipath.jsx b/pkg/storaged/multipath.jsx
index 4179941aa287..cdc2c33dfb74 100644
--- a/pkg/storaged/multipath.jsx
+++ b/pkg/storaged/multipath.jsx
@@ -50,17 +50,29 @@ export class MultipathAlert extends React.Component {
const multipathd_running = !this.multipathd_service.state || this.multipathd_service.state === "running";
const multipath_broken = client.broken_multipath_present === true;
- function activate(event) {
+ async function activate(event) {
if (!event || event.button !== 0)
return;
- cockpit.spawn(["mpathconf", "--enable", "--with_multipathd", "y"],
- { superuser: "try" })
- .catch(function (error) {
- dialog_open({
- Title: _("Error"),
- Body: error.toString()
+ try {
+ await cockpit.spawn(["type", "mpathconf"], { err: "ignore" });
+ cockpit.spawn(["mpathconf", "--enable", "--with_multipathd", "y"],
+ { superuser: "try" })
+ .catch(function (error) {
+ dialog_open({
+ Title: _("Error"),
+ Body: error.toString()
+ });
});
- });
+ } catch (err1) {
+ cockpit.spawn(["systemctl", "enable", "--now", "multipathd.service"],
+ { superuser: "try" })
+ .catch(function (error) {
+ dialog_open({
+ Title: _("Error"),
+ Body: error.toString()
+ });
+ });
+ }
}
if (multipath_broken && !multipathd_running)
diff --git a/pkg/users/users.js b/pkg/users/users.js
index 28a8a2bee36b..329c1167809a 100755
--- a/pkg/users/users.js
+++ b/pkg/users/users.js
@@ -91,7 +91,14 @@ function AccountsPage() {
const handleShadow = cockpit.file("/etc/shadow", { superuser: "try" });
handleShadow.watch(() => debouncedGetLoginDetails(), { read: false });
- const handleLogindef = cockpit.file("/etc/login.defs");
+ let handleLogindef;
+ try {
+ await cockpit.spawn(["test", "-e", "/etc/login.defs"], { err: "ignore" });
+ handleLogindef = cockpit.file("/etc/login.defs");
+ } catch (err1) {
+ handleLogindef = cockpit.file("/usr/etc/login.defs");
+ }
+
handleLogindef.watch((logindef) => {
if (logindef === null)
return;
diff --git a/src/common/cockpitconf.c b/src/common/cockpitconf.c
index 7e070346e8a9..e809f1785ca7 100644
--- a/src/common/cockpitconf.c
+++ b/src/common/cockpitconf.c
@@ -76,7 +76,7 @@ regcompx (regex_t *preg, const char *regex, int cflags)
/* For optimization, this modifies string; the returned array has pointers into string
* The array itself gets allocated and must be freed after use. */
static const char **
-strsplit (char *string, char delimiter)
+strsplit (char *string, char delimiter, unsigned *len_out)
{
const char ** parts = reallocarrayx (NULL, 2, sizeof (char*));
char *cur = string;
@@ -104,6 +104,10 @@ strsplit (char *string, char delimiter)
}
parts[len] = NULL;
+
+ if (len_out)
+ *len_out = len;
+
return parts;
}
@@ -299,9 +303,21 @@ cockpit_conf_get_dirs (void)
env = getenv ("XDG_CONFIG_DIRS");
if (env && env[0])
{
+ unsigned len_out;
+
+ const char ** xdg_config_dirs = NULL;
+ unsigned len = sizeof (cockpit_config_dirs) / sizeof (char*);
+ system_config_dirs = reallocarrayx (NULL, len, sizeof (char*));
+ memcpy (system_config_dirs, cockpit_config_dirs, sizeof (cockpit_config_dirs));
+
/* strsplit() modifies the string inline, so copy and keep a ref */
env = strdup (env);
- system_config_dirs = strsplit (env, ':');
+ xdg_config_dirs = strsplit (env, ':', &len_out);
+
+ system_config_dirs = reallocarrayx (system_config_dirs, len + len_out + 1, sizeof (char*));
+ memcpy (system_config_dirs + (len-1), xdg_config_dirs, len_out * sizeof (char*));
+ system_config_dirs[len - 1 + len_out] = NULL;
+ free (xdg_config_dirs);
}
}
@@ -348,7 +364,7 @@ cockpit_conf_strv (const char *section,
entry->strv_value = strdupx (entry->value);
for (char *c = entry->strv_value + strlen (entry->strv_value) - 1; c >= entry->strv_value && isspace (*c); --c)
*c = '\0';
- entry->strv_cache = strsplit (entry->strv_value, delimiter);
+ entry->strv_cache = strsplit (entry->strv_value, delimiter, NULL);
entry->strv_delimiter = delimiter;
}
diff --git a/src/common/test-config.c b/src/common/test-config.c
index b6d8a31d0fef..675405e2fccf 100644
--- a/src/common/test-config.c
+++ b/src/common/test-config.c
@@ -139,7 +139,7 @@ test_load_dir (void)
cockpit_config_file = "cockpit.conf";
g_assert_cmpstr (cockpit_conf_string ("Section2", "value1"), ==, "string");
- g_assert_cmpstr (cockpit_conf_get_dirs ()[1], ==, SRCDIR "/src/ws/mock-config");
+ g_assert_cmpstr (cockpit_conf_get_dirs ()[2], ==, SRCDIR "/src/ws/mock-config");
cockpit_conf_cleanup ();
}
diff --git a/test/common/netlib.py b/test/common/netlib.py
index 053830211d0a..e6ded64d0104 100644
--- a/test/common/netlib.py
+++ b/test/common/netlib.py
@@ -44,8 +44,11 @@ def add_veth(self, name, dhcp_cidr=None, dhcp_range=None):
if dhcp_cidr:
# up the remote end, give it an IP, and start DHCP server
self.machine.execute(f"ip a add {dhcp_cidr} dev v_{name}; ip link set v_{name} up")
- server = self.machine.spawn("dnsmasq --keep-in-foreground --log-queries --log-facility=- "
- f"--conf-file=/dev/null --dhcp-leasefile=/tmp/leases.{name} --no-resolv "
+
+ # TODO: Consider running all env's through /run/dnsmasq
+ lease_path = "/run/dnsmasq" if "suse" in self.machine.image else "/tmp"
+ server = self.machine.spawn(f"mkdir -p {lease_path}; dnsmasq --keep-in-foreground --log-queries --log-facility=- "
+ f"--conf-file=/dev/null --dhcp-leasefile={lease_path}/leases.{name} --no-resolv "
f"--bind-interfaces --except-interface=lo --interface=v_{name} --dhcp-range={dhcp_range[0]},{dhcp_range[1]},4h",
f"dhcp-{name}.log")
self.addCleanup(self.machine.execute, "kill %i" % server)
diff --git a/test/common/packagelib.py b/test/common/packagelib.py
index 02877b40d36e..1cbede27b2fb 100644
--- a/test/common/packagelib.py
+++ b/test/common/packagelib.py
@@ -46,6 +46,10 @@ def setUp(self):
self.backend = "dnf5"
self.primary_arch = "noarch"
self.secondary_arch = "x86_64"
+ elif "suse" in self.machine.image:
+ self.backend = "zypper"
+ self.primary_arch = "noarch"
+ self.secondary_arch = "x86_64"
elif self.machine.image == "arch":
self.backend = "alpm"
self.primary_arch = "any"
@@ -157,7 +161,7 @@ def createPackage(self, name, version, release, install=False,
else:
self.createRpm(name, version, release, depends, postinst, install, content, arch, provides)
if updateinfo:
- self.updateInfo[(name, version, release)] = updateinfo
+ self.updateInfo[name, version, release] = updateinfo
def createDeb(self, name, version, depends, postinst, install, content, arch, provides):
"""Create a dummy deb in repo_dir on self.machine
@@ -262,6 +266,13 @@ def createRpm(self, name, version, release, requires, post, install, content, ar
cp ~/rpmbuild/RPMS/{4}/*.rpm {0}
rm -rf ~/rpmbuild
"""
+ if self.backend == "zypper":
+ cmd = """
+rpmbuild --quiet -bb /tmp/spec
+mkdir -p {0}
+cp /usr/src/packages/RPMS/{4}/*.rpm {0}
+"""
+
if install:
cmd += "rpm -i {0}/{1}-{2}-{3}.*.rpm"
self.machine.execute(cmd.format(self.repo_dir, name, version, release, arch))
@@ -435,6 +446,10 @@ def enableRepo(self):
self.machine.write("/etc/pacman.conf", config)
self.machine.execute("pacman -Sy")
+ elif self.backend == "zypper":
+ # Need to work out how zypper handles repo changelogs so we can handle that here
+ self.machine.execute(f"zypper ar --no-gpgcheck --refresh {self.repo_dir} local")
+
else:
# HACK - https://bugzilla.redhat.com/show_bug.cgi?id=2306114
# We need to explicitly create /var/cache/libdnf5 to make "dnf clean" happy.
diff --git a/test/common/testlib.py b/test/common/testlib.py
index f6a96951940e..b30f66885bda 100644
--- a/test/common/testlib.py
+++ b/test/common/testlib.py
@@ -1094,7 +1094,7 @@ def become_superuser(
# In (open)SUSE images, superuser access always requires the root password
if user is None:
- user = "root" if "suse" in self.machine.image else "admin"
+ user = get_superuser(self.machine.image)
if passwordless:
self.wait_in_text("div[role=dialog]", "Administrative access")
@@ -1776,7 +1776,7 @@ def setUp(self, restrict: bool = True) -> None:
self.sshd_socket = 'ssh.socket'
else:
self.sshd_service = 'sshd.service'
- if image == 'arch':
+ if image == 'arch' or "suse" in image:
self.sshd_socket = None
else:
self.sshd_socket = 'sshd.socket'
@@ -1785,7 +1785,7 @@ def setUp(self, restrict: bool = True) -> None:
# only enabled by default on released OSes; see pkg/shell/manifest.json
self.multihost_enabled = image.startswith(("rhel-9", "centos-9")) or image in [
"ubuntu-2204", "ubuntu-2404", "debian-stable",
- "fedora-39", "fedora-40"]
+ "fedora-39", "fedora-40", "opensuse-tumbleweed"]
# Transitional code while we move ubuntu-stable from 24.04 to 24.10
if image == "ubuntu-stable" and m.execute(". /etc/os-release; echo $VERSION_ID").strip() == "24.04":
self.multihost_enabled = True
@@ -2052,6 +2052,12 @@ def login_and_go(
# timedatex.service shuts down after timeout, runs into race condition with property watching
".*org.freedesktop.timedate1: couldn't get all properties.*Error:org.freedesktop.DBus.Error.NoReply.*",
+
+ # https://github.com/cockpit-project/cockpit/issues/19235
+ "invalid non-UTF8 @data passed as text to web_socket_connection_send.*",
+
+ # Noise from btmp file missing systems where it doesn't exists
+ r"cockpit-session: open\(\/var\/log\/btmp\) failed: No such file or directory",
]
default_allowed_messages += os.environ.get("TEST_ALLOW_JOURNAL_MESSAGES", "").split(",")
@@ -2467,6 +2473,15 @@ def get_decorator(method: object, _class: object, name: str, default: Any = None
return getattr(method, attr, getattr(_class, attr, default))
+def get_superuser(image: str) -> str:
+ # In (open)SUSE images, superuser access always requires the root password
+ return "root" if "suse" in image else "admin"
+
+
+def get_sshd_config_path(image: str) -> str:
+ # In (open)SUSE images, superuser access always requires the root password
+ return "/usr/etc/ssh/sshd_config" if "suse" in image else "/etc/ssh/sshd_config"
+
###########################
# Test decorators
#
diff --git a/test/verify/check-apps b/test/verify/check-apps
index 6ad8202dea6a..c6143f018c89 100755
--- a/test/verify/check-apps
+++ b/test/verify/check-apps
@@ -80,6 +80,7 @@ class TestApps(packagelib.PackageCase):
# ignore the corresponding journal entry
self.allow_journal_messages("org.freedesktop.PackageKit.*org.freedesktop.DBus.Error.NoReply.*")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self, urlroot=""):
b = self.browser
m = self.machine
@@ -139,9 +140,11 @@ class TestApps(packagelib.PackageCase):
b.wait_visible(f".app-list .pf-v5-c-data-list__item-row:contains('app-1') img[src^='{urlroot}/cockpit/channel/']")
m.execute("! test -f /stamp-app-1-1.0-1")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testWithUrlRoot(self):
self.testBasic(urlroot="/webcon")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testOsMap(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-client b/test/verify/check-client
index 8367f83262e8..ccc468f40ebd 100755
--- a/test/verify/check-client
+++ b/test/verify/check-client
@@ -94,11 +94,13 @@ exec "$@"
self.m_target.execute("while pgrep -af '([c]ockpit|[s]sh-agent)' >&2; do sleep 1; done",
timeout=30)
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBeibootNoBridge(self):
# set up target machine: no cockpit
self.m_target.execute("rm /usr/bin/cockpit-bridge; rm -r /usr/share/cockpit")
self.checkLoginScenarios()
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBeibootWithBridge(self):
self.checkLoginScenarios()
diff --git a/test/verify/check-connection b/test/verify/check-connection
index 9c7906e55091..3f13bf0c54e9 100755
--- a/test/verify/check-connection
+++ b/test/verify/check-connection
@@ -597,13 +597,29 @@ class TestConnection(testlib.MachineCase):
m = self.machine
# non-admin user
- m.execute("useradd user")
+ m.execute("useradd -m user")
+ if "user" not in m.execute("cat /etc/group"):
+ m.execute("groupadd user")
+ m.execute("usermod -a -G user user")
# enable no-password login for 'admin' and 'user'
m.execute("passwd -d admin")
m.execute("passwd -d user")
- self.sed_file('$ a\\\nPermitEmptyPasswords yes', '/etc/ssh/sshd_config',
+ # HACK: For SUSE images, PAM for ssh is setup in a way that disallowed passwordless ssh
+ # In order to get around this lets generate an ssh key and handle auth that way
+ if "suse" in m.image:
+ m.execute("ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ''")
+ m.execute("""cat /root/.ssh/id_ed25519.pub > /home/admin/.ssh/authorized_keys
+ chown admin:admin /home/admin/.ssh/authorized_keys
+ chmod 600 /home/admin/.ssh/authorized_keys""")
+ m.execute("""mkdir -p /home/user/.ssh
+ cat /root/.ssh/id_ed25519.pub > /home/user/.ssh/authorized_keys
+ chown -R user:user /home/user/.ssh
+ chmod 700 /home/user/.ssh
+ chmod 600 /home/user/.ssh/authorized_keys""")
+
+ self.sed_file('$ a\\\nPermitEmptyPasswords yes', testlib.get_sshd_config_path(m.image),
self.restart_sshd)
def assertInOrNot(string: str, result: str, *, expected: bool) -> None:
@@ -1107,7 +1123,24 @@ until pgrep -f '^(/usr/[^ ]+/[^ /]*python[^ /]* )?/usr/bin/cockpit-bridge'; do s
# branding.css undergoes variable substitution based on the content of
# /usr/lib/os-release. Perform the substitution for ourselves for
# validation. envsubst comes from gettext.
- os_release_vars = m.execute("cat /usr/lib/os-release").replace('\n', ' ')
+ os_release_vars = m.execute("cat /usr/lib/os-release")
+ # Sometimes os-release has lines containing comments which messes up the shell
+ if "#" in os_release_vars:
+ valid_lines = []
+ for line in os_release_vars.split('\n'):
+ line = line.strip()
+ # Ignore commented out lines
+ if line.startswith('#'):
+ continue
+
+ # remove the comment at the end of line if it exists
+ line = line.split('#')[0]
+ valid_lines.append(line)
+
+ os_release_vars = ' '.join(valid_lines)
+ else:
+ os_release_vars = os_release_vars.replace('\n', ' ')
+
m.execute(f'{os_release_vars} envsubst < {branddir}/branding.css > /tmp/branding.ref')
self.addCleanup(m.execute, "rm /tmp/branding.ref")
@@ -1130,7 +1163,10 @@ until pgrep -f '^(/usr/[^ ]+/[^ /]*python[^ /]* )?/usr/bin/cockpit-bridge'; do s
# some brands miss the images, but the OSes we CI have them all
curl_and_compare('branding.css', 'text/css', reference='/tmp/branding.ref')
- curl_and_compare('logo.png', 'image/png')
+ if "suse" in m.image:
+ curl_and_compare('square-hicolor.svg', 'image/svg')
+ else:
+ curl_and_compare('logo.png', 'image/png')
curl_and_compare('favicon.ico')
# do a pixel test to make sure everything looks like we expect
@@ -1322,6 +1358,7 @@ ProtocolHeader = X-Forwarded-Proto
@testlib.skipImage("nginx not installed", "centos-*", "rhel-*", "debian-*", "ubuntu-*", "arch")
@testlib.skipOstree("nginx not installed")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testNginxTLS(self):
"""test proxying to Cockpit with TLS
@@ -1402,6 +1439,7 @@ server {
@testlib.skipImage("nginx not installed", "centos-*", "rhel-*", "debian-*", "ubuntu-*", "arch")
@testlib.skipOstree("nginx not installed")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testNginxNoTLS(self):
"""test proxying to Cockpit with plain HTTP
diff --git a/test/verify/check-kdump b/test/verify/check-kdump
index b35cc0d47364..eec4c88e37b3 100755
--- a/test/verify/check-kdump
+++ b/test/verify/check-kdump
@@ -40,6 +40,11 @@ class KdumpHelpers(testlib.MachineCase):
browser.wait_visible(".pf-v5-c-switch__input" + (":checked" if active else ":not(:checked)"))
def enableKdump(self):
+ if "suse" in self.machine.image:
+ self.machine.execute("sed -i -r 's/GRUB_CMDLINE_LINUX=\"(.*)\"/GRUB_CMDLINE_LINUX=\"\\1 crashkernel=192M,high crashkernel=0,low\"/g' /etc/default/grub")
+ self.machine.execute("update-bootloader; systemctl enable kdump")
+ self.machine.reboot()
+
if self.machine.image.startswith("fedora"):
self.machine.execute("systemctl enable kdump; kdumpctl reset-crashkernel")
self.machine.reboot()
@@ -104,7 +109,7 @@ class TestKdump(KdumpHelpers):
b.wait_visible("#app")
- if m.image.startswith("fedora"):
+ if m.image.startswith("fedora") or "suse" in m.image:
# crashkernel command line not set, needs to be explicitly enabled in Fedora
b.wait_in_text(".pf-v5-c-alert__title", "Kernel did not boot with the crashkernel setting")
# no service on/off button
@@ -154,7 +159,10 @@ class TestKdump(KdumpHelpers):
b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
with b.wait_timeout(120): # needs to rebuild initrd
b.wait_not_present(pathInput)
- m.execute("cat /etc/kdump.conf | grep -qE 'makedumpfile.*-c.*'")
+ if "suse" in m.image:
+ m.execute("cat /etc/sysconfig/kdump | grep -qE 'KDUMP_SAVEDIR'")
+ else:
+ m.execute("cat /etc/kdump.conf | grep -qE 'makedumpfile.*-c.*'")
# generate a valid kdump config with ssh target
b.click("#kdump-change-target")
@@ -202,13 +210,19 @@ class TestKdump(KdumpHelpers):
# crash the kernel and make sure it wrote a report into the right directory
self.crashKernel(f"stored in {customPath} as vmcore")
- m.execute(f"until test -e {customPath}/127.0.0.1*/vmcore; do sleep 1; done", timeout=180)
- self.assertIn("Kdump compressed dump", m.execute(f"file {customPath}/127.0.0.1*/vmcore"))
+ m.execute(f"until test -e {customPath}/*/vmcore; do sleep 1; done", timeout=180)
+ # HACK: Tumbleweed's file doesn't correctly recognise kdump's, they are only shown
+ # as data, lets skip this and just check the file exists
+ if "suse" not in m.image:
+ self.assertIn("Kdump compressed dump", m.execute(f"file {customPath}/*/vmcore"))
self.login_and_go("/kdump")
b.wait_visible("#app")
- m.execute("cp /etc/kdump.conf /etc/kdump.conf.orig")
+ if "suse" in m.image:
+ m.execute("cp /etc/sysconfig/kdump /etc/sysconfig/kdump.orig")
+ else:
+ m.execute("cp /etc/kdump.conf /etc/kdump.conf.orig")
b.click("#kdump-automation-script")
b.click("button:contains('Shell script')")
shell_script_sel = ".automation-script-modal .pf-v5-c-modal-box__body section:nth-child(3) textarea"
@@ -217,7 +231,10 @@ class TestKdump(KdumpHelpers):
m.execute(b.text(shell_script_sel))
b.click(".pf-v5-c-modal-box__footer button:contains('Close')")
b.wait_not_present(".automation-script-modal")
- m.execute("diff /etc/kdump.conf /etc/kdump.conf.orig", stdout=None)
+ if "suse" in m.image:
+ m.execute("diff /etc/sysconfig/kdump /etc/sysconfig/kdump.orig", stdout=None)
+ else:
+ m.execute("diff /etc/kdump.conf /etc/kdump.conf.orig", stdout=None)
# service errors
@@ -246,6 +263,7 @@ class TestKdump(KdumpHelpers):
b.wait_js_cond('window.location.hash === "#/kdump.service"')
@testlib.nondestructive
+ @testlib.skipImage("SUSE uses a different kdump conf format, so we'll use it's own test", "*suse*")
def testConfiguration(self):
b = self.browser
m = self.machine
@@ -407,7 +425,7 @@ class TestKdump(KdumpHelpers):
b.wait_text("#kdump-target-info", "Remote over NFS, someserver:/srv/var/crash")
conf = m.execute("cat /etc/sysconfig/kdump")
self.assertIn('KDUMP_SAVEDIR=nfs://someserver/srv', conf)
- self.assertNotIn("ssh://", conf)
+ self.assertNotIn("=ssh://", conf)
# NFS with custom path
b.click("#kdump-change-target")
@@ -429,7 +447,7 @@ class TestKdump(KdumpHelpers):
b.wait_text("#kdump-target-info", "Local, /var/tmp")
conf = m.execute("cat /etc/sysconfig/kdump")
self.assertIn('KDUMP_SAVEDIR=file:///var/tmp', conf)
- self.assertNotIn("nfs://", conf)
+ self.assertNotIn("=nfs://", conf)
# Check compression
conf = m.execute("cat /etc/sysconfig/kdump")
@@ -472,6 +490,7 @@ class TestKdumpNFS(KdumpHelpers):
"nfs": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/24", "memory_mb": 512}
}
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
m = self.machine
b = self.browser
@@ -502,8 +521,12 @@ class TestKdumpNFS(KdumpHelpers):
b.wait_not_present("#kdump-settings-dialog")
# explicit nfs option, unset path
- conf = m.execute("cat /etc/kdump.conf")
- self.assertIn("\nnfs 10.111.113.2:/srv/kdump\n", conf)
+ if "suse" in m.image:
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn("nfs://10.111.113.2/srv/kdump", conf)
+ else:
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("\nnfs 10.111.113.2:/srv/kdump\n", conf)
self.assertNotIn("\npath", conf)
try:
diff --git a/test/verify/check-loopback b/test/verify/check-loopback
index ee5e343c3af8..ab2c1522f917 100755
--- a/test/verify/check-loopback
+++ b/test/verify/check-loopback
@@ -40,7 +40,10 @@ class TestLoopback(testlib.MachineCase):
m.disconnect()
self.restore_dir("/etc/ssh", restart_unit=self.sshd_service)
- m.execute("sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)")
+ sshd_path = testlib.get_sshd_config_path(m.image)
+ m.execute(f"sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' {sshd_path} $(ls {sshd_path}.d/* 2>/dev/null || true)")
+ if "suse" in m.image:
+ m.execute(f"echo 'ChallengeResponseAuthentication no' >> {sshd_path}")
m.execute(self.restart_sshd)
m.wait_execute()
diff --git a/test/verify/check-metrics b/test/verify/check-metrics
index c3b627f679b1..eaf88168a2ba 100755
--- a/test/verify/check-metrics
+++ b/test/verify/check-metrics
@@ -68,7 +68,7 @@ def prepareArchive(machine, name, time, hostname="localhost.localdomain"):
def redisService(image):
if image.startswith(("debian", "ubuntu")):
return "redis-server"
- if image.startswith(("fedora", "centos-10", "rhel-10")):
+ if image.startswith(("fedora", "centos-10", "rhel-10")) or "suse" in image:
return "valkey"
return "redis"
@@ -179,6 +179,7 @@ class TestHistoryMetrics(testlib.MachineCase):
b.enter_page("/system")
b.wait_visible('.system-information')
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testEvents(self):
b = self.browser
m = self.machine
@@ -574,9 +575,10 @@ class TestHistoryMetrics(testlib.MachineCase):
m.execute("firewall-cmd --reload")
redis = customRedisService or redisService(m.image)
+ redis_service = f"{redis}.target" if "suse" in m.image else f"{redis}.service"
hostname = m.execute("hostname").strip()
- self.addCleanup(m.execute, f"systemctl disable --now {redis}")
+ self.addCleanup(m.execute, f"systemctl disable --now {redis_service}")
def checkEnable(firewalld_alert):
b.click("#metrics-header-section button.pf-m-secondary")
@@ -591,7 +593,7 @@ class TestHistoryMetrics(testlib.MachineCase):
b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
m.execute('while [ $(systemctl is-active pmproxy) = activating ]; do sleep 1; done')
self.assertEqual(m.execute("systemctl is-active pmproxy").strip(), "active")
- self.assertEqual(m.execute(f"systemctl is-active {redis}").strip(), "active")
+ self.assertEqual(m.execute(f"systemctl is-active {redis_service}").strip(), "active")
self.assertEqual(m.execute("systemctl is-enabled pmproxy").strip(), "enabled")
self.assertIn(redis, m.execute("systemctl show -p Wants --value pmproxy").strip())
testlib.wait(lambda: hostname in m.execute("curl --max-time 10 --silent --show-error 'http://localhost:44322/series/labels?names=hostname'"), delay=10, tries=30)
@@ -608,13 +610,13 @@ class TestHistoryMetrics(testlib.MachineCase):
self.assertEqual(m.execute("! systemctl is-active pmproxy").strip(), "inactive")
self.assertEqual(m.execute("! systemctl is-enabled pmproxy").strip(), "disabled")
# keeps redis running, it's a shared service
- self.assertEqual(m.execute(f"systemctl is-active {redis}").strip(), "active")
+ self.assertEqual(m.execute(f"systemctl is-active {redis_service}").strip(), "active")
# but drops the pmproxy dependency
self.assertNotIn(redis, m.execute("systemctl show -p Wants --value pmproxy").strip())
m.execute("! curl --silent --show-error --max-time 10 'http://localhost:44322/series/labels?names=hostname' 2>&1")
# start in a defined state; all test images have pcp and redis pre-installed
- m.execute(f"systemctl disable --now pmlogger pmie pmproxy {redis}")
+ m.execute(f"systemctl disable --now pmlogger pmie pmproxy {redis_service}")
m.execute("systemctl reset-failed")
# ensure pmproxy is not already opened in firewall
m.execute("firewall-cmd --remove-service pmproxy; firewall-cmd --permanent --remove-service pmproxy")
@@ -638,12 +640,12 @@ class TestHistoryMetrics(testlib.MachineCase):
checkDisable()
# redis already running
- m.execute(f"systemctl start {redis}")
+ m.execute(f"systemctl start {redis_service}")
checkEnable(firewalld_alert=True)
checkDisable()
# pmproxy already running; 44322 queries hang without redis and until restart
- m.execute(f"systemctl disable --now {redis}; systemctl start pmproxy")
+ m.execute(f"systemctl disable --now {redis_service}; systemctl start pmproxy")
checkEnable(firewalld_alert=True)
# without firewalld
@@ -722,9 +724,9 @@ class TestHistoryMetrics(testlib.MachineCase):
else:
raise testlib.Error("PCP settings dialog did not get expected value")
- m.execute(f"systemctl stop {redis}")
+ m.execute(f"systemctl stop {redis_service}")
checkEnabled(expected=False)
- m.execute(f"systemctl start {redis}")
+ m.execute(f"systemctl start {redis_service}")
checkEnabled(expected=True)
m.execute("systemctl stop pmproxy")
checkEnabled(expected=False)
@@ -1273,6 +1275,7 @@ BEGIN {{
class TestMetricsPackages(packagelib.PackageCase):
pcp_dialog_selector = "#pcp-settings-modal"
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
b = self.browser
m = self.machine
@@ -1284,6 +1287,7 @@ class TestMetricsPackages(packagelib.PackageCase):
redis_service = redisService(m.image)
redis_package = "valkey" if redis_service == "valkey" else "redis"
+ redis_service = f"{redis_service}.target" if "suse" in m.image else f"{redis_service}.service"
extra_packages = ""
if redis_package == "valkey":
if m.image == "fedora-40":
@@ -1320,6 +1324,8 @@ class TestMetricsPackages(packagelib.PackageCase):
""")
# HACK: pcp does not clean up correctly on Debian https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=986074
m.execute("rm -f /etc/systemd/system/pmlogger.service.requires/pmlogger_farm.service")
+ elif "suse" in m.image:
+ m.execute(f"zypper rm -y pcp python3-pcp {redis_package} {extra_packages}")
else:
m.execute(f"rpm --erase --verbose pcp python3-pcp {redis_package} {extra_packages}")
if "centos-8" in m.image or "rhel-8" in m.image:
@@ -1338,7 +1344,7 @@ class TestMetricsPackages(packagelib.PackageCase):
"/lib/systemd/system/pmproxy.service": dummy_service,
}
redis_content = {
- f"/lib/systemd/system/{redis_service}.service": dummy_service,
+ f"/lib/systemd/system/{redis_service}": dummy_service,
}
self.createPackage("python3-pcp", "999", "1", content=cpcp_content, depends="pcp")
diff --git a/test/verify/check-networkmanager-firewall b/test/verify/check-networkmanager-firewall
index 7f549fd23edb..5bb87082aa53 100755
--- a/test/verify/check-networkmanager-firewall
+++ b/test/verify/check-networkmanager-firewall
@@ -390,10 +390,13 @@ class TestFirewall(netlib.NetworkCase):
set_field("#tcp-ports", "", "")
set_field("#tcp-ports", "80,7", "custom--http-echo")
set_field("#udp-ports", "123", "custom--http-echo-ntp")
- set_field("#udp-ports", "123, 50000", "custom--http-echo-ntp-50000")
- set_field("#tcp-ports", "", "custom--ntp-50000")
- set_field("#tcp-ports", "https", "custom--https-ntp-50000")
- save("custom--https-ntp-50000", "443", "123, 50000")
+
+ # SUSE has port 50000 listed as mrt
+ mrt_port = "mrt" if "suse" in m.image else "50000"
+ set_field("#udp-ports", "123, 50000", f"custom--http-echo-ntp-{mrt_port}")
+ set_field("#tcp-ports", "", f"custom--ntp-{mrt_port}")
+ set_field("#tcp-ports", "https", f"custom--https-ntp-{mrt_port}")
+ save(f"custom--https-ntp-{mrt_port}", "443", "123, 50000")
open_dialog()
set_field("#tcp-ports", "80-82", "custom--80-82")
diff --git a/test/verify/check-networkmanager-mac b/test/verify/check-networkmanager-mac
index 1fde246d1b8a..675d11d8e7ba 100755
--- a/test/verify/check-networkmanager-mac
+++ b/test/verify/check-networkmanager-mac
@@ -28,6 +28,7 @@ class TestNetworkingMAC(netlib.NetworkCase):
"machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 512}
}
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testMac(self):
b = self.browser
m = self.machine
@@ -58,6 +59,7 @@ class TestNetworkingMAC(netlib.NetworkCase):
else:
b.wait_not_present("#network-interface-mac button")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBondMac(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-networkmanager-mtu b/test/verify/check-networkmanager-mtu
index 1664c404c596..6663d14093aa 100755
--- a/test/verify/check-networkmanager-mtu
+++ b/test/verify/check-networkmanager-mtu
@@ -28,6 +28,7 @@ class TestNetworkingMTU(netlib.NetworkCase):
"machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 512}
}
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testMtu(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-networkmanager-settings b/test/verify/check-networkmanager-settings
index 60cc64edb0a4..a0c2d13d8af8 100755
--- a/test/verify/check-networkmanager-settings
+++ b/test/verify/check-networkmanager-settings
@@ -28,6 +28,7 @@ class TestNetworkingSettings(netlib.NetworkCase):
"machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 512}
}
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testNoConnectionSettings(self):
b = self.browser
m = self.machine
@@ -151,6 +152,7 @@ class TestNetworkingSettings(netlib.NetworkCase):
for method in unsupported_ip6_methods:
b.wait_not_present(f"#network-ip-settings-select-method option[value='{method}']")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testOtherSettings(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-networkmanager-team b/test/verify/check-networkmanager-team
index 8f38c56e880b..5a7620f98f4c 100755
--- a/test/verify/check-networkmanager-team
+++ b/test/verify/check-networkmanager-team
@@ -28,6 +28,7 @@ import testlib
@testlib.skipImage("team not supported", "centos-10*", "rhel-10*")
@testlib.nondestructive
class TestTeam(netlib.NetworkCase):
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
b = self.browser
@@ -90,6 +91,7 @@ class TestTeam(netlib.NetworkCase):
self.wait_for_iface(iface1)
self.wait_for_iface(iface2)
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testActive(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-packagekit b/test/verify/check-packagekit
index 4e49ee149510..54ef2f08f71c 100755
--- a/test/verify/check-packagekit
+++ b/test/verify/check-packagekit
@@ -26,7 +26,7 @@ import packagelib
import testlib
OSesWithoutTracer = ["debian-*", "ubuntu-*", "fedora-coreos", "centos-10", "rhel-10*"]
-OSesWithoutKpatch = ["debian-*", "ubuntu-*", "arch", "fedora-*", "centos-*"]
+OSesWithoutKpatch = ["debian-*", "ubuntu-*", "arch", "fedora-*", "centos-*", "*-tumbleweed"]
class NoSubManCase(packagelib.PackageCase):
@@ -264,6 +264,7 @@ echo -e "Loaded patch modules:\nkpatch_3_10_0_1062_1_1 [enabled]\n\nInstalled pa
# Patches should be installed
m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testBasic(self):
# no security updates, no changelogs
@@ -418,6 +419,7 @@ echo -e "Loaded patch modules:\nkpatch_3_10_0_1062_1_1 [enabled]\n\nInstalled pa
b.wait_text(self.update_text, "System is up to date")
@testlib.skipImage("TODO: Packagekit on Arch does not detect the pear update", "arch")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.skipImage("tracer not available", *OSesWithoutTracer)
def testTracer(self):
b = self.browser
@@ -597,6 +599,7 @@ ExecStart=/usr/local/bin/{self.packageName}
@testlib.skipImage("Arch Linux does not start services by default", "arch")
@testlib.skipImage("tracer not available", *OSesWithoutTracer)
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testFailServiceRestart(self):
b = self.browser
@@ -657,6 +660,7 @@ ExecStart=/usr/local/bin/{packageName}
b.wait_visible("#restart-services-modal .pf-v5-c-alert")
@testlib.skipImage("No security changelog support in packagekit", "arch")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testInfoSecurity(self):
b = self.browser
m = self.machine
@@ -825,6 +829,7 @@ ExecStart=/usr/local/bin/{packageName}
b.wait_visible("table.updates-history tbody:not(.pf-m-expanded)")
@testlib.skipImage("No security changelog support in packagekit", "arch")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testSecurityOnly(self):
b = self.browser
@@ -865,6 +870,7 @@ ExecStart=/usr/local/bin/{packageName}
self.check_nth_update(2, "secnocve", "1-2", "security", desc_matches=["Fix leak"])
@testlib.skipImage("No changelog support in Arch Linux", "arch")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testInfoTruncation(self):
b = self.browser
@@ -961,6 +967,7 @@ ExecStart=/usr/local/bin/{packageName}
m.wait_reboot()
self.allow_restart_journal_messages()
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testUpdateError(self):
b = self.browser
@@ -996,6 +1003,7 @@ ExecStart=/usr/local/bin/{packageName}
# not expecting any buttons
self.assertFalse(b.is_present("#app button"))
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testPackageKitCrash(self):
b = self.browser
@@ -1102,6 +1110,7 @@ ExecStart=/usr/local/bin/{packageName}
@testlib.skipImage("TODO: Arch Linux has no cockpit-ws package, it's in cockpit", "arch")
@testlib.skipWsContainer("no cockpit-ws package with cockpit/ws container")
class TestWsUpdate(NoSubManCase):
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
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
@@ -1680,7 +1689,7 @@ class TestAutoUpdatesInstall(NoSubManCase):
b.wait_not_present("#settings")
b.wait_not_present("#autoupdates-settings")
- @testlib.skipImage("No supported auto update backend", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipImage("No supported auto update backend", "debian-*", "ubuntu-*", "arch", "opensuse-tumbleweed")
def testInstall(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-pages b/test/verify/check-pages
index 9f57345fa63a..67f7b1ddb638 100755
--- a/test/verify/check-pages
+++ b/test/verify/check-pages
@@ -46,6 +46,8 @@ class TestPages(testlib.MachineCase):
expected = "Fedora Linux documentation" + expected
elif m.image.startswith("rhel"):
expected = "Red Hat Enterprise Linux documentation" + expected
+ elif "suse" in m.image:
+ expected = "openSUSE Tumbleweed documentation" + expected
elif m.image == "arch":
expected = "Arch Linux documentation" + expected
@@ -785,6 +787,7 @@ OnCalendar=daily
b.wait_text("#received-type", "-")
b.wait_text("#received-title", "-")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testHistory(self):
b = self.browser
diff --git a/test/verify/check-reauthorize b/test/verify/check-reauthorize
index 0d35f6d1e91e..b96f315d8da6 100755
--- a/test/verify/check-reauthorize
+++ b/test/verify/check-reauthorize
@@ -89,7 +89,7 @@ class TestReauthorize(testlib.MachineCase):
m.execute("echo user:foobar | chpasswd")
b.default_user = "user"
- self.login_and_go("/playground/test")
+ self.login_and_go("/playground/test", superuser=False)
b.click(".super-channel button")
b.wait_in_text(".super-channel span", 'result: ')
diff --git a/test/verify/check-selinux b/test/verify/check-selinux
index 45736af00141..a7966bc7c008 100755
--- a/test/verify/check-selinux
+++ b/test/verify/check-selinux
@@ -54,7 +54,7 @@ class TestSelinux(testlib.MachineCase):
"ansible_machine": {"image": TEST_OS_DEFAULT, "memory_mb": 650}
}
- @testlib.skipImage("No setroubleshoot", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipImage("No setroubleshoot", "debian-*", "ubuntu-*", "arch", "*suse*")
@testlib.skipOstree("No setroubleshoot")
@testlib.skipBrowser("Firefox fails to recognize 'clipboard-read' permission", "firefox")
def testTroubleshootAlerts(self):
@@ -292,7 +292,7 @@ class TestSelinux(testlib.MachineCase):
b.wait_text("ul[aria-label='System modifications']", "The logged in user is not permitted to view system modifications")
-@testlib.skipImage("No cockpit-selinux", "debian-*", "ubuntu-*", "arch")
+@testlib.skipImage("No cockpit-selinux", "debian-*", "ubuntu-*", "arch", "*suse*")
@testlib.skipOstree("No cockpit-selinux")
@testlib.nondestructive
class TestSelinuxEnforcing(testlib.MachineCase):
diff --git a/test/verify/check-session b/test/verify/check-session
index bf68cc6b7b03..d39227939f98 100755
--- a/test/verify/check-session
+++ b/test/verify/check-session
@@ -23,6 +23,7 @@ import testlib
@testlib.nondestructive
class TestSession(testlib.MachineCase):
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
m = self.machine
b = self.browser
@@ -94,7 +95,9 @@ class TestSession(testlib.MachineCase):
# this is enabled by default in tools/cockpit.debian.pam, as well as
# Debian/Ubuntu's /etc/pam.d/sshd; but not in Fedora/RHEL
- if "debian" not in m.image and "ubuntu" not in m.image:
+ if "suse" in m.image:
+ self.write_file("/usr/lib/pam.d/cockpit", "session required pam_env.so user_readenv=1\n", append=True)
+ elif "debian" not in m.image and "ubuntu" not in m.image:
self.write_file("/etc/pam.d/cockpit", "session required pam_env.so user_readenv=1\n", append=True)
victim_pid = m.spawn("sleep infinity", "sleep.log")
self.addCleanup(m.execute, f"kill {victim_pid} || true")
diff --git a/test/verify/check-shell-active-pages b/test/verify/check-shell-active-pages
index ffa0d96f9757..c73fdbb1dca3 100755
--- a/test/verify/check-shell-active-pages
+++ b/test/verify/check-shell-active-pages
@@ -65,7 +65,10 @@ class TestActivePages(testlib.MachineCase):
# wait for prompt in first line
b.wait_visible(".terminal .xterm-accessibility-tree")
- b.wait_in_text(line_sel(1), '$')
+ shell_indicator = '$'
+ if "suse" in m.image:
+ shell_indicator = '>'
+ b.wait_in_text(line_sel(1), shell_indicator)
# run a command that we can easily identify, and which will die with the terminal
b.input_text("bash -c 'exec -a kitten cat'\n")
diff --git a/test/verify/check-shell-host-switching b/test/verify/check-shell-host-switching
index 94e029277457..a825efb7a1e7 100755
--- a/test/verify/check-shell-host-switching
+++ b/test/verify/check-shell-host-switching
@@ -138,6 +138,7 @@ class TestHostSwitching(testlib.MachineCase, HostSwitcherHelpers):
self.allow_restart_journal_messages()
self.allow_hostkey_messages()
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
b = self.browser
m1 = self.machines["machine1"]
diff --git a/test/verify/check-shell-keys b/test/verify/check-shell-keys
index f9d1cd726ab6..8a9a85849117 100755
--- a/test/verify/check-shell-keys
+++ b/test/verify/check-shell-keys
@@ -25,6 +25,7 @@ KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEAkRTvQCSEZNPXpA5bP82ilQn3TMeQ6z2NO3
@testlib.nondestructive
class TestKeys(testlib.MachineCase):
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testAuthorizedKeys(self):
m = self.machine
b = self.browser
@@ -148,6 +149,7 @@ class TestKeys(testlib.MachineCase):
# Possible workaround - ssh as `admin` and just do `m.execute()`
@testlib.skipBrowser("Firefox cannot do `cockpit.spawn`", "firefox")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testPrivateKeys(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-shell-multi-machine b/test/verify/check-shell-multi-machine
index d0c4650cc21b..da6743463d46 100755
--- a/test/verify/check-shell-multi-machine
+++ b/test/verify/check-shell-multi-machine
@@ -92,8 +92,9 @@ def change_ssh_port(machine, address, sshd_socket, sshd_service, port=None, time
machine.execute("systemctl daemon-reload")
machine.execute("systemctl restart ssh.socket")
else:
- machine.execute("sed -i 's/.*Port .*/#\\0/' /etc/ssh/sshd_config")
- machine.write("/etc/ssh/sshd_config",
+ sshd_path = testlib.get_sshd_config_path(machine.image)
+ machine.execute(f"sed -i 's/.*Port .*/#\\0/' {sshd_path}")
+ machine.write(sshd_path,
f"ListenAddress 127.27.0.15:22\nListenAddress {address}:{port}\n",
append=True)
# We stop the sshd.socket unit and just go with a regular
@@ -434,6 +435,7 @@ class TestMultiMachine(testlib.MachineCase):
self.assertEqual(200, http_code("/cockpit/@localhost/manifests.json"))
self.assertEqual(403, http_code("/cockpit/@10.111.113.2/manifests.json"))
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testUrlRoot(self):
b = self.browser
m = self.machine
@@ -653,13 +655,15 @@ class TestMultiMachine(testlib.MachineCase):
self.allow_hostkey_messages()
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testTroubleshooting(self):
b = self.browser
m1 = self.machine
m2 = self.machine2
# Logging in as root is no longer allowed by default by sshd
- m2.execute("sed -ri 's/#?PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config")
+ sshd_path = testlib.get_sshd_config_path(m1.image)
+ m2.execute(f"sed -ri 's/#?PermitRootLogin prohibit-password/PermitRootLogin yes/' {sshd_path}")
m2.execute(self.restart_sshd)
machine_path = "/@10.111.113.2"
@@ -798,6 +802,7 @@ class TestMultiMachine(testlib.MachineCase):
'.*: server offered unsupported authentication methods: .*')
@testlib.skipImage("TODO: Broken on Arch Linux", "arch")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testSshKeySetup(self):
b = self.browser
m1 = self.machine
diff --git a/test/verify/check-shell-multi-machine-key b/test/verify/check-shell-multi-machine-key
index 3acb385c0669..bad05100521a 100755
--- a/test/verify/check-shell-multi-machine-key
+++ b/test/verify/check-shell-multi-machine-key
@@ -101,8 +101,13 @@ class TestMultiMachineKeyAuth(testlib.MachineCase):
# Add user
self.machine2.disconnect()
self.machine2.execute("useradd user -c User", direct=True)
+ sshd_path = testlib.get_sshd_config_path(self.machine.image)
self.machine2.execute(
- "sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)", direct=True)
+ f"sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' {sshd_path} $(ls {sshd_path}.d/* 2>/dev/null || true)", direct=True)
+ # HACK - Increase MaxAuthTries because of
+ # https://bugs.libssh.org/T233 and because we have a lot of
+ # keys and cockpit-ssh tries them all, and many of them twice.
+ self.machine2.write(sshd_path, "MaxAuthTries 100", append=True)
self.machine2.execute(self.restart_sshd, direct=True)
self.machine2.wait_execute()
@@ -117,6 +122,7 @@ class TestMultiMachineKeyAuth(testlib.MachineCase):
# Possible workaround - ssh as `admin` and just do `m.execute()`
@testlib.skipBrowser("Firefox cannot do `cockpit.spawn`", "firefox")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
b = self.browser
m1 = self.machine
diff --git a/test/verify/check-sosreport b/test/verify/check-sosreport
index 0d44bdc8dd2b..457ab6cda876 100755
--- a/test/verify/check-sosreport
+++ b/test/verify/check-sosreport
@@ -25,7 +25,7 @@ import testlib
@testlib.skipOstree("No sosreport")
-@testlib.skipImage("No sosreport", "arch")
+@testlib.skipImage("No sosreport", "arch", "*suse*")
@testlib.nondestructive
class TestSOS(testlib.MachineCase):
diff --git a/test/verify/check-ssh-api b/test/verify/check-ssh-api
index 5af660900c8b..881efaafb578 100755
--- a/test/verify/check-ssh-api
+++ b/test/verify/check-ssh-api
@@ -42,6 +42,7 @@ class TestSshDialog(testlib.MachineCase):
self.machine.execute("pkill -ef [s]sh.*ferny.*127.0.0.1")
@testlib.skipImage("FIXME: Assertion failed: UnknownHostDialog needs a host-key and host-fingerprint in error", "rhel-8-10")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testPassword(self):
m = self.machine
b = self.browser
diff --git a/test/verify/check-static-login b/test/verify/check-static-login
index 6717e6cd7154..70e30f51e203 100755
--- a/test/verify/check-static-login
+++ b/test/verify/check-static-login
@@ -39,6 +39,7 @@ class TestLogin(testlib.MachineCase):
b.set_layout("desktop")
# @testlib.skipBeiboot("no local cockpit PAM config in beiboot mode")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
m = self.machine
b = self.browser
@@ -59,7 +60,11 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
if not m.ws_container:
deny_non_root("/etc/pam.d/cockpit")
- deny_non_root("/etc/pam.d/sshd")
+
+ if "suse" in m.image:
+ deny_non_root("/usr/lib/pam.d/sshd")
+ else:
+ deny_non_root("/etc/pam.d/sshd")
m.start_cockpit()
b.open("/system")
@@ -213,6 +218,7 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
"noise-rc-.*")
@testlib.skipWsContainer("logs in via ssh, not cockpit-session")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testLogging(self):
m = self.machine
b = self.browser
@@ -276,13 +282,16 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
# with the ws container this happens over ssh
if m.ws_container:
self.restore_dir("/etc/ssh", restart_unit=self.sshd_service)
+ sshd_path = testlib.get_sshd_config_path(m.image)
m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' "
- "/etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)")
+ f"{sshd_path} $(ls {sshd_path}.d/* 2>/dev/null || true)")
m.execute(self.restart_sshd)
# test steps below assume a pam_pwquality config with retry > 1; on some images authselect drops that setting
- if not m.image.startswith('debian') and not m.image.startswith('ubuntu') and not m.image.startswith("arch"):
+ if not m.image.startswith('debian') and not m.image.startswith('ubuntu') and not m.image.startswith("arch") and "suse" not in m.image:
self.sed_file("/password.*requisite.*pam_pwquality/ s/$/ retry=3/", "/etc/pam.d/password-auth")
+ elif "suse" in m.image:
+ self.sed_file("/password.*requisite.*pam_pwquality/ s/$/ retry=3/", "/etc/pam.d/common-password-pc")
m.execute("chage -d 0 admin")
m.start_cockpit()
@@ -377,10 +386,14 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
if m.ostree_image:
conf = "/etc/pam.d/sshd"
self.restore_dir("/etc/ssh", restart_unit=self.sshd_service)
+ sshd_path = testlib.get_sshd_config_path(m.image)
m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' "
- "/etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)")
+ f"{sshd_path} $(ls {sshd_path}.d/* 2>/dev/null || true)")
m.execute(self.restart_sshd)
+ if "suse" in m.image:
+ conf = "/usr/lib/pam.d/cockpit"
+
# On Arch Linux the ordering matters due to an auth include for system-remote-login
if self.machine.image == "arch":
self.sed_file('1 a auth required mock-pam-conv-mod.so', conf)
@@ -418,7 +431,7 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
self.allow_restart_journal_messages()
- @testlib.skipImage("No tlog", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipImage("No tlog", "debian-*", "ubuntu-*", "arch", "*suse*")
@testlib.skipOstree("No tlog")
@testlib.skipImage("FIXME: tlog messes up bridge frame protocol", "rhel-8-10")
def testSessionRecordingShell(self):
@@ -474,7 +487,7 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
"couldn't parse login input: Malformed input",
"couldn't parse login input: Authentication failed")
- @testlib.skipImage("No SELinux", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipImage("No SELinux", "debian-*", "ubuntu-*", "arch", "*suse*")
@testlib.skipOstree("No semanage")
def testSELinuxRestrictedUser(self):
m = self.machine
@@ -700,6 +713,16 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
"/etc/pam.d/common-auth")
elif m.image == "arch":
self.sed_file("s/# deny = 3/deny = 4/", "/etc/security/faillock.conf")
+ elif "suse" in m.image:
+ m.execute("""cat < /etc/security/faillock.conf
+deny = 4
+EOF""")
+ m.execute("""cat <> /etc/pam.d/common-auth
+auth [success=1 default=bad] pam_unix.so
+auth [default=die] pam_faillock.so authfail
+auth sufficient pam_faillock.so authsucc
+auth required pam_deny.so
+EOF""")
else:
# see https://access.redhat.com/solutions/62949
self.sed_file("""/pam_unix/ {
@@ -765,6 +788,7 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
self.allow_journal_messages(".*minutes left to unlock.*")
@testlib.skipWsContainer("root logins disabled by default with ssh")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testPamAccess(self):
b = self.browser
m = self.machine
@@ -995,6 +1019,7 @@ matchrule = ^DC=LAN,DC=COCKPIT,CN=alice$
# enable this once our cockpit/ws container can beiboot
@testlib.skipWsContainer("client setup does not work with ws container")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testSSH(self):
m = self.machine
b = self.browser
diff --git a/test/verify/check-storage-anaconda b/test/verify/check-storage-anaconda
index a188d704c53e..2f0c22a41a0b 100755
--- a/test/verify/check-storage-anaconda
+++ b/test/verify/check-storage-anaconda
@@ -166,7 +166,7 @@ class TestStorageAnaconda(storagelib.StorageCase):
b.wait(lambda: b.call_js_func('ph_count', self.card("Storage") + " tbody tr") == 1)
b.wait_not_present(self.card_row("Storage", location="/var"))
- @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+ @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "*suse*")
@testlib.skipImage("commit 817c957899a4 removed Statis 2 support", "rhel-8-*")
def testStratis(self):
m = self.machine
@@ -635,7 +635,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
self.dialog_wait_close()
b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
- @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+ @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "*suse*")
@testlib.skipImage("No Anaconda", "arch")
@testlib.destructive
def testNoOndemandPackages(self):
diff --git a/test/verify/check-storage-iscsi b/test/verify/check-storage-iscsi
index a81c162beb7a..18d7ae7825e6 100755
--- a/test/verify/check-storage-iscsi
+++ b/test/verify/check-storage-iscsi
@@ -21,7 +21,7 @@ import storagelib
import testlib
-@testlib.skipImage("UDisks doesn't have support for iSCSI", "debian-*", "ubuntu-*", "arch")
+@testlib.skipImage("UDisks doesn't have support for iSCSI", "debian-*", "ubuntu-*", "arch", "*suse*")
@testlib.nondestructive
class TestStorageISCSI(storagelib.StorageCase):
diff --git a/test/verify/check-storage-luks b/test/verify/check-storage-luks
index e24fce42bed4..2e35560aa5f9 100755
--- a/test/verify/check-storage-luks
+++ b/test/verify/check-storage-luks
@@ -536,6 +536,7 @@ class TestStorageNBDE(storagelib.StorageCase, packagelib.PackageCase):
"tang": {"address": "10.111.112.5/20"}
}
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testBasic(self):
m = self.machine
b = self.browser
@@ -720,6 +721,7 @@ class TestStorageNBDE(storagelib.StorageCase, packagelib.PackageCase):
b.wait_not_present(panel + 'ul li:contains("Slot 1")')
@testlib.skipImage("TODO: don't know how to encrypt the rootfs", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
# this doesn't work in ostree either, but we don't run storage tests there
@testlib.skipWsContainer("newly built root doesn't contain ws container")
@testlib.timeout(1200)
diff --git a/test/verify/check-storage-lvm2 b/test/verify/check-storage-lvm2
index 61886ef4a13c..b3dda789fd9a 100755
--- a/test/verify/check-storage-lvm2
+++ b/test/verify/check-storage-lvm2
@@ -786,6 +786,7 @@ class TestStorageLvm2(storagelib.StorageCase):
wait_partial(4) # striped mirror has lost more than half and is kaputt
wait_partial(6) # raid6 is finally toast as well
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testLvmOnLuks(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-storage-nfs b/test/verify/check-storage-nfs
index 3738931a2cc9..5cca2008496a 100755
--- a/test/verify/check-storage-nfs
+++ b/test/verify/check-storage-nfs
@@ -25,6 +25,7 @@ import testlib
@testlib.nondestructive
class TestStorageNfs(storagelib.StorageCase):
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testNfsClient(self):
m = self.machine
b = self.browser
diff --git a/test/verify/check-storage-stratis b/test/verify/check-storage-stratis
index 7fd77bc3ce84..9bec59275131 100755
--- a/test/verify/check-storage-stratis
+++ b/test/verify/check-storage-stratis
@@ -46,7 +46,7 @@ def create_pool_key(machine, keyname, passphrase):
machine.execute(f"echo -n '{passphrase}' | stratis key set --keyfile-path /dev/stdin {keyname}")
-@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "*suse*")
@testlib.skipImage("commit 817c957899a4 removed Statis 2 support", "rhel-8-*")
@testlib.nondestructive
class TestStorageStratis(storagelib.StorageCase):
@@ -537,7 +537,7 @@ systemctl restart stratisd
b.wait_text(self.card_desc("Stratis filesystem", "Virtual size limit"), "none")
-@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "*suse*")
@testlib.skipImage("commit 817c957899a4 removed Statis 2 support", "rhel-8-*")
class TestStorageStratisReboot(storagelib.StorageCase):
# LUKS uses memory hard PBKDF, 1 GiB is not enough; see https://bugzilla.redhat.com/show_bug.cgi?id=1881829
@@ -826,7 +826,7 @@ class TestStorageStratisReboot(storagelib.StorageCase):
b.wait_in_text(self.card_row("Storage", name="pool0"), "1.8 GB")
-@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "arch")
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "arch", "*suse*")
class TestStoragePackagesStratis(packagelib.PackageCase, storagelib.StorageCase):
def testStratisOndemandInstallation(self):
@@ -869,7 +869,7 @@ class TestStoragePackagesStratis(packagelib.PackageCase, storagelib.StorageCase)
b.wait_not_present(stratis_action)
-@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "*suse*")
@testlib.skipImage("Stratis too old", "rhel-8-*")
class TestStorageStratisNBDE(packagelib.PackageCase, storagelib.StorageCase):
provision = {
diff --git a/test/verify/check-superuser b/test/verify/check-superuser
index 1538f7cb1103..3c9d16c1e6f4 100755
--- a/test/verify/check-superuser
+++ b/test/verify/check-superuser
@@ -70,7 +70,9 @@ class TestSuperuser(testlib.MachineCase):
if "ubuntu" not in m.image and "debian" not in m.image:
b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')",
"We trust you have received the usual lecture")
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+
+ user = testlib.get_superuser(m.image)
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
b.focus(".pf-v5-c-modal-box button:contains('Cancel')")
b.assert_pixels(".pf-v5-c-modal-box:contains('Switch to administrative access')", "superuser-modal")
@@ -140,6 +142,14 @@ auth required pam_unix.so
auth required mock-pam-conv-mod.so
@include common-account
@include common-session-noninteractive
+""")
+ elif "suse" in m.image:
+ self.write_file("/etc/pam.d/sudo", """
+auth required pam_unix.so
+auth required mock-pam-conv-mod.so
+account include common-account
+password include common-password
+session include common-session
""")
else:
self.write_file("/etc/pam.d/sudo", """
@@ -151,7 +161,8 @@ session include system-auth
""")
b.open_superuser_dialog()
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ user = testlib.get_superuser(m.image)
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
# Let the dialog sit there for 45 seconds, to test that this doesn't trigger a D-Bus timeout.
time.sleep(45)
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
@@ -170,14 +181,15 @@ session include system-auth
b.check_superuser_indicator("Limited access")
b.open_superuser_dialog()
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ user = testlib.get_superuser(self.machine.image)
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "wrong")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Sorry, try again")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "wronger")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Sorry, try again")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "wrongest")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
@@ -197,6 +209,11 @@ session include system-auth
if "ubuntu" in m.image:
self.sed_file("/^%admin/d", "/etc/sudoers")
+ # The default in tumbleweed is to allow passworded sudo to root
+ # We need to remove this before we actually run this test
+ if "suse" in m.image:
+ self.sed_file("/^ALL/d", "/usr/etc/sudoers")
+
m.execute(f"gpasswd -d admin {m.get_admin_group()}")
self.login_and_go()
@@ -215,7 +232,8 @@ session include system-auth
# cancelling auth dialog stops sudo
b.open_superuser_dialog()
b.wait_in_text(".pf-v5-c-modal-box", "Switch to administrative access")
- b.wait_in_text(".pf-v5-c-modal-box", "Password for admin")
+ user = testlib.get_superuser(m.image)
+ b.wait_in_text(".pf-v5-c-modal-box", f"Password for {user}")
status = m.execute("loginctl --lines=0 user-status admin")
self.assertIn("sudo", status)
if not m.ws_container:
@@ -228,7 +246,7 @@ session include system-auth
# logging out cleans up pending sudo auth; user should either go to "State: closing" or disappear completely
b.open_superuser_dialog()
- b.wait_in_text(".pf-v5-c-modal-box", "Password for admin")
+ b.wait_in_text(".pf-v5-c-modal-box", f"Password for {user}")
if not m.ws_container:
self.assertIn("cockpit-askpass", m.execute("loginctl --lines=0 user-status admin"))
b.click(".pf-v5-c-modal-box button:contains('Cancel')")
@@ -331,6 +349,7 @@ session include system-auth
b.check_superuser_indicator("Limited access")
@testlib.skipBeiboot("no local overrides/PAM config in beiboot mode")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testMultipleBridgeConfig(self):
b = self.browser
@@ -391,7 +410,8 @@ session include system-auth
b.set_val(".pf-v5-c-modal-box:contains('Switch to administrative access') select", "Sudo")
b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access') select", "Sudo")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ user = testlib.get_superuser(self.machine.image)
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
@@ -403,7 +423,8 @@ session include system-auth
self.login_and_go("/system", superuser=False)
b.wait_visible(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
b.click(".pf-v5-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ user = testlib.get_superuser(self.machine.image)
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
diff --git a/test/verify/check-system-info b/test/verify/check-system-info
index 5ce5000a0abe..5955bec8775d 100755
--- a/test/verify/check-system-info
+++ b/test/verify/check-system-info
@@ -143,7 +143,8 @@ class TestSystemInfo(testlib.MachineCase):
b.wait_not_present("#system_information_ssh_keys")
# Change ssh config and restart
- self.sed_file(r"s,.*HostKey *,#,; $ a HostKey /etc/ssh/weirdname", "/etc/ssh/sshd_config",
+ sshd_path = testlib.get_sshd_config_path(m.image)
+ self.sed_file(r"s,.*HostKey *,#,; $ a HostKey /etc/ssh/weirdname", sshd_path,
self.restart_sshd)
ssh_reconnect(m)
@@ -220,7 +221,10 @@ class TestSystemInfo(testlib.MachineCase):
self.restore_file("/etc/motd")
# run this test with a tight umask to check preserving file permissions
- self.sed_file(r"/^UMASK/ s/0../077/", "/etc/login.defs")
+ if "suse" in m.image:
+ self.sed_file(r"/^UMASK/ s/0../077/", "/usr/etc/login.defs")
+ else:
+ self.sed_file(r"/^UMASK/ s/0../077/", "/etc/login.defs")
m.execute("rm -f /etc/motd")
self.login_and_go("/system")
@@ -556,6 +560,9 @@ echo dummy > /boot/vmlinuz-42.0.0; mkdir -p /lib/modules/42.0.0/
if type update-grub >/dev/null 2>&1; then
update-grub # Debian/Ubuntu
grep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfg
+elif type update-bootloader >/dev/null 2>&1; then
+ update-bootloader # suse
+ grep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfg
else
cp -a /boot/grub2/grubenv /boot/grub2/grubenv.prev
kernel-install add 42.0.0 /boot/vmlinuz-42.0.0 2>/dev/null
@@ -569,6 +576,8 @@ fi
rm /boot/vmlinuz-42.0.0
if type update-grub >/dev/null 2>&1; then
update-grub # Debian/Ubuntu
+elif type update-bootloader >/dev/null 2>&1; then
+ update-bootloader # suse
else
kernel-install remove 42.0.0 /boot/vmlinuz-42.0.0
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2078359 and https://bugzilla.redhat.com/show_bug.cgi?id=2078379
@@ -620,6 +629,8 @@ fi
m.execute('mv /etc/default/grub /etc/default/grub.bak || true')
m.write('/tmp/grubby', '#!/bin/sh\necho 0')
m.execute('[ ! -f /usr/sbin/grubby ] || mount --bind /tmp/grubby /usr/sbin/grubby')
+ m.write('/tmp/update-bootloader', '#!/bin/sh\necho 0')
+ m.execute('[ ! -f /usr/sbin/update-bootloader ] || mount --bind /tmp/update-bootloader /usr/sbin/update-bootloader')
m.execute('systemctl stop rpm-ostreed.service || true; systemctl mask rpm-ostreed.service')
self.login_and_go('/system/hwinfo')
b.click('#hwinfo button:contains(Mitigations)')
@@ -933,7 +944,8 @@ class TestSystemInfoTime(packagelib.PackageCase):
# Gain admin access
b.click(".pf-v5-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
- b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ user = testlib.get_superuser(m.image)
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", f"Password for {user}:")
b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
diff --git a/test/verify/check-system-realms b/test/verify/check-system-realms
index 721292468f45..4ae9fa5008e1 100755
--- a/test/verify/check-system-realms
+++ b/test/verify/check-system-realms
@@ -514,6 +514,7 @@ class TestRealms(testlib.MachineCase):
@testlib.no_retry_when_changed
@testlib.skipImage("freeipa not currently available", "debian-testing")
+@testlib.skipImage("ipa-client-install not available", "opensuse-tumbleweed")
class TestIPA(TestRealms, CommonTests):
def setUp(self):
super().setUp()
@@ -740,7 +741,8 @@ ipa-advise enable-admins-sudo | sh -ex
""")
# enable ssh GSSAPI authentication
- m.execute("sed -ri 's/#GSSAPIAuthentication.*/GSSAPIAuthentication yes/' /etc/ssh/sshd_config")
+ sshd_path = testlib.get_sshd_config_path(m.image)
+ m.execute(f"sed -ri 's/#GSSAPIAuthentication.*/GSSAPIAuthentication yes/' {sshd_path}")
m.execute(self.restart_sshd)
# avoid "unknown host" error in SSH
@@ -835,7 +837,7 @@ ipa-advise enable-admins-sudo | sh -ex
m.execute(f"! su -c '{ccache_env} klist' alice")
-@testlib.skipImage("adcli not on test images", "debian-*", "ubuntu-*")
+@testlib.skipImage("adcli not on test images", "debian-*", "ubuntu-*", "*suse*")
@testlib.no_retry_when_changed
class TestAD(TestRealms, CommonTests):
def setUp(self):
@@ -982,7 +984,11 @@ fi
ipa-getkeytab -p HTTP/x0.cockpit.lan -k %(keytab)s
# HACK: due to sudo's "last rule wins", our /etc/sudoers rule becomes trumped by sssd's, so swap the order
-sed -i '/^sudoers:/ s/files sss/sss files/' /etc/nsswitch.conf
+if [ -f /etc/nsswitch.conf ]; then
+ sed -i '/^sudoers:/ s/files sss/sss files/' /etc/nsswitch.conf
+else
+ sed -i '/^sudoers:/ s/files sss/sss files/' /usr/etc/nsswitch.conf
+fi
"""
# This is here because our test framework can't run ipa VM's twice
@@ -992,6 +998,7 @@ sed -i '/^sudoers:/ s/files sss/sss files/' /etc/nsswitch.conf
@testlib.skipWsContainer("No sssd PAM integration in beiboot mode")
@testlib.skipImage("No realmd available", "arch")
@testlib.skipImage("freeipa not currently available", "debian-*")
+@testlib.skipImage("ipa-client-install util is not available for SUSE", "*suse*")
@testlib.no_retry_when_changed
class TestKerberos(testlib.MachineCase):
provision = {
diff --git a/test/verify/check-system-services b/test/verify/check-system-services
index 07f81cdcea72..6e2d89830869 100755
--- a/test/verify/check-system-services
+++ b/test/verify/check-system-services
@@ -1220,6 +1220,7 @@ Where=/fail
b.wait_js_cond('window.location.pathname == "/system/services"')
@testlib.nondestructive
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testServicesThemeConsistency(self):
b = self.browser
diff --git a/test/verify/check-users b/test/verify/check-users
index d1cae819d916..a92b93d0d3b6 100755
--- a/test/verify/check-users
+++ b/test/verify/check-users
@@ -197,7 +197,12 @@ class TestAccounts(testlib.MachineCase):
b.wait_visible("#accounts-create")
# Create a user from the UI
- self.sed_file('s@^SHELL=.*$@SHELL=/bin/true@', '/etc/default/useradd')
+ if "suse" in m.image:
+ # suse doesn't have /etc/default/useradd
+ m.execute("echo 'SHELL=/bin/true' > /etc/default/useradd")
+ else:
+ self.sed_file('s@^SHELL=.*$@SHELL=/bin/true@', '/etc/default/useradd')
+
b.click('#accounts-create')
b.wait_visible('#accounts-create-dialog')
b.set_input_text('#accounts-create-user-name', "Berta")
@@ -263,6 +268,11 @@ class TestAccounts(testlib.MachineCase):
b.logout()
b.login_and_go("/users")
+
+ # in suse images, Administrative requires root to be unlocked
+ if "suse" in m.image:
+ m.execute("usermod root --unlock")
+
createUser(
browser=b,
machine=m,
@@ -405,6 +415,7 @@ class TestAccounts(testlib.MachineCase):
b.click("#accounts-list > thead > tr > th:nth-child(3) > button")
check_column_sort("[data-label='ID']")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testUserPasswords(self):
b = self.browser
m = self.machine
@@ -415,7 +426,11 @@ class TestAccounts(testlib.MachineCase):
self.login_and_go("/users")
# Create a locked user with weak password
- self.sed_file('s/^SHELL=.*$/SHELL=/', '/etc/default/useradd')
+ if "suse" in m.image:
+ # suse doesn't have /etc/default/useradd
+ m.execute("echo 'SHELL=' > /etc/default/useradd")
+ else:
+ self.sed_file('s/^SHELL=.*$/SHELL=/', '/etc/default/useradd')
self.allow_journal_messages(".*required to change your password immediately.*")
self.allow_journal_messages(".*user account or password has expired.*")
@@ -530,7 +545,13 @@ class TestAccounts(testlib.MachineCase):
# Login as jussi and change role admin for itself
b.logout()
- b.login_and_go("/users", user="jussi", password="bar")
+ if "suse" in m.image:
+ # on suse images, you cannot use login_and_go hackery to automatigically get admin rights
+ # when the user doesn't have the same password as root
+ b.login_and_go("/users", user="jussi", password="bar", superuser=False)
+ b.become_superuser()
+ else:
+ b.login_and_go("/users", user="jussi", password="bar")
# There is only one badge and it is for jussi
b.wait_text('#current-account-badge', 'Your account')
@@ -606,7 +627,13 @@ class TestAccounts(testlib.MachineCase):
m.execute('echo "damaged:x:1234:1234:Damaged" >> /etc/passwd')
# Logout and login with the new password
- b.relogin(path="/users", user="jussi", password=good_password_2)
+ if "suse" in m.image:
+ # on suse images, you cannot use login_and_go hackery to automatigically get admin rights
+ # when the user doesn't have the same password as root
+ b.relogin(path="/users", user="jussi", password=good_password_2, superuser=False)
+ b.become_superuser()
+ else:
+ b.relogin(path="/users", user="jussi", password=good_password_2)
b.go("/users")
b.enter_page("/users")
@@ -662,7 +689,8 @@ class TestAccounts(testlib.MachineCase):
# with container this happens over ssh
if m.ws_container:
self.restore_dir("/etc/ssh", restart_unit=self.sshd_service)
- m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null) || true")
+ sshd_path = testlib.get_sshd_config_path(m.image)
+ m.execute(f"sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' {sshd_path} $(ls {sshd_path}.d/* 2>/dev/null) || true")
m.execute(self.restart_sshd)
b.wait_visible("#login")
@@ -891,6 +919,7 @@ class TestAccounts(testlib.MachineCase):
b.go("#/robert")
b.wait_text("#account-shell", custom_shell)
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testUnprivileged(self):
m = self.machine
b = self.browser
@@ -975,6 +1004,7 @@ class TestAccounts(testlib.MachineCase):
return value.strip()
return None
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testExpire(self):
m = self.machine
b = self.browser
@@ -1051,6 +1081,7 @@ class TestAccounts(testlib.MachineCase):
b.wait_not_present("#password-expiration")
b.logout()
+
# HACK: https://github.com/cockpit-project/cockpit/issues/20262
self.login_and_go("/users#/scruffy", user="scruffy", superuser=False)
b.wait_text("#account-user-name", "scruffy")
@@ -1071,6 +1102,7 @@ class TestAccounts(testlib.MachineCase):
self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "password must be changed")
@testlib.skipWsContainer("User is not shown as logged in with cockpit/ws")
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
def testAccountLogs(self):
b = self.browser
m = self.machine
diff --git a/test/verify/check-users-roles b/test/verify/check-users-roles
index 53eb733e1988..fc71ddafb159 100755
--- a/test/verify/check-users-roles
+++ b/test/verify/check-users-roles
@@ -22,6 +22,7 @@ import testlib
class TestRoles(testlib.MachineCase):
+ @testlib.skipImage("TODO: support opensuse-tumbleweed", "*tumbleweed*")
@testlib.nondestructive
def testBasic(self):
m = self.machine
diff --git a/test/vm.install b/test/vm.install
index e954ecfa4c79..a9443e80d5b3 100644
--- a/test/vm.install
+++ b/test/vm.install
@@ -45,6 +45,25 @@ if [ "$ID" = "debian" ]; then
echo '* soft core unlimited' >> /etc/security/limits.conf
fi
+if [ "$ID" = "opensuse-tumbleweed" ]; then
+ # Default pam policy notes pam last login date due to lastlog.so
+ # This will add cockpit's pam config to the silent list so we
+ # Don't fail due to unexpected journal lines
+ sed -i 's/sddm/sddm,cockpit/' /etc/pam.d/postlogin-session
+
+ # Force allowing memory accounting and cgroup delegation in suse
+ # This is because by default suse images disable both of these
+ # so we can't run any tests that rely on checking memory usage
+ printf '[Manager]\nDefaultMemoryAccounting=yes\n' > /usr/lib/systemd/system.conf.d/80-memory-accounting.conf
+ mkdir -p /etc/systemd/system/user@.service.d
+ printf '[Service]\nDelegate=cpu io memory\n' > /etc/systemd/system/user@.service.d/80-cgroups-delegation.conf
+
+ # Zypper will take forever to work out these don't have net access
+ # Let's lo them
+ echo "127.0.1.1 download.opensuse.org" >> /etc/hosts
+ echo "127.0.1.1 codecs.opensuse.org" >> /etc/hosts
+fi
+
PLATFORM_ID="${PLATFORM_ID:-}"
if [ "${PLATFORM_ID#platform:el}" != "$PLATFORM_ID" ]; then
# allow /usr/local/bin/ in sudo shells, to use our installed tools like the Python bridge
diff --git a/tools/cockpit.spec b/tools/cockpit.spec
index 07ed05eae1e8..1f71e563c84c 100644
--- a/tools/cockpit.spec
+++ b/tools/cockpit.spec
@@ -155,7 +155,7 @@ BuildRequires: python3-pytest-timeout
%check
make -j$(nproc) check
-%if 0%{?rhel} == 0
+%if 0%{?rhel} == 0 && 0%{?suse_version} == 0
export NO_QUNIT=1
%pytest
%endif
@@ -163,8 +163,13 @@ export NO_QUNIT=1
%install
%make_install
make install-tests DESTDIR=%{buildroot}
+%if 0%{?suse_version} > 1500
+mkdir -p $RPM_BUILD_ROOT%{_pam_vendordir}
+install -p -m 644 tools/cockpit.suse.pam $RPM_BUILD_ROOT%{_pam_vendordir}/cockpit
+%else
mkdir -p $RPM_BUILD_ROOT%{_sysconfdir}/pam.d
install -p -m 644 tools/cockpit.pam $RPM_BUILD_ROOT%{_sysconfdir}/pam.d/cockpit
+%endif
rm -f %{buildroot}/%{_libdir}/cockpit/*.so
install -D -p -m 644 AUTHORS COPYING README.md %{buildroot}%{_docdir}/cockpit/
@@ -218,7 +223,22 @@ find %{buildroot}%{_datadir}/cockpit/static -type f >> static.list
sed -i "s|%{buildroot}||" *.list
-%if ! 0%{?suse_version}
+%if 0%{?suse_version}
+# remove brandings with stale symlinks. Means they don't match
+# the distro.
+pushd %{buildroot}/%{_datadir}/cockpit/branding
+ls --hide={default,kubernetes,opensuse,registry,sle-micro,suse} | xargs rm -rv
+popd
+# need this in SUSE as post build checks dislike stale symlinks
+install -m 644 -D /dev/null %{buildroot}/run/cockpit/motd
+test -e %{buildroot}/usr/share/cockpit/branding/opensuse/default-1920x1200.jpg || install -m 644 -D /dev/null %{buildroot}/usr/share/cockpit/branding/opensuse/default-1920x1200.jpg
+test -e %{buildroot}/usr/share/cockpit/branding/sle-micro/apple-touch-icon.png || install -m 644 -D /dev/null %{buildroot}/usr/share/cockpit/branding/sle-micro/apple-touch-icon.png
+test -e %{buildroot}/usr/share/cockpit/branding/sle-micro/default-1920x1200.png || install -m 644 -D /dev/null %{buildroot}/usr/share/cockpit/branding/sle-micro/default-1920x1200.png
+# remove files of not installable packages
+rm -r %{buildroot}%{_datadir}/cockpit/sosreport
+rm -f %{buildroot}/%{_prefix}/share/metainfo/org.cockpit_project.cockpit_sosreport.metainfo.xml
+rm -f %{buildroot}%{_datadir}/icons/hicolor/64x64/apps/cockpit-sosreport.png
+%else
%global _debugsource_packages 1
%global _debuginfo_subpackages 0
@@ -348,6 +368,12 @@ Recommends: system-logos
Suggests: sssd-dbus >= 2.6.2
# for cockpit-desktop
Suggests: python3
+%if 0%{?suse_version}
+Provides: group(cockpit-ws)
+Provides: group(cockpit-wsinstance)
+Provides: user(cockpit-ws)
+Provides: user(cockpit-wsinstance)
+%endif
# prevent hard python3 dependency for cockpit-desktop, it falls back to other browsers
%global __requires_exclude_from ^%{_libexecdir}/cockpit-client$
@@ -366,11 +392,18 @@ authentication via sssd/FreeIPA.
%doc %{_mandir}/man8/pam_ssh_add.8.gz
%dir %{_sysconfdir}/cockpit
%config(noreplace) %{_sysconfdir}/cockpit/ws-certs.d
+%if 0%{?suse_version} > 1500
+%{_pam_vendordir}/cockpit
+%else
%config(noreplace) %{_sysconfdir}/pam.d/cockpit
+%endif
# created in %post, so that users can rm the files
%ghost %{_sysconfdir}/issue.d/cockpit.issue
%ghost %{_sysconfdir}/motd.d/cockpit
%ghost %attr(0644, root, root) %{_sysconfdir}/cockpit/disallowed-users
+%if 0%{?suse_version}
+%ghost /run/cockpit/motd
+%endif
%dir %{_datadir}/cockpit/motd
%{_datadir}/cockpit/motd/update-motd
%{_datadir}/cockpit/motd/inactive.motd
@@ -465,7 +498,11 @@ fi
Summary: Cockpit user interface for kernel crash dumping
Requires: cockpit-bridge >= %{required_base}
Requires: cockpit-shell >= %{required_base}
+%if 0%{?suse_version}
+Requires: kexec-tools
+%else
Requires: /usr/bin/kdumpctl
+%endif
BuildArch: noarch
%description kdump
@@ -474,6 +511,8 @@ The Cockpit component for configuring kernel crash dumping.
%files kdump -f kdump.list
%{_datadir}/metainfo/org.cockpit_project.cockpit_kdump.metainfo.xml
+# sosreport is not supported on opensuse yet
+%if !0%{?suse_version}
%package sosreport
Summary: Cockpit user interface for diagnostic reports
Requires: cockpit-bridge >= %{required_base}
@@ -488,6 +527,7 @@ sosreport tool.
%files sosreport -f sosreport.list
%{_datadir}/metainfo/org.cockpit_project.cockpit_sosreport.metainfo.xml
%{_datadir}/icons/hicolor/64x64/apps/cockpit-sosreport.png
+%endif
%package networkmanager
Summary: Cockpit user interface for networking, using NetworkManager
@@ -512,7 +552,10 @@ The Cockpit component for managing networking. This package uses NetworkManager
Summary: Cockpit SELinux package
Requires: cockpit-bridge >= %{required_base}
Requires: cockpit-shell >= %{required_base}
-Requires: setroubleshoot-server >= 3.3.3
+# setroubleshoot is available on SLE Micro starting with 5.5
+%if !0%{?is_smo} || ( 0%{?is_smo} && 0%{?sle_version} >= 150500 )
+Requires: setroubleshoot-server >= 3.3.3
+%endif
BuildArch: noarch
%description selinux
diff --git a/tools/cockpit.suse.pam b/tools/cockpit.suse.pam
new file mode 100644
index 000000000000..ffd1729508c0
--- /dev/null
+++ b/tools/cockpit.suse.pam
@@ -0,0 +1,11 @@
+#%PAM-1.0
+auth substack common-auth
+# List of users to deny access to Cockpit, by default root is included.
+auth required pam_listfile.so item=user sense=deny file=/etc/cockpit/disallowed-users onerr=succeed
+account required pam_nologin.so
+account include common-account
+password include common-password
+session required pam_loginuid.so
+session optional pam_keyinit.so force revoke
+session include common-session
+auth [user_unknown=ignore success=ok] pam_oath.so usersfile=${HOME}/.pam_oath_usersfile no_usersfile_okay window=20 digits=6