diff --git a/java/code/src/com/redhat/rhn/domain/notification/NotificationMessage.java b/java/code/src/com/redhat/rhn/domain/notification/NotificationMessage.java index 5466b26f94c3..e4398979a1d1 100644 --- a/java/code/src/com/redhat/rhn/domain/notification/NotificationMessage.java +++ b/java/code/src/com/redhat/rhn/domain/notification/NotificationMessage.java @@ -25,6 +25,7 @@ import com.redhat.rhn.domain.notification.types.PaygAuthenticationUpdateFailed; import com.redhat.rhn.domain.notification.types.StateApplyFailed; import com.redhat.rhn.domain.notification.types.SubscriptionWarning; +import com.redhat.rhn.domain.notification.types.UpdateAvailable; import com.google.gson.Gson; @@ -134,7 +135,9 @@ public NotificationData getNotificationData() { return new Gson().fromJson(getData(), EndOfLifePeriod.class); case SubscriptionWarning: return new Gson().fromJson(getData(), SubscriptionWarning.class); - default: throw new RuntimeException("should not happen!"); + case UpdateAvailable: + return new Gson().fromJson(getData(), UpdateAvailable.class); + default: throw new RuntimeException("Notification type not found"); } } @@ -153,6 +156,7 @@ public String getTypeAsString() { case PaygAuthenticationUpdateFailed: return "PAYG refresh failed"; case EndOfLifePeriod: return "End of Life Period"; case SubscriptionWarning: return "Subscription Warning"; + case UpdateAvailable: return "Updates are Available"; default: return getType().name(); } } diff --git a/java/code/src/com/redhat/rhn/domain/notification/types/NotificationType.java b/java/code/src/com/redhat/rhn/domain/notification/types/NotificationType.java index cdcb45018608..99ca32a3548d 100644 --- a/java/code/src/com/redhat/rhn/domain/notification/types/NotificationType.java +++ b/java/code/src/com/redhat/rhn/domain/notification/types/NotificationType.java @@ -26,4 +26,5 @@ public enum NotificationType { PaygAuthenticationUpdateFailed, EndOfLifePeriod, SubscriptionWarning, + UpdateAvailable, } diff --git a/java/code/src/com/redhat/rhn/domain/notification/types/UpdateAvailable.java b/java/code/src/com/redhat/rhn/domain/notification/types/UpdateAvailable.java new file mode 100644 index 000000000000..acb3ed4857b9 --- /dev/null +++ b/java/code/src/com/redhat/rhn/domain/notification/types/UpdateAvailable.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.rhn.domain.notification.types; + +import com.redhat.rhn.common.conf.ConfigDefaults; +import com.redhat.rhn.common.localization.LocalizationService; +import com.redhat.rhn.domain.notification.NotificationMessage; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +/** + * Notification data for an update being available for the server. + */ +public class UpdateAvailable implements NotificationData { + + private static final LocalizationService LOCALIZATION_SERVICE = LocalizationService.getInstance(); + private static final Logger LOG = LogManager.getLogger(UpdateAvailable.class); + private static final String UYUNI_PATCH_REPO = "systemsmanagement_Uyuni_Stable_Patches"; + private static final String UYUNI_UPDATE_REPO = "systemsmanagement_Uyuni_Stable"; + + private final boolean mgr = !ConfigDefaults.get().isUyuni(); + private final String version = StringUtils.substringBeforeLast(ConfigDefaults.get().getProductVersion(), "."); + private final Runtime runtime; + + /** + * Constructor allowing to pass the runtime as argument. + * + * @param runtimeIn runtime object for command execution + */ + public UpdateAvailable(Runtime runtimeIn) { + this.runtime = runtimeIn; + } + + /** + * returns true if there are updates available. + * + * @return boolean + **/ + public boolean updateAvailable() { + boolean hasUpdates = false; + String repo = mgr ? "SLE-Module-SUSE-Manager-Server-" + version + "-Updates" : UYUNI_PATCH_REPO; + + try { + Process patchProc = runtime.exec(new String[]{"/bin/bash", "-c", + "LC_ALL=C zypper lp -r " + repo + " | grep 'applicable patch'"}); + patchProc.waitFor(); + // 0 here means there are patches + hasUpdates = (0 == patchProc.exitValue()); + if (!hasUpdates && !mgr) { + // Check for updates on uyuni when there are no patches + Process updateProc = runtime.exec(new String[]{"/bin/bash", "-c", + "LC_ALL=C zypper lu -r " + UYUNI_UPDATE_REPO + " | grep 'Available Version'"}); + updateProc.waitFor(); + hasUpdates = (0 == updateProc.exitValue()); + } + } + catch (IOException e) { + LOG.warn("Unable to check for updates", e); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("Wait for update check was interrupted", e); + } + return hasUpdates; + } + + @Override + public NotificationMessage.NotificationMessageSeverity getSeverity() { + return NotificationMessage.NotificationMessageSeverity.warning; + } + + @Override + public NotificationType getType() { + return NotificationType.UpdateAvailable; + } + + @Override + public String getSummary() { + return LOCALIZATION_SERVICE.getMessage("notification.updateavailable.summary"); + } + + @Override + public String getDetails() { + String url = mgr ? + "https://www.suse.com/releasenotes/x86_64/SUSE-MANAGER/" + version + "/index.html" : + "https://www.uyuni-project.org/pages/stable-version.html"; + return LOCALIZATION_SERVICE.getMessage("notification.updateavailable.detail", url); + } +} diff --git a/java/code/src/com/redhat/rhn/domain/notification/types/test/UpdateAvailableTest.java b/java/code/src/com/redhat/rhn/domain/notification/types/test/UpdateAvailableTest.java new file mode 100644 index 000000000000..d5c297799380 --- /dev/null +++ b/java/code/src/com/redhat/rhn/domain/notification/types/test/UpdateAvailableTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.rhn.domain.notification.types.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.redhat.rhn.common.conf.ConfigDefaults; +import com.redhat.rhn.domain.notification.NotificationMessage; +import com.redhat.rhn.domain.notification.types.NotificationType; +import com.redhat.rhn.domain.notification.types.UpdateAvailable; +import com.redhat.rhn.testing.MockObjectTestCase; + +import org.jmock.Expectations; +import org.jmock.imposters.ByteBuddyClassImposteriser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UpdateAvailableTest extends MockObjectTestCase { + + private Runtime runtimeMock; + private Process processMock; + + @BeforeEach + public void setUp() { + setImposteriser(ByteBuddyClassImposteriser.INSTANCE); + runtimeMock = mock(Runtime.class); + processMock = mock(Process.class); + } + + @Test + public void testPropertiesAndStrings() { + UpdateAvailable notification = new UpdateAvailable(runtimeMock); + assertEquals(NotificationType.UpdateAvailable, notification.getType()); + assertEquals(NotificationMessage.NotificationMessageSeverity.warning, notification.getSeverity()); + assertEquals("Updates are available.", notification.getSummary()); + if (ConfigDefaults.get().isUyuni()) { + assertEquals("A new update for Uyuni is now available. For further details, please refer to the " + + "release notes.", + notification.getDetails()); + } + else { + assertEquals("A new update for SUSE Manager is now available. For further details, please refer to the " + + "release " + + "notes.", notification.getDetails()); + } + } + + @Test + public void testUpdatesAvailable() throws Exception { + // Return 0 on all invocations of exec() -> an update is available + checking(new Expectations() {{ + allowing(runtimeMock).exec(with(any(String[].class))); + will(returnValue(processMock)); + allowing(processMock).waitFor(); + allowing(processMock).exitValue(); + will(returnValue(0)); + }}); + + UpdateAvailable notification = new UpdateAvailable(runtimeMock); + assertTrue(notification.updateAvailable()); + } + + @Test + public void testNoUpdatesAvailable() throws Exception { + // Return 1 on all invocations of exec() -> no update is available + checking(new Expectations() {{ + allowing(runtimeMock).exec(with(any(String[].class))); + will(returnValue(processMock)); + allowing(processMock).waitFor(); + allowing(processMock).exitValue(); + will(returnValue(1)); + }}); + + UpdateAvailable notification = new UpdateAvailable(runtimeMock); + assertFalse(notification.updateAvailable()); + } +} diff --git a/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml b/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml index 9c220e7a1657..975b639f13ff 100644 --- a/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml +++ b/java/code/src/com/redhat/rhn/frontend/strings/java/StringResource_en_US.xml @@ -9424,6 +9424,12 @@ For a detailed analysis, please refer to the log files. You have subscriptions that are expiring soon or already expired. Please check the <a href="/rhn/manager/subscription-matching">Subscription Matching</a> page. + + Updates are available. + + + A new update for @@PRODUCT_NAME@@ is now available. For further details, please refer to the <a href="{0}">release notes<a>. + @@PRODUCT_NAME@@ {0} has reached end of life diff --git a/java/code/src/com/redhat/rhn/taskomatic/task/DailySummary.java b/java/code/src/com/redhat/rhn/taskomatic/task/DailySummary.java index 9fa161e4847c..5e970a7a2674 100644 --- a/java/code/src/com/redhat/rhn/taskomatic/task/DailySummary.java +++ b/java/code/src/com/redhat/rhn/taskomatic/task/DailySummary.java @@ -24,6 +24,7 @@ import com.redhat.rhn.domain.notification.UserNotificationFactory; import com.redhat.rhn.domain.notification.types.EndOfLifePeriod; import com.redhat.rhn.domain.notification.types.SubscriptionWarning; +import com.redhat.rhn.domain.notification.types.UpdateAvailable; import com.redhat.rhn.domain.org.OrgFactory; import com.redhat.rhn.domain.role.RoleFactory; import com.redhat.rhn.frontend.dto.ActionMessage; @@ -85,6 +86,7 @@ public String getConfigNamespace() { @Override public void execute(JobExecutionContext ctxIn) { + processUpdateAvailableNotification(); processEndOfLifeNotification(); processSubscriptionWarningNotification(); @@ -139,6 +141,16 @@ private void processSubscriptionWarningNotification() { } } + private void processUpdateAvailableNotification() { + UpdateAvailable uan = new UpdateAvailable(Runtime.getRuntime()); + if (uan.updateAvailable()) { + NotificationMessage notificationMessage = + UserNotificationFactory.createNotificationMessage(uan); + UserNotificationFactory.storeNotificationMessageFor(notificationMessage, + Collections.singleton(RoleFactory.SAT_ADMIN), Optional.empty()); + } + } + private void processEmails() { SelectMode m = ModeFactory.getMode(TaskConstants.MODE_NAME, TaskConstants.TASK_QUERY_DAILY_SUMMARY_QUEUE); diff --git a/java/conf/rhn_java.conf b/java/conf/rhn_java.conf index 73e991186233..0ff252b7690f 100644 --- a/java/conf/rhn_java.conf +++ b/java/conf/rhn_java.conf @@ -217,7 +217,7 @@ java.kiwi_os_image_building_enabled = true java.notifications_lifetime = 30 # Configure the disablement of notification messages by type - example disabling all notification types -#java.notifications_type_disabled = OnboardingFailed,ChannelSyncFailed,ChannelSyncFinished,CreateBootstrapRepoFailed,StateApplyFailed +#java.notifications_type_disabled = OnboardingFailed,ChannelSyncFailed,ChannelSyncFinished,CreateBootstrapRepoFailed,StateApplyFailed,UpdateAvailable,SubscriptionWarning java.notifications_type_disabled = ChannelSyncFinished # Maximal number of parallel connections to refresh from SCC diff --git a/java/spacewalk-java.changes.mseidl.update-notification b/java/spacewalk-java.changes.mseidl.update-notification new file mode 100644 index 000000000000..79e5cd4614bc --- /dev/null +++ b/java/spacewalk-java.changes.mseidl.update-notification @@ -0,0 +1 @@ +- Shows a notification when an update for Uyuni is available diff --git a/web/html/src/manager/notifications/notification-messages.tsx b/web/html/src/manager/notifications/notification-messages.tsx index f0689b6f438c..bb45aa9b54da 100644 --- a/web/html/src/manager/notifications/notification-messages.tsx +++ b/web/html/src/manager/notifications/notification-messages.tsx @@ -56,6 +56,10 @@ const _MESSAGE_TYPE = { id: "SubscriptionWarning", text: t("Subscription Warning"), }, + UpdateAvailable: { + id: "UpdateAvailable", + text: t("Update Notification"), + }, }; function reloadData(dataUrlSlice: string) { diff --git a/web/spacewalk-web.changes.mseidl.update-notification b/web/spacewalk-web.changes.mseidl.update-notification new file mode 100644 index 000000000000..79e5cd4614bc --- /dev/null +++ b/web/spacewalk-web.changes.mseidl.update-notification @@ -0,0 +1 @@ +- Shows a notification when an update for Uyuni is available