Merge ~d0ugal/maas:release-notifications-2.7 into maas:2.7

Proposed by Dougal Matthews
Status: Merged
Approved by: Adam Collard
Approved revision: 9963f27b772ccb33da83031cd2a3b2544941518b
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~d0ugal/maas:release-notifications-2.7
Merge into: maas:2.7
Diff against target: 282 lines (+243/-0)
4 files modified
src/maasserver/bootresources.py (+5/-0)
src/maasserver/models/bootsource.py (+13/-0)
src/maasserver/release_notifications.py (+124/-0)
src/maasserver/tests/test_release_notifications.py (+101/-0)
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
MAAS Lander Approve
Review via email: mp+391050@code.launchpad.net

Commit message

Add release notifications to MAAS

This change adds a new release notification service which periodically
queries the configured simplestream and checks the metadata for any
release notifications.

The notifications are created for all users and if a user dismisses the
notification it will be resurfaced every six weeks.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b release-notifications-2.7 lp:~d0ugal/maas/+git/maas into -b 2.7 lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/8399/console
COMMIT: 31025099926be9973c6c0f822288e4e6480f0356

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b release-notifications-2.7 lp:~d0ugal/maas/+git/maas into -b 2.7 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: b24f1ed88081e345d1c284bb34a2dbf8b37f1115

review: Approve
Revision history for this message
Lee Trager (ltrager) wrote :
review: Disapprove
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b release-notifications-2.7 lp:~d0ugal/maas/+git/maas into -b 2.7 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 65700e30160c4775641c70b4342cedef8824b53f

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b release-notifications-2.7 lp:~d0ugal/maas/+git/maas into -b 2.7 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: cba291a35c2ced5dd49f66674fc7519f2f17f677

review: Approve
9963f27... by Dougal Matthews

Add release notifications to MAAS

This change adds a new release notification service which periodically
queries the configured simplestream and checks the metadata for any
release notifications.

The notifications are created for all users and if a user dismisses the
notification it will be resurfaced every six weeks.

(cherry picked from commit 2a342196f366aea386f3bc341f71786c43c94e0d)

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b release-notifications-2.7 lp:~d0ugal/maas/+git/maas into -b 2.7 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 9963f27b772ccb33da83031cd2a3b2544941518b

review: Approve
Revision history for this message
Lee Trager (ltrager) wrote :

