From 4f83dd64cb18a7ab77f2c318170751768010fab3 Mon Sep 17 00:00:00 2001 From: Welder Luz Date: Tue, 21 Nov 2023 15:03:04 -0300 Subject: [PATCH 1/3] Include reboot required indication for non-Suse distros --- .../rhn/domain/server/MinionServer.java | 22 +- .../domain/server/Server_legacyUser.hbm.xml | 9 +- .../rhn/manager/satellite/UpgradeCommand.java | 21 ++ .../com/suse/manager/reactor/SaltReactor.java | 5 +- .../reactor/test/RebootInfoBeaconTest.java | 6 +- .../src/com/suse/manager/utils/SaltUtils.java | 4 + .../pillar/MinionGeneralPillarGenerator.java | 5 + .../custom/PkgProfileUpdateSlsResult.java | 11 + ....changes.welder.reboot-required-any-distro | 1 + .../common/tables/suseMinionInfo.sql | 18 +- .../postgres/procs/update_system_overview.sql | 5 +- .../200-reboot-required-any-distro.sql | 19 + .../201-replace-update-system-overview.sql | 354 ++++++++++++++++++ .../203-enable-pillar-refresh.sql | 4 + .../salt/packages/profileupdate.sls | 6 + .../src/beacons/reboot_info.py | 23 +- .../src/modules/reboot_info.py | 46 +++ .../src/tests/test_beacon_reboot_info.py | 107 ++---- .../src/tests/test_module_reboot_info.py | 86 +++++ ....changes.welder.reboot-required-any-distro | 1 + .../step_definitions/command_steps.rb | 1 + 21 files changed, 645 insertions(+), 109 deletions(-) create mode 100644 java/spacewalk-java.changes.welder.reboot-required-any-distro create mode 100644 schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/200-reboot-required-any-distro.sql create mode 100644 schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/201-replace-update-system-overview.sql create mode 100644 schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/203-enable-pillar-refresh.sql create mode 100644 susemanager-utils/susemanager-sls/src/modules/reboot_info.py create mode 100644 susemanager-utils/susemanager-sls/src/tests/test_module_reboot_info.py create mode 100644 susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.reboot-required-any-distro diff --git a/java/code/src/com/redhat/rhn/domain/server/MinionServer.java b/java/code/src/com/redhat/rhn/domain/server/MinionServer.java index 0f1bd29ae1b9..21b11394f6a1 100644 --- a/java/code/src/com/redhat/rhn/domain/server/MinionServer.java +++ b/java/code/src/com/redhat/rhn/domain/server/MinionServer.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -39,14 +40,7 @@ public class MinionServer extends Server implements SaltConfigurable { private Integer sshPushPort; private Set accessTokens = new HashSet<>(); private Set pillars = new HashSet<>(); - /** - We typically look at the packages installed on a system to identify whether a reboot is necessary. - This property initially only works for transactional systems, but the idea is that gradually the - other types of systems also provide this information directly to be stored here so we no longer rely - on package related inference to determine whether a reboot is necessary. Even because this - information does not always depend only on the packages. - */ - private Boolean rebootNeeded; + private Date rebootRequiredAfter; /** * Constructs a MinionServer instance. @@ -325,11 +319,15 @@ private boolean updateServerPaths(Optional proxy, Optional hostn return changed; } - public Boolean isRebootNeeded() { - return rebootNeeded; + public boolean isRebootNeeded() { + return getLastBoot() != null && rebootRequiredAfter != null && getLastBootAsDate().before(rebootRequiredAfter); + } + + public Date getRebootRequiredAfter() { + return rebootRequiredAfter; } - public void setRebootNeeded(Boolean rebootNeededIn) { - this.rebootNeeded = rebootNeededIn; + public void setRebootRequiredAfter(Date rebootRequiredAfterIn) { + rebootRequiredAfter = rebootRequiredAfterIn; } } diff --git a/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml b/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml index 7493155e7b09..17bd1caf395c 100644 --- a/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml +++ b/java/code/src/com/redhat/rhn/domain/server/Server_legacyUser.hbm.xml @@ -184,14 +184,7 @@ PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" table="suseMinionInfo"> - - + diff --git a/java/code/src/com/redhat/rhn/manager/satellite/UpgradeCommand.java b/java/code/src/com/redhat/rhn/manager/satellite/UpgradeCommand.java index 3deeb72dbd03..8bdf9e728c57 100644 --- a/java/code/src/com/redhat/rhn/manager/satellite/UpgradeCommand.java +++ b/java/code/src/com/redhat/rhn/manager/satellite/UpgradeCommand.java @@ -78,6 +78,8 @@ public class UpgradeCommand extends BaseTransactionCommand { UPGRADE_TASK_NAME + "refresh_custom_sls_files"; public static final String REFRESH_VIRTHOST_PILLARS = UPGRADE_TASK_NAME + "virthost_pillar_refresh"; + public static final String REFRESH_ALL_SYSTEMS_PILLARS = + UPGRADE_TASK_NAME + "all_systems_pillar_refresh"; public static final String SYSTEM_THRESHOLD_FROM_CONFIG = UPGRADE_TASK_NAME + "system_threshold_conf"; @@ -152,6 +154,9 @@ public void upgrade() { case SYSTEM_THRESHOLD_FROM_CONFIG: convertSystemThresholdFromConfig(); break; + case REFRESH_ALL_SYSTEMS_PILLARS: + refreshAllSystemsPillar(); + break; default: } // always run this @@ -359,6 +364,22 @@ private void refreshVirtHostPillar() { } } + /** + * Regenerate pillar data for every registered system. + */ + private void refreshAllSystemsPillar() { + try { + List hosts = MinionServerFactory.listMinions(); + hosts.forEach(MinionPillarManager.INSTANCE::generatePillar); + List minionIds = hosts.stream().map(MinionServer::getMinionId).collect(Collectors.toList()); + GlobalInstanceHolder.SALT_API.refreshPillar(new MinionList(minionIds)); + log.info("Refreshed hosts pillar"); + } + catch (Exception e) { + log.error("Error refreshing hosts pillar. Ignoring.", e); + } + } + private void convertSystemThresholdFromConfig() { log.warn("Converting web.system_checkin_threshold to DB config"); SatConfigFactory.setSatConfigValue(SatConfigFactory.SYSTEM_CHECKIN_THRESHOLD, diff --git a/java/code/src/com/suse/manager/reactor/SaltReactor.java b/java/code/src/com/suse/manager/reactor/SaltReactor.java index 0fdf5e12a3e4..11d9c9ab05e6 100644 --- a/java/code/src/com/suse/manager/reactor/SaltReactor.java +++ b/java/code/src/com/suse/manager/reactor/SaltReactor.java @@ -76,6 +76,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Date; import java.util.Optional; import java.util.stream.Stream; @@ -326,7 +327,9 @@ else if (beaconEvent.getBeacon().equals("reboot_info")) { Optional minion = MinionServerFactory.findByMinionId(beaconEvent.getMinionId()); minion.ifPresent( m -> { - m.setRebootNeeded((Boolean) beaconEvent.getData().get("reboot_needed")); + Boolean rebootRequired = (Boolean) beaconEvent.getData().get("reboot_needed"); + Date rebootRequiredAfter = rebootRequired ? new Date() : null; + m.setRebootRequiredAfter(rebootRequiredAfter); SystemManager.updateSystemOverview(m); } ); diff --git a/java/code/src/com/suse/manager/reactor/test/RebootInfoBeaconTest.java b/java/code/src/com/suse/manager/reactor/test/RebootInfoBeaconTest.java index 483e39a7e69d..d3d03867d302 100644 --- a/java/code/src/com/suse/manager/reactor/test/RebootInfoBeaconTest.java +++ b/java/code/src/com/suse/manager/reactor/test/RebootInfoBeaconTest.java @@ -76,11 +76,13 @@ public void setUp() { public void testRebootInfoEvent() throws Exception { MinionServer minion1 = MinionServerFactoryTest.createTestMinionServer(user); minion1.setMinionId("slemicro100001"); - minion1.setRebootNeeded(false); + minion1.setLastBoot(System.currentTimeMillis() / 1000); + minion1.setRebootRequiredAfter(null); MinionServer minion2 = MinionServerFactoryTest.createTestMinionServer(user); minion2.setMinionId("slemicro100002"); - minion2.setRebootNeeded(false); + minion1.setLastBoot(System.currentTimeMillis() / 1000); + minion2.setRebootRequiredAfter(null); // Event indicating that reboot is needed for minion 1 BeaconEvent event = buildRebootInfoEvent(minion1.getMinionId(), true); diff --git a/java/code/src/com/suse/manager/utils/SaltUtils.java b/java/code/src/com/suse/manager/utils/SaltUtils.java index 57c1727a24bf..1986d8215d52 100644 --- a/java/code/src/com/suse/manager/utils/SaltUtils.java +++ b/java/code/src/com/suse/manager/utils/SaltUtils.java @@ -1485,6 +1485,10 @@ else if ("debian".equalsIgnoreCase(grains.getValueAsString("os"))) { .map(n -> n.longValue()) .orElse(null)); + result.getRebootRequired() + .map(flag -> (Boolean) flag.getChanges().getRet().get("reboot_required")) + .ifPresent(flag -> server.setRebootRequiredAfter(flag ? new Date() : null)); + // Update live patching version server.setKernelLiveVersion(result.getKernelLiveVersionInfo() .map(klv -> klv.getChanges().getRet()).filter(Objects::nonNull) diff --git a/java/code/src/com/suse/manager/webui/services/pillar/MinionGeneralPillarGenerator.java b/java/code/src/com/suse/manager/webui/services/pillar/MinionGeneralPillarGenerator.java index f07f58af08cc..577777838621 100644 --- a/java/code/src/com/suse/manager/webui/services/pillar/MinionGeneralPillarGenerator.java +++ b/java/code/src/com/suse/manager/webui/services/pillar/MinionGeneralPillarGenerator.java @@ -43,10 +43,14 @@ public class MinionGeneralPillarGenerator extends MinionPillarGeneratorBase { public static final String CATEGORY = "general"; private static final int PKGSET_INTERVAL = 5; + private static final Integer REBOOT_INFO_INTERVAL = 10; private static final Map PKGSET_BEACON_PROPS = new HashMap<>(); + private static final Map REBOOT_INFO_BEACON_PROPS = new HashMap<>(); + static { PKGSET_BEACON_PROPS.put("interval", PKGSET_INTERVAL); + REBOOT_INFO_BEACON_PROPS.put("interval", REBOOT_INFO_INTERVAL); } /** @@ -91,6 +95,7 @@ public Optional generatePillarData(MinionServer minion) { minion.getOsFamily().toLowerCase().equals("redhat") || minion.getOsFamily().toLowerCase().equals("debian")) { beaconConfig.put("pkgset", PKGSET_BEACON_PROPS); + beaconConfig.put("reboot_info", REBOOT_INFO_BEACON_PROPS); } if (!beaconConfig.isEmpty()) { pillar.add("beacons", beaconConfig); diff --git a/java/code/src/com/suse/manager/webui/utils/salt/custom/PkgProfileUpdateSlsResult.java b/java/code/src/com/suse/manager/webui/utils/salt/custom/PkgProfileUpdateSlsResult.java index 55258917a1f7..953f2d8c8709 100644 --- a/java/code/src/com/suse/manager/webui/utils/salt/custom/PkgProfileUpdateSlsResult.java +++ b/java/code/src/com/suse/manager/webui/utils/salt/custom/PkgProfileUpdateSlsResult.java @@ -56,6 +56,9 @@ public class PkgProfileUpdateSlsResult { @SerializedName("mgrcompat_|-status_uptime_|-status.uptime_|-module_run") private Optional>>> upTime = Optional.empty(); + @SerializedName("mgrcompat_|-reboot_required_|-reboot_info.reboot_required_|-module_run") + private Optional>>> rebootRequired = Optional.empty(); + @SerializedName("mgrcompat_|-kernel_live_version_|-sumautil.get_kernel_live_version_|-module_run") private Optional>> kernelLiveVersionInfo = Optional.empty(); @@ -103,6 +106,14 @@ public Optional>>> getUpTime() { return upTime; } + /** + * Gets the reboot required indication + * @return optional of reboot required flag + */ + public Optional>>> getRebootRequired() { + return rebootRequired; + } + /** * Gets live patching info. * diff --git a/java/spacewalk-java.changes.welder.reboot-required-any-distro b/java/spacewalk-java.changes.welder.reboot-required-any-distro new file mode 100644 index 000000000000..634728cc7860 --- /dev/null +++ b/java/spacewalk-java.changes.welder.reboot-required-any-distro @@ -0,0 +1 @@ +- Include reboot required indication for non-Suse distros diff --git a/schema/spacewalk/common/tables/suseMinionInfo.sql b/schema/spacewalk/common/tables/suseMinionInfo.sql index f9bcddc8b90c..cb85182ca7ea 100644 --- a/schema/spacewalk/common/tables/suseMinionInfo.sql +++ b/schema/spacewalk/common/tables/suseMinionInfo.sql @@ -15,15 +15,15 @@ CREATE TABLE suseMinionInfo ( - server_id NUMERIC NOT NULL - CONSTRAINT suse_minion_info_sid_fk - REFERENCES rhnServer (id) - ON DELETE CASCADE, - minion_id VARCHAR(256) NOT NULL, - os_family VARCHAR(32), - kernel_live_version VARCHAR(255), - ssh_push_port NUMERIC, - reboot_needed CHAR(1), + server_id NUMERIC NOT NULL + CONSTRAINT suse_minion_info_sid_fk + REFERENCES rhnServer (id) + ON DELETE CASCADE, + minion_id VARCHAR(256) NOT NULL, + os_family VARCHAR(32), + kernel_live_version VARCHAR(255), + ssh_push_port NUMERIC, + reboot_required_after TIMESTAMPTZ, created TIMESTAMPTZ DEFAULT (current_timestamp) NOT NULL, modified TIMESTAMPTZ diff --git a/schema/spacewalk/postgres/procs/update_system_overview.sql b/schema/spacewalk/postgres/procs/update_system_overview.sql index 32b2142ccd07..717ae3babf7b 100644 --- a/schema/spacewalk/postgres/procs/update_system_overview.sql +++ b/schema/spacewalk/postgres/procs/update_system_overview.sql @@ -216,9 +216,10 @@ begin (SELECT 1 FROM suseMinionInfo smi WHERE smi.server_id = S.id - AND smi.reboot_needed = 'Y' + AND to_date('1970-01-01', 'YYYY-MM-DD') + + numtodsinterval(S.last_boot, 'second') < smi.reboot_required_after at time zone 'UTC' ) - ); + ); SELECT TRUE into new_kickstarting FROM rhnKickstartSession KSS, rhnKickstartSessionState KSSS diff --git a/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/200-reboot-required-any-distro.sql b/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/200-reboot-required-any-distro.sql new file mode 100644 index 000000000000..0ac8b6b977b5 --- /dev/null +++ b/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/200-reboot-required-any-distro.sql @@ -0,0 +1,19 @@ +ALTER TABLE suseMinionInfo ADD COLUMN IF NOT EXISTS + reboot_required_after TIMESTAMPTZ; + +DO $$ +BEGIN + IF EXISTS ( + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'suseminioninfo' AND column_name = 'reboot_needed' + ) THEN + UPDATE suseMinionInfo + SET reboot_required_after = CASE + WHEN reboot_needed = 'Y' THEN CURRENT_TIMESTAMP + ELSE NULL + END; + END IF; +END $$; + +ALTER TABLE suseMinionInfo DROP COLUMN IF EXISTS reboot_needed; diff --git a/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/201-replace-update-system-overview.sql b/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/201-replace-update-system-overview.sql new file mode 100644 index 000000000000..b5599830409f --- /dev/null +++ b/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/201-replace-update-system-overview.sql @@ -0,0 +1,354 @@ +------------------- +-- Update procedure +------------------- + +create or replace function update_system_overview ( + sid in numeric +) returns void as +$$ +declare + new_id numeric; + new_server_name varchar; + new_created timestamptz; + new_creator_name varchar; + new_modified timestamptz; + new_group_count numeric; + new_channel_id numeric; + new_channel_labels varchar; + new_security_errata numeric; + new_bug_errata numeric; + new_enhancement_errata numeric; + new_outdated_packages numeric; + new_config_files_with_differences numeric; + new_last_checkin timestamptz; + new_entitlement_level VARCHAR(256); + new_virtual_guest BOOLEAN; + new_virtual_host BOOLEAN; + new_proxy BOOLEAN; + new_mgr_server BOOLEAN; + new_selectable BOOLEAN; + new_extra_pkg_count NUMERIC; + new_requires_reboot BOOLEAN; + new_kickstarting BOOLEAN; + new_actions_count NUMERIC; + new_package_actions_count NUMERIC; + new_unscheduled_errata_count NUMERIC; + new_status_type VARCHAR(20); + awol BOOLEAN; +begin + SELECT s.id, s.name, s.modified + INTO + new_id, + new_server_name, + new_modified + FROM rhnServer s + WHERE s.id = sid; + + -- The system has likely been removed after filing the update request + IF new_id IS NULL THEN + return; + END IF; + + SELECT count(server_group_id) + INTO new_group_count + FROM rhnVisibleServerGroupMembers + WHERE server_id = sid; + + SELECT C.id + INTO new_channel_id + FROM rhnChannel C, + rhnServerChannel SC + WHERE SC.server_id = sid AND SC.channel_id = C.id AND C.parent_channel IS NULL; + + SELECT coalesce(C.name, '(none)') + INTO new_channel_labels + FROM rhnChannel C, + rhnServerChannel SC + WHERE SC.server_id = sid AND SC.channel_id = C.id AND C.parent_channel IS NULL; + + SELECT count(*) + INTO new_security_errata + FROM rhnServerErrataTypeView setv + WHERE setv.server_id = sid AND setv.errata_type = 'Security Advisory'; + + SELECT count(*) + INTO new_bug_errata + FROM rhnServerErrataTypeView setv + WHERE setv.server_id = sid AND setv.errata_type = 'Bug Fix Advisory'; + + SELECT count(*) + INTO new_enhancement_errata + FROM rhnServerErrataTypeView setv + WHERE setv.server_id = sid AND setv.errata_type = 'Product Enhancement Advisory'; + + SELECT count(DISTINCT p.name_id) + INTO new_outdated_packages + FROM rhnPackage p, rhnServerNeededCache snc + WHERE snc.server_id = sid AND p.id = snc.package_id; + + SELECT count(*) + INTO new_config_files_with_differences + FROM rhnActionConfigRevision ACR + INNER JOIN rhnActionConfigRevisionResult ACRR on ACR.id = ACRR.action_config_revision_id + WHERE ACR.server_id = sid + AND ACR.action_id = ( + SELECT MAX(rA.id) + FROM rhnAction rA + INNER JOIN rhnServerAction rSA ON rSA.action_id = rA.id + INNER JOIN rhnActionStatus rAS ON rAS.id = rSA.status + INNER JOIN rhnActionType rAT ON rAT.id = rA.action_type + WHERE rSA.server_id = sid + AND rAS.name in ('Completed', 'Failed') + AND rAT.label = 'configfiles.diff' + ) + AND ACR.failure_id is null + AND ACRR.result is not null; + + SELECT TO_CHAR(checkin, 'YYYY-MM-DD HH24:MI:SS') + INTO new_last_checkin + FROM rhnServerInfo WHERE server_id = sid; + + SELECT date_diff_in_days(CAST(new_last_checkin AS TIMESTAMP), NOW()) > C.threshold INTO awol + FROM (SELECT CAST(coalesce(value, default_value) AS INTEGER) AS threshold + FROM rhnconfiguration WHERE key = 'system_checkin_threshold') C; + + select created, + (SELECT wc.login FROM web_contact wc WHERE wc.id = s.creator_id) as creator_name + into new_created, new_creator_name from rhnServer as S where S.id = sid; + + select TRUE into new_virtual_guest from rhnVirtualInstance where virtual_system_id = sid; + + select TRUE into new_virtual_host + from rhnServerGroup sg + INNER JOIN rhnServerGroupMembers sgm ON sg.id = sgm.server_group_id + INNER JOIN rhnServerGroupType sgt ON sgt.id = sg.group_type + where + sgm.server_id = sid and + sgt.label='virtualization_host' + union + select TRUE as virtual_host + from rhnVirtualInstance VI + where VI.host_system_id = sid; + + select TRUE into new_proxy FROM rhnProxyInfo PI WHERE PI.server_id = sid; + + select TRUE into new_mgr_server FROM suseMgrServerInfo SI WHERE SI.server_id = sid; + + SELECT TRUE into new_selectable + FROM rhnServerFeaturesView SFV + WHERE SFV.server_id = sid AND SFV.label = 'ftr_system_grouping'; + + SELECT string_agg(ordered.label, ',') INTO new_entitlement_level + FROM ( + SELECT label + FROM rhnServerEntitlementView AS SEV + WHERE SEV.server_id = sid + ORDER BY CASE SEV.is_base WHEN 'Y' THEN 1 WHEN 'N' THEN 2 END, SEV.label + ) AS ordered; + + SELECT count(sp.name_id) AS extra_pkg_count + INTO new_extra_pkg_count + FROM rhnServerPackage sp + LEFT OUTER JOIN (SELECT sc.server_id, + cp.package_id, + p.name_id, + p.evr_id, + p.package_arch_id + FROM rhnPackage p, + rhnServerChannel sc, + rhnServerPackage sp2, + rhnChannelPackage cp, + rhnUserServerPerms usp2 + WHERE cp.package_id = p.id + AND cp.channel_id = sc.channel_id + AND sc.server_id = usp2.server_id + AND sc.server_id = sp2.server_id + AND sp2.server_id = sid + AND sp2.name_id = p.name_id + AND sp2.evr_id = p.evr_id + AND sp2.package_arch_id = p.package_arch_id + ) scp ON (scp.server_id = sp.server_id AND + sp.name_id = scp.name_id AND + sp.evr_id = scp.evr_id AND + sp.package_arch_id = scp.package_arch_id) + WHERE scp.package_id IS NULL AND sp.server_id = sid + GROUP BY sp.server_id; + + SELECT TRUE into new_requires_reboot + FROM rhnServer S + WHERE + S.id = sid + AND (EXISTS (SELECT 1 + FROM rhnServerPackage SP + JOIN rhnPackage P ON (P.evr_id = SP.evr_id AND P.name_id = SP.name_id) + JOIN rhnErrataPackage EP ON EP.package_id = P.id + JOIN rhnErrata E ON EP.errata_id = E.id + JOIN rhnerratakeyword EK ON E.id = EK.errata_id + WHERE SP.server_id = S.id + AND EK.keyword = 'reboot_suggested' + AND (to_date('1970-01-01', 'YYYY-MM-DD') + + numtodsinterval(S.last_boot, 'second')) < SP.installtime at time zone 'UTC' + ) + OR EXISTS + (SELECT 1 + FROM rhnServerPackage SP + JOIN rhnPackage P ON (P.evr_id = SP.evr_id AND P.name_id = SP.name_id) + JOIN rhnPackageProvides PP ON P.id = PP.package_id + JOIN rhnPackageCapability PC ON PP.capability_id = PC.id + WHERE SP.server_id = S.id + AND PC.name = 'installhint(reboot-needed)' + AND (to_date('1970-01-01', 'YYYY-MM-DD') + + numtodsinterval(S.last_boot, 'second')) < SP.installtime at time zone 'UTC' + ) + OR EXISTS + (SELECT 1 + FROM suseMinionInfo smi + WHERE smi.server_id = S.id + AND to_date('1970-01-01', 'YYYY-MM-DD') + + numtodsinterval(S.last_boot, 'second') < smi.reboot_required_after at time zone 'UTC' + ) + ); + + SELECT TRUE into new_kickstarting + FROM rhnKickstartSession KSS, rhnKickstartSessionState KSSS + WHERE (KSS.old_server_id = sid OR KSS.new_server_id = sid) + AND KSSS.id = KSS.state_id + AND KSSS.label NOT IN ('complete', 'failed'); + + SELECT count(distinct SA.action_id) INTO new_actions_count + FROM rhnServerAction SA, rhnActionStatus AST + WHERE SA.server_id = sid + AND AST.id = SA.status + AND AST.name = 'Queued'; + + SELECT count(A.id) INTO new_package_actions_count + FROM rhnServerAction SA, rhnActionStatus AST, rhnActionType AT, rhnAction A + WHERE SA.server_id = sid + AND AST.id = SA.status + AND AST.name = 'Queued' + AND A.id = SA.action_id + AND AT.id = A.action_type + AND AT.label IN('packages.refresh_list', 'packages.update', + 'packages.remove', 'errata.update', 'packages.delta'); + + SELECT COUNT(DISTINCT E.id) INTO new_unscheduled_errata_count + FROM rhnErrata E, rhnServerNeededErrataCache SNPC + WHERE SNPC.server_id = sid + AND SNPC.errata_id = E.id + AND NOT EXISTS (SELECT SA.server_id + FROM rhnActionErrataUpdate AEU, + rhnServerAction SA, + rhnActionStatus AST + WHERE SA.server_id = sid + AND SA.status = AST.id + AND AST.name IN('Queued', 'Picked Up') + AND AEU.action_id = SA.action_id + AND AEU.errata_id = E.id); + + if new_entitlement_level = '' then + new_status_type = 'unentitled'; + elsif awol then + new_status_type = 'awol'; + elsif coalesce(new_kickstarting, FALSE) then + new_status_type = 'kickstarting'; + elsif new_enhancement_errata + new_bug_errata + new_security_errata > 0 and new_unscheduled_errata_count = 0 then + new_status_type = 'updates scheduled'; + elsif new_actions_count > 0 then + new_status_type = 'actions scheduled'; + elsif new_enhancement_errata + new_bug_errata + new_security_errata + new_outdated_packages + new_package_actions_count = 0 then + new_status_type = 'up2date'; + elsif new_security_errata > 0 then + new_status_type = 'critical'; + elsif new_outdated_packages > 0 then + new_status_type = 'updates'; + else + new_status_type = null; + end if; + + insert into suseSystemOverview ( + id, + server_name, + created, + creator_name, + modified, + group_count, + channel_id, + channel_labels, + security_errata, + bug_errata, + enhancement_errata, + outdated_packages, + config_files_with_differences, + last_checkin, + entitlement_level, + virtual_guest, + virtual_host, + proxy, + mgr_server, + selectable, + extra_pkg_count, + requires_reboot, + kickstarting, + actions_count, + package_actions_count, + unscheduled_errata_count, + status_type + ) values ( + new_id, + new_server_name, + new_created, + new_creator_name, + new_modified, + new_group_count, + new_channel_id, + new_channel_labels, + new_security_errata, + new_bug_errata, + new_enhancement_errata, + new_outdated_packages, + new_config_files_with_differences, + new_last_checkin, + new_entitlement_level, + coalesce(new_virtual_guest, FALSE), + coalesce(new_virtual_host, FALSE), + coalesce(new_proxy, FALSE), + coalesce(new_mgr_server, FALSE), + coalesce(new_selectable, FALSE), + new_extra_pkg_count, + coalesce(new_requires_reboot, FALSE), + coalesce(new_kickstarting, FALSE), + new_actions_count, + new_package_actions_count, + new_unscheduled_errata_count, + new_status_type + ) on conflict (id) + do update set + server_name = EXCLUDED.server_name, + created = EXCLUDED.created, + creator_name = EXCLUDED.creator_name, + modified = EXCLUDED.modified, + group_count = EXCLUDED.group_count, + channel_id = EXCLUDED.channel_id, + channel_labels = EXCLUDED.channel_labels, + security_errata = EXCLUDED.security_errata, + bug_errata = EXCLUDED.bug_errata, + enhancement_errata = EXCLUDED.enhancement_errata, + outdated_packages = EXCLUDED.outdated_packages, + config_files_with_differences = EXCLUDED.config_files_with_differences, + last_checkin = EXCLUDED.last_checkin, + entitlement_level = EXCLUDED.entitlement_level, + virtual_guest = EXCLUDED.virtual_guest, + virtual_host = EXCLUDED.virtual_host, + proxy = EXCLUDED.proxy, + mgr_server = EXCLUDED.mgr_server, + selectable = EXCLUDED.selectable, + extra_pkg_count = EXCLUDED.extra_pkg_count, + requires_reboot = EXCLUDED.requires_reboot, + kickstarting = EXCLUDED.kickstarting, + actions_count = EXCLUDED.actions_count, + package_actions_count = EXCLUDED.package_actions_count, + unscheduled_errata_count = EXCLUDED.unscheduled_errata_count, + status_type = EXCLUDED.status_type; +end; +$$ +language plpgsql; diff --git a/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/203-enable-pillar-refresh.sql b/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/203-enable-pillar-refresh.sql new file mode 100644 index 000000000000..e65695557200 --- /dev/null +++ b/schema/spacewalk/upgrade/susemanager-schema-4.4.9-to-susemanager-schema-4.4.10/203-enable-pillar-refresh.sql @@ -0,0 +1,4 @@ +INSERT INTO rhnTaskQueue (id, org_id, task_name, task_data) +SELECT nextval('rhn_task_queue_id_seq'), id, 'upgrade_satellite_all_systems_pillar_refresh', 0 +FROM web_customer +WHERE id = 1; diff --git a/susemanager-utils/susemanager-sls/salt/packages/profileupdate.sls b/susemanager-utils/susemanager-sls/salt/packages/profileupdate.sls index 93634d0499e3..7ba5d68d24fb 100644 --- a/susemanager-utils/susemanager-sls/salt/packages/profileupdate.sls +++ b/susemanager-utils/susemanager-sls/salt/packages/profileupdate.sls @@ -44,6 +44,12 @@ status_uptime: mgrcompat.module_run: - name: status.uptime +{%- if not grains.get('transactional', False) %} +reboot_required: + mgrcompat.module_run: + - name: reboot_info.reboot_required +{%- endif %} + kernel_live_version: mgrcompat.module_run: - name: sumautil.get_kernel_live_version diff --git a/susemanager-utils/susemanager-sls/src/beacons/reboot_info.py b/susemanager-utils/susemanager-sls/src/beacons/reboot_info.py index 8834c2af64bc..fa4dd082541d 100644 --- a/susemanager-utils/susemanager-sls/src/beacons/reboot_info.py +++ b/susemanager-utils/susemanager-sls/src/beacons/reboot_info.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- """ -Watch pending transactions in transactional systems and -fire an event to SUSE Manager indicating if a reboot is needed. +Watch system status and fire an event to SUSE Manager indicating +when a reboot is required. """ __virtualname__ = "reboot_info" def __virtual__(): - return __grains__.get("transactional", False) + ''' + Run on Debian, Suse and RedHat systems. + ''' + return __grains__['os_family'] in ['Debian', 'Suse', 'RedHat'] def validate(config): @@ -22,9 +25,9 @@ def validate(config): def beacon(config): """ - Monitor pending transactions of transactional update - to verify whether a reboot is required. When the reboot - needed status changes, it fires a new event. + Monitor system status to verify whether a reboot + is required. The first time it detects that a reboot + is necessary, it fires an event. Example Config @@ -36,10 +39,12 @@ def beacon(config): """ ret = [] - reboot_needed = __salt__["transactional_update.pending_transaction"]() - if __context__.get("reboot_needed") != reboot_needed: + result = __salt__["reboot_info.reboot_required"]() + reboot_needed = result.get("reboot_required", False) + + if reboot_needed and not __context__.get("reboot_needed", False): ret.append({"reboot_needed": reboot_needed}) - __context__["reboot_needed"] = reboot_needed + __context__["reboot_needed"] = reboot_needed return ret diff --git a/susemanager-utils/susemanager-sls/src/modules/reboot_info.py b/susemanager-utils/susemanager-sls/src/modules/reboot_info.py new file mode 100644 index 000000000000..cb69aeb31370 --- /dev/null +++ b/susemanager-utils/susemanager-sls/src/modules/reboot_info.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import logging +import os +import salt + +log = logging.getLogger(__name__) + +__salt__ = { + 'cmd.run_all': salt.modules.cmdmod.run_all, +} + +__virtualname__ = 'reboot_info' + +def __virtual__(): + ''' + Run on Debian, Suse and RedHat systems. + ''' + return __grains__['os_family'] in ['Debian', 'Suse', 'RedHat'] + +def _check_cmd_exit_code(cmd, code): + output = __salt__['cmd.run_all'](cmd, python_shell=True) + if 'stderr' in output and output['stderr']: + log.error(output['stderr']) + return output['retcode'] == code + +def reboot_required(): + ''' + Check if reboot is required + + CLI Example: + + .. code-block:: bash + + salt '*' reboot_info.reboot_required + ''' + if __grains__.get('transactional', False): + result = __salt__['transactional_update.pending_transaction']() + elif __grains__['os_family'] == 'Debian': + result = os.path.exists('/var/run/reboot-required') + elif __grains__['os_family'] == 'Suse': + result = os.path.exists('/run/reboot-needed') or os.path.exists('/boot/do_purge_kernels') + elif __grains__['os_family'] == 'RedHat': + cmd = 'dnf -q needs-restarting -r' if __grains__['osmajorrelease'] >= 8 else 'needs-restarting -r' + result = _check_cmd_exit_code(cmd, 1) + + return { 'reboot_required': result } diff --git a/susemanager-utils/susemanager-sls/src/tests/test_beacon_reboot_info.py b/susemanager-utils/susemanager-sls/src/tests/test_beacon_reboot_info.py index b6493885d415..2d19c4fabf7a 100644 --- a/susemanager-utils/susemanager-sls/src/tests/test_beacon_reboot_info.py +++ b/susemanager-utils/susemanager-sls/src/tests/test_beacon_reboot_info.py @@ -1,70 +1,45 @@ from ..beacons import reboot_info - -def _pending_transaction_false(): - return False - -def _pending_transaction_true(): - return True - -def test_should_fire_event_when_context_is_empty(): - """ - The __context__ is empty and reboot is not required - """ - reboot_info.__context__ = {} - reboot_info.__salt__ = { - "transactional_update.pending_transaction": _pending_transaction_false - } - ret = reboot_info.beacon({}) - assert ret == [{ "reboot_needed": False }] - - """ - The __context__ is empty and reboot is required - """ - reboot_info.__context__ = {} - reboot_info.__salt__ = { - "transactional_update.pending_transaction": _pending_transaction_true - } - ret = reboot_info.beacon({}) - assert ret == [{ "reboot_needed": True }] - -def test_should_not_fire_event_when_already_fired(): - """ - The __context__ already register that reboot is required - """ - reboot_info.__salt__ = { - "transactional_update.pending_transaction": _pending_transaction_true - } - reboot_info.__context__ = { "reboot_needed": True } - ret = reboot_info.beacon({}) - assert ret == [] - - """ - The __context__ already register that reboot is not required - """ - reboot_info.__salt__ = { - "transactional_update.pending_transaction": _pending_transaction_false - } - reboot_info.__context__ = { "reboot_needed": False } - ret = reboot_info.beacon({}) - assert ret == [] - -def test_should_fire_event_when_reboot_status_changes(): - """ - The __context__ register that reboot is required but there is no pending transaction - """ - reboot_info.__salt__ = { - "transactional_update.pending_transaction": _pending_transaction_false - } - reboot_info.__context__ = { "reboot_needed": True } - ret = reboot_info.beacon({}) - assert ret == [{ "reboot_needed": False }] - - """ - The __context__ register that reboot is not required but there is a pending transaction - """ +import pytest + +def _reboot_not_required(): + return { "reboot_required": False } + +def _reboot_required(): + return { "reboot_required": True } + +context_reboot_required = {"reboot_needed": True} +context_reboot_not_required = {"reboot_needed": False} + +@pytest.mark.parametrize( + "context, module_fn, fire_event", + [ + ( + # The __context__ is empty and reboot is not required, don't fire event. + {}, _reboot_not_required, False, + ), + ( + # The __context__ is empty and reboot is required, fire event. + {}, _reboot_required, True, + ), + ( + # The __context__ already register that reboot is required and it keeps, don't fire again. + context_reboot_required, _reboot_required, False, + ), + ( + # The __context__ register that reboot is required and it changes, don't fire event. + context_reboot_required, _reboot_not_required, False, + ), + ( + # The __context__ register that reboot isn't required but it changed, fire event. + context_reboot_not_required, _reboot_required, True, + ), + ], +) +def test_beacon(context, module_fn, fire_event): + reboot_info.__context__ = context reboot_info.__salt__ = { - "transactional_update.pending_transaction": _pending_transaction_true + "reboot_info.reboot_required": module_fn } - reboot_info.__context__ = { "reboot_needed": False } ret = reboot_info.beacon({}) - assert ret == [{ "reboot_needed": True }] + expected_result = [{ "reboot_needed": True }] if fire_event else [] + assert ret == expected_result diff --git a/susemanager-utils/susemanager-sls/src/tests/test_module_reboot_info.py b/susemanager-utils/susemanager-sls/src/tests/test_module_reboot_info.py new file mode 100644 index 000000000000..b4b5f2d81b0e --- /dev/null +++ b/susemanager-utils/susemanager-sls/src/tests/test_module_reboot_info.py @@ -0,0 +1,86 @@ +from ..modules import reboot_info + +from unittest.mock import MagicMock, patch +import pytest + +@pytest.mark.parametrize( + "os_family, expected_result", + [ + ("Debian", True), + ("Suse", True), + ("RedHat", True), + ("Windows", False), + ], +) +def test_virtual(os_family, expected_result): + reboot_info.__grains__ = { + "os_family": os_family + } + assert reboot_info.__virtual__() == expected_result + + +@pytest.mark.parametrize( + "exit_code_to_check, real_exit_code, result", + [ + (0, 0, True), + (0, 1, False) + ], +) +def test_check_cmd_exit_code(exit_code_to_check, real_exit_code, result): + mock_run_all = MagicMock(return_value={"stderr": None, "retcode": real_exit_code}) + with patch.dict(reboot_info.__salt__, {"cmd.run_all": mock_run_all}): + assert reboot_info._check_cmd_exit_code("fake command", exit_code_to_check) == result + +@pytest.mark.parametrize( + "file_exists, result", + [ + (True, True), + (False, False) + ], +) +def test_reboot_required_debian(file_exists, result): + reboot_info.__grains__["os_family"] = "Debian" + with patch("os.path.exists", return_value=file_exists): + assert reboot_info.reboot_required()["reboot_required"] == result + +@pytest.mark.parametrize( + "file_exists, result", + [ + (True, True), + (False, False) + ], +) +def test_reboot_required_suse(file_exists, result): + reboot_info.__grains__["os_family"] = "Suse" + with patch("os.path.exists", return_value=file_exists): + assert reboot_info.reboot_required()["reboot_required"] == result + +@pytest.mark.parametrize( + "os_major_release, cmd, exit_code, result", + [ + (7, "needs-restarting -r", 1, True), + (7, "needs-restarting -r", 0, False), + (8, "dnf -q needs-restarting -r", 1, True), + (8, "dnf -q needs-restarting -r", 99, False), + ], +) +def test_reboot_required_redhat(os_major_release, cmd, exit_code, result): + reboot_info.__grains__["os_family"] = "RedHat" + reboot_info.__grains__["osmajorrelease"] = os_major_release + reboot_info._check_cmd_exit_code = MagicMock(return_value=exit_code == 1) + assert reboot_info.reboot_required()["reboot_required"] == result + reboot_info._check_cmd_exit_code.assert_called_once_with(cmd, 1) + + +@pytest.mark.parametrize( + "pending_transaction, result", + [ + (True, True), + (False, False) + ], +) +def test_reboot_required_transactional(pending_transaction, result): + reboot_info.__grains__["transactional"] = True + mock_pending_transactions = MagicMock(return_value=pending_transaction) + with patch.dict(reboot_info.__salt__, {"transactional_update.pending_transaction": mock_pending_transactions}): + assert reboot_info.reboot_required()["reboot_required"] == result diff --git a/susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.reboot-required-any-distro b/susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.reboot-required-any-distro new file mode 100644 index 000000000000..634728cc7860 --- /dev/null +++ b/susemanager-utils/susemanager-sls/susemanager-sls.changes.welder.reboot-required-any-distro @@ -0,0 +1 @@ +- Include reboot required indication for non-Suse distros diff --git a/testsuite/features/step_definitions/command_steps.rb b/testsuite/features/step_definitions/command_steps.rb index 97ce01efffb8..4012c77314a2 100644 --- a/testsuite/features/step_definitions/command_steps.rb +++ b/testsuite/features/step_definitions/command_steps.rb @@ -518,6 +518,7 @@ $node_by_host.each do |host, node| next if node.nil? || %w[salt_migration_minion localhost *-ctl].include?(host) + $stdout.puts "Host: #{host}" $stdout.puts "Node: #{node.full_hostname}" extract_logs_from_node(node) end From cfe79bfbc146f8547e255511764dc02c8587d2ae Mon Sep 17 00:00:00 2001 From: Welder Luz Date: Sat, 20 Jan 2024 19:20:08 -0300 Subject: [PATCH 2/3] Add a 1-second timeout for waiting for pillar data to be applied in the test scenario --- .../secondary/min_salt_formulas_advanced.feature | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/testsuite/features/secondary/min_salt_formulas_advanced.feature b/testsuite/features/secondary/min_salt_formulas_advanced.feature index b390639c80b2..9855ced68fa4 100644 --- a/testsuite/features/secondary/min_salt_formulas_advanced.feature +++ b/testsuite/features/secondary/min_salt_formulas_advanced.feature @@ -84,7 +84,8 @@ Feature: Use advanced features of Salt formulas And I enter "pw3" as "testing#pw_opt" And I click on "Save Formula" Then I should see a "Formula saved" text - And the pillar data for "testing:str" should be "text1" on "sle_minion" + When I wait for "1" seconds + Then the pillar data for "testing:str" should be "text1" on "sle_minion" And the pillar data for "testing:str_def" should be "text2" on "sle_minion" And the pillar data for "testing:str_or_null" should be "text3" on "sle_minion" And the pillar data for "testing:str_opt" should be "text4" on "sle_minion" @@ -104,7 +105,8 @@ Feature: Use advanced features of Salt formulas And I click on "Clear values" and confirm And I click on "Save Formula" Then I should see a "Formula saved" text - And the pillar data for "testing:str" should be "" on "sle_minion" + When I wait for "1" seconds + Then the pillar data for "testing:str" should be "" on "sle_minion" And the pillar data for "testing:str_def" should be "defvalue" on "sle_minion" And the pillar data for "testing:str_or_null" should be "None" on "sle_minion" And the pillar data for "testing" should not contain "str_opt" on "sle_minion" @@ -141,7 +143,8 @@ Feature: Use advanced features of Salt formulas And I enter "pw1" as "testing#pw" And I click on "Save Formula" Then I should see a "Formula saved" text - And the pillar data for "testing:str" should be "text1" on "sle_minion" + When I wait for "1" seconds + Then the pillar data for "testing:str" should be "text1" on "sle_minion" And the pillar data for "testing:str_def" should be "defvalue" on "sle_minion" And the pillar data for "testing:str_or_null" should be "None" on "sle_minion" And the pillar data for "testing" should not contain "str_opt" on "sle_minion" @@ -188,7 +191,8 @@ Feature: Use advanced features of Salt formulas And I enter "min_pw3" as "testing#pw_opt" And I click on "Save Formula" Then I should see a "Formula saved" text - And the pillar data for "testing:str" should be "min_text1" on "sle_minion" + When I wait for "1" seconds + Then the pillar data for "testing:str" should be "min_text1" on "sle_minion" And the pillar data for "testing:str_def" should be "min_text2" on "sle_minion" And the pillar data for "testing:str_or_null" should be "min_text3" on "sle_minion" And the pillar data for "testing:str_opt" should be "min_text4" on "sle_minion" @@ -207,7 +211,8 @@ Feature: Use advanced features of Salt formulas And I click on "Clear values" and confirm And I click on "Save Formula" Then I should see a "Formula saved" text - And the pillar data for "testing:str" should be "text1" on "sle_minion" + When I wait for "1" seconds + Then the pillar data for "testing:str" should be "text1" on "sle_minion" And the pillar data for "testing:str_def" should be "defvalue" on "sle_minion" And the pillar data for "testing:str_or_null" should be "None" on "sle_minion" And the pillar data for "testing" should not contain "str_opt" on "sle_minion" From 48e8732c38a55e71b60bc5341c00be6b62d00e1b Mon Sep 17 00:00:00 2001 From: Welder Luz Date: Mon, 22 Jan 2024 09:52:00 -0300 Subject: [PATCH 3/3] Add cucumber test for covering reboot required flag --- .../min_reboot_required_flag.feature | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 testsuite/features/secondary/min_reboot_required_flag.feature diff --git a/testsuite/features/secondary/min_reboot_required_flag.feature b/testsuite/features/secondary/min_reboot_required_flag.feature new file mode 100644 index 000000000000..73769cc59fde --- /dev/null +++ b/testsuite/features/secondary/min_reboot_required_flag.feature @@ -0,0 +1,55 @@ +# Copyright (c) 2024 SUSE LLC +# Licensed under the terms of the MIT license. + +Feature: Reboot Required Indication + + Scenario: Log in as admin user + Given I am authorized for the "Admin" section + + @sle_minion + Scenario: Trigger reboot required indication for SUSE distributions + Given I am on the Systems overview page of this "sle_minion" + When I follow "Remote Command" + And I enter "#!/bin/sh\ntouch /run/reboot-needed" as "script_body" text area + And I click on "Schedule" + Then I should see a "Remote Command has been scheduled" text + When I follow "Overview" + And I wait until I see "The system requires a reboot" text, refreshing the page + + @sle_minion + Scenario: Remove reboot required indication for SUSE distributions + Given I am on the Systems overview page of this "sle_minion" + Then I should see a "The system requires a reboot" text + When I follow "Remote Command" + And I enter "#!/bin/sh\nrm -rf /run/reboot-needed" as "script_body" text area + And I click on "Schedule" + Then I should see a "Remote Command has been scheduled" text + When I follow "Software" in the content area + And I click on "Update Package List" + Then I should see a "You have successfully scheduled a package profile refresh" text + When I follow "Details" + And I wait until I do not see "The system requires a reboot" text, refreshing the page + + @deblike_minion + Scenario: Trigger reboot required indication for Debian-like distributions + Given I am on the Systems overview page of this "deblike_minion" + When I follow "Remote Command" + And I enter "#!/bin/sh\ntouch /var/run/reboot-required" as "script_body" text area + And I click on "Schedule" + Then I should see a "Remote Command has been scheduled" text + When I follow "Overview" + And I wait until I see "The system requires a reboot" text, refreshing the page + + @deblike_minion + Scenario: Remove reboot required indication for Debian-like distributions + Given I am on the Systems overview page of this "deblike_minion" + Then I should see a "The system requires a reboot" text + When I follow "Remote Command" + And I enter "#!/bin/sh\nrm -rf /var/run/reboot-required" as "script_body" text area + And I click on "Schedule" + Then I should see a "Remote Command has been scheduled" text + When I follow "Software" in the content area + And I click on "Update Package List" + Then I should see a "You have successfully scheduled a package profile refresh" text + When I follow "Details" + And I wait until I do not see "The system requires a reboot" text, refreshing the page