LGTM!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/bootresources.py b/src/maasserver/bootresources.py
2index 3cf6c30..3cc3689 100644
3--- a/src/maasserver/bootresources.py
4+++ b/src/maasserver/bootresources.py
5@@ -65,6 +65,7 @@ from maasserver.models import (
6 Event,
7 LargeFile,
8 )
9+from maasserver.release_notifications import ReleaseNotifications
10 from maasserver.rpc import getAllClients
11 from maasserver.utils import (
12 absolute_reverse,
13@@ -1129,6 +1130,10 @@ class BootResourceRepoWriter(BasicMirrorWriter):
14 % (product_name, supported_version)
15 )
16 return
17+ if item["ftype"] == "notifications":
18+ ReleaseNotifications(
19+ data["release_notification"]
20+ ).maybe_check_release_notifications()
21 if (
22 item["ftype"] == BOOT_RESOURCE_FILE_TYPE.ROOT_IMAGE
23 and BOOT_RESOURCE_FILE_TYPE.SQUASHFS_IMAGE in items.keys()
24diff --git a/src/maasserver/models/bootsource.py b/src/maasserver/models/bootsource.py
25index 1a2717c..73526e6 100644
26--- a/src/maasserver/models/bootsource.py
27+++ b/src/maasserver/models/bootsource.py
28@@ -115,4 +115,17 @@ class BootSource(CleanSave, TimestampedModel):
29 "labels": ["*"],
30 }
31 )
32+ # Always download all release notifications from the stream.
33+ for release_notification in self.bootsourcecache_set.filter(
34+ release="notifications"
35+ ):
36+ data["selections"].append(
37+ {
38+ "os": release_notification.os,
39+ "release": release_notification.release,
40+ "arches": [release_notification.arch],
41+ "subarches": ["*"],
42+ "labels": ["*"],
43+ }
44+ )
45 return data
46diff --git a/src/maasserver/release_notifications.py b/src/maasserver/release_notifications.py
47new file mode 100644
48index 0000000..808c886
49--- /dev/null
50+++ b/src/maasserver/release_notifications.py
51@@ -0,0 +1,124 @@
52+# Copyright 2020 Canonical Ltd. This software is licensed under the
53+# GNU Affero General Public License version 3 (see the file LICENSE).
54+
55+"""Release Notifications for MAAS
56+
57+Checks for new MAAS releases in the images.maas.io stream and creates
58+notifications for the user.
59+"""
60+
61+from datetime import datetime, timedelta
62+
63+import attr
64+from twisted.internet.defer import inlineCallbacks
65+
66+from maasserver.models import Config, Notification
67+from maasserver.utils.orm import transactional
68+from maasserver.utils.threads import deferToDatabase
69+from provisioningserver.logger import get_maas_logger, LegacyLogger
70+from provisioningserver.utils import version
71+from provisioningserver.utils.twisted import asynchronous
72+
73+maaslog = get_maas_logger("release-notifications")
74+log = LegacyLogger()
75+
76+RELEASE_NOTIFICATION_SERVICE_PERIOD = timedelta(hours=24)
77+RESURFACE_AFTER = timedelta(weeks=6)
78+RELEASE_NOTIFICATION_IDENT = "release_notification"
79+
80+
81+@attr.s()
82+class ReleaseNotification:
83+ maas_version = attr.ib()
84+ message = attr.ib()
85+ # Product version number
86+ version = attr.ib()
87+
88+
89+class NoReleasenotification(LookupError):
90+ pass
91+
92+
93+def notification_available(notification_version, maas_version=None):
94+ current_version = version.get_version_tuple(
95+ maas_version or version.get_maas_version()
96+ )
97+ log.debug(f"Current MAAS version: {repr(current_version)}")
98+ log.debug(f"Notification version: {notification_version}")
99+
100+ notification_version_tuple = version.get_version_tuple(
101+ notification_version
102+ )
103+ return notification_version_tuple > current_version
104+
105+
106+@transactional
107+def ensure_notification_exists(message, resurface_after=RESURFACE_AFTER):
108+
109+ notification, created = Notification.objects.get_or_create(
110+ ident=RELEASE_NOTIFICATION_IDENT,
111+ defaults={
112+ "message": message,
113+ "category": "info",
114+ "users": True,
115+ "admins": True,
116+ },
117+ )
118+ if created:
119+ # Since this is a new notification there is nothing else to do. It will
120+ # now be showen to all users
121+ return
122+
123+ # Only the message will be updated in release notifications.
124+ if notification.message != message:
125+ notification.message = message
126+ notification.save()
127+ # If the notification is being updated, we want to resuface it for all
128+ # users by deleting their dismissals
129+ notification.notificationdismissal_set.all().delete()
130+ return
131+
132+ if (datetime.now() - notification.updated) > resurface_after:
133+ notification.notificationdismissal_set.all().delete()
134+ # We are using save to update the `updated` field on the Notification
135+ # model. This behaviour will be changed in MAAS 2.9 and above when
136+ # we can update the schema and make the notification dismissal a
137+ # timestamped model
138+ notification.save(force_update=True)
139+
140+
141+class ReleaseNotifications:
142+ def __init__(self, release_notification):
143+ self.release_notification = ReleaseNotification(**release_notification)
144+
145+ def maybe_check_release_notifications(self):
146+ def check_config():
147+ return Config.objects.get_config("release_notifications")
148+
149+ d = deferToDatabase(transactional(check_config))
150+ d.addCallback(self.check_notifications)
151+ d.addErrback(log.err, "Failure checking release notifications.")
152+ return d
153+
154+ def cleanup_notification(self):
155+ maaslog.debug("Cleaning up notifications")
156+ Notification.objects.filter(ident=RELEASE_NOTIFICATION_IDENT).delete()
157+
158+ @asynchronous
159+ @inlineCallbacks
160+ def check_notifications(self, notifications_enabled):
161+
162+ if not notifications_enabled:
163+ maaslog.debug("Release notifications are disabled")
164+ # Notifications are disabled, we can delete any that currently exist.
165+ yield deferToDatabase(transactional(self.cleanup_notification))
166+ return
167+
168+ if not notification_available(self.release_notification.maas_version):
169+ maaslog.debug("No new release notifications available")
170+ return
171+
172+ maaslog.debug("Notification to display")
173+ yield deferToDatabase(
174+ ensure_notification_exists, self.release_notification.message
175+ )
176diff --git a/src/maasserver/tests/test_release_notifications.py b/src/maasserver/tests/test_release_notifications.py
177new file mode 100644
178index 0000000..c63f8b4
179--- /dev/null
180+++ b/src/maasserver/tests/test_release_notifications.py
181@@ -0,0 +1,101 @@
182+import datetime
183+
184+from maasserver import release_notifications
185+from maasserver.models import Notification
186+from maasserver.testing.testcase import (
187+ MAASServerTestCase,
188+ MAASTransactionServerTestCase,
189+)
190+
191+
192+class TestVersionCheck(MAASServerTestCase):
193+ def test_new_release_available(self):
194+ current_version = "2.8.1"
195+ notification_maas_version = "2.9.0"
196+ self.assertTrue(
197+ release_notifications.notification_available(
198+ notification_maas_version, current_version
199+ )
200+ )
201+
202+ def test_already_on_latest_version(self):
203+ current_version = "2.9.0"
204+ notification_maas_version = "2.9.0"
205+ self.assertFalse(
206+ release_notifications.notification_available(
207+ notification_maas_version, current_version
208+ )
209+ )
210+
211+ def test_notification_is_old(self):
212+ current_version = "2.9.0"
213+ notification_maas_version = "2.8.0"
214+ self.assertFalse(
215+ release_notifications.notification_available(
216+ notification_maas_version, current_version
217+ )
218+ )
219+
220+
221+class TestReleaseNotification(MAASTransactionServerTestCase):
222+ def _get_release_notifications(self):
223+ return Notification.objects.filter(
224+ ident=release_notifications.RELEASE_NOTIFICATION_IDENT
225+ )
226+
227+ def test_create_notification(self):
228+ """Test creating a new release notification"""
229+
230+ message = "Upgrade to version 3.14 today"
231+ release_notifications.ensure_notification_exists(message)
232+ original_notification = self._get_release_notifications().get()
233+
234+ release_notifications.ensure_notification_exists(message)
235+
236+ self.assertEqual(self._get_release_notifications().count(), 1)
237+
238+ notification = self._get_release_notifications().get()
239+ self.assertEqual(message, notification.message)
240+ self.assertEqual(notification.created, original_notification.created)
241+ # As the notification hasn't changed, we don't want the updated datetime
242+ # to be changed.
243+ self.assertEqual(notification.updated, original_notification.updated)
244+
245+ def test_updating_notification(self):
246+ """Test updating a release notification with a new message"""
247+
248+ message = "Upgrade to version 3.1 today"
249+ release_notifications.ensure_notification_exists(message)
250+ original_notification = self._get_release_notifications().get()
251+
252+ message = "Upgrade to version 3.2 today"
253+ release_notifications.ensure_notification_exists(message)
254+
255+ self.assertEqual(self._get_release_notifications().count(), 1)
256+
257+ notification = self._get_release_notifications().get()
258+ self.assertEqual(message, notification.message)
259+ self.assertEqual(notification.created, original_notification.created)
260+ self.assertGreater(notification.updated, original_notification.updated)
261+
262+ def test_resurface_notification(self):
263+ """Test resufacing release notifications that were dismissed"""
264+
265+ message = "Upgrade to version 3.14 today"
266+ release_notifications.ensure_notification_exists(message)
267+
268+ # Manually update the notification to appear in the past.
269+ seven_weeks_ago = datetime.datetime.now() - datetime.timedelta(weeks=7)
270+ self._get_release_notifications().update(
271+ updated=seven_weeks_ago, created=seven_weeks_ago
272+ )
273+ original_notification = self._get_release_notifications().get()
274+
275+ release_notifications.ensure_notification_exists(message)
276+
277+ self.assertEqual(self._get_release_notifications().count(), 1)
278+
279+ notification = self._get_release_notifications().get()
280+ self.assertEqual(message, notification.message)
281+ self.assertEqual(notification.created, original_notification.created)
282+ self.assertGreater(notification.updated, original_notification.updated)

Subscribers

People subscribed via source and target branches