Merge ~lloydwaltersj/maas:image-deployment-data into maas:master

Proposed by Jack Lloyd-Walters
Status: Merged
Approved by: Jack Lloyd-Walters
Approved revision: 29626a824458f9ec2de1b6b2d1959cc5e5fc5c4c
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~lloydwaltersj/maas:image-deployment-data
Merge into: maas:master
Diff against target: 283 lines (+95/-19)
8 files modified
src/maasserver/api/boot_resources.py (+1/-0)
src/maasserver/api/tests/test_boot_resources.py (+3/-0)
src/maasserver/models/bootresource.py (+19/-13)
src/maasserver/models/node.py (+11/-1)
src/maasserver/models/tests/test_node.py (+5/-2)
src/maasserver/websockets/handlers/bootresource.py (+26/-3)
src/maasserver/websockets/handlers/tests/test_bootresource.py (+26/-0)
src/provisioningserver/events.py (+4/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Adam Collard (community) Approve
Review via email: mp+434563@code.launchpad.net

Commit message

Add deployment information to boot resources

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1616/consoleText
COMMIT: 3a8d73cfb8628de67530edf6d73d3ebbb7c46bd8

review: Needs Fixing
Revision history for this message
Jack Lloyd-Walters (lloydwaltersj) wrote :

jenkins: !test

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1617/consoleText
COMMIT: dc80b0ccfc25d1c77d93d893f68f005e93321416

review: Needs Fixing
Revision history for this message
Adam Collard (adam-collard) :
review: Needs Fixing
d5ce368... by Jack Lloyd-Walters

Replace with events

5896d87... by Jack Lloyd-Walters

formatting and linting

fc6df1e... by Jack Lloyd-Walters

add type hinting and linting

88e1b84... by Jack Lloyd-Walters

Fix tests raising errors

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1627/consoleText
COMMIT: fc6df1e7bd9324a979cf07cb944967e892e3cc45

review: Needs Fixing
bdef783... by Jack Lloyd-Walters

fix status change test

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 88e1b84eb5da9cdadd27ea9ba2c2a020a43fbdf7

review: Approve
Revision history for this message
Adam Collard (adam-collard) :
review: Needs Fixing
Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: bdef783c5d5dd1ed96cc9ab234bb93ff5800b37a

review: Approve
6054bb3... by Jack Lloyd-Walters

remove layering violation

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 6054bb33c2141e121d0eb41a3691453f1641679d

review: Approve
600e0f7... by Jack Lloyd-Walters

remove unused column from database

4448eb7... by Jack Lloyd-Walters

fix restAPI issues

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1650/consoleText
COMMIT: 600e0f7bf05013340516e3766dbad939c806ae08

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 4448eb7bb7ee9310c4eb4b280550f365436e7093

review: Approve
Revision history for this message
Adam Collard (adam-collard) wrote :

nit picks

review: Approve
f128bd4... by Jack Lloyd-Walters

respond to comments

29626a8... by Jack Lloyd-Walters

remove erroneous code

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

UNIT TESTS
-b image-deployment-data lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 29626a824458f9ec2de1b6b2d1959cc5e5fc5c4c

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/boot_resources.py b/src/maasserver/api/boot_resources.py
2index 6100c89..770fdf1 100644
3--- a/src/maasserver/api/boot_resources.py
4+++ b/src/maasserver/api/boot_resources.py
5@@ -108,6 +108,7 @@ def boot_resource_to_dict(resource, with_sets=False):
6 "name": resource.name,
7 "architecture": resource.architecture,
8 "resource_uri": reverse("boot_resource_handler", args=[resource.id]),
9+ "last_deployed": resource.get_last_deploy(),
10 }
11 dict_representation.update(resource.extra)
12 if with_sets:
13diff --git a/src/maasserver/api/tests/test_boot_resources.py b/src/maasserver/api/tests/test_boot_resources.py
14index a8ac72c..a4e0dc5 100644
15--- a/src/maasserver/api/tests/test_boot_resources.py
16+++ b/src/maasserver/api/tests/test_boot_resources.py
17@@ -101,6 +101,9 @@ class TestHelpers(APITestCase.ForUser):
18 get_boot_resource_uri(resource),
19 dict_representation["resource_uri"],
20 )
21+ self.assertEqual(
22+ resource.get_last_deploy(), dict_representation["last_deployed"]
23+ )
24 self.assertFalse("sets" in dict_representation)
25
26 def test_boot_resource_to_dict_with_sets(self):
27diff --git a/src/maasserver/models/bootresource.py b/src/maasserver/models/bootresource.py
28index 3ec9528..7fbb860 100644
29--- a/src/maasserver/models/bootresource.py
30+++ b/src/maasserver/models/bootresource.py
31@@ -1,9 +1,9 @@
32-# Copyright 2014-2018 Canonical Ltd. This software is licensed under the
33+# Copyright 2014-2022 Canonical Ltd. This software is licensed under the
34 # GNU Affero General Public License version 3 (see the file LICENSE).
35
36 """Boot Resource."""
37
38-
39+from datetime import datetime
40 from operator import attrgetter
41
42 from django.core.exceptions import ValidationError
43@@ -16,7 +16,6 @@ from django.db.models import (
44 Prefetch,
45 Sum,
46 )
47-from django.utils import timezone
48
49 from maasserver.enum import (
50 BOOT_RESOURCE_FILE_TYPE,
51@@ -516,16 +515,6 @@ class BootResource(CleanSave, TimestampedModel):
52 """Return rtype text as displayed to the user."""
53 return BOOT_RESOURCE_TYPE_CHOICES_DICT[self.rtype]
54
55- @property
56- def last_deployed(self) -> timezone.datetime:
57- """Returns the most recent time of deplyment for an image."""
58- # Mock data: Generates a random time based on the hash of the
59- # resource name.
60- ms_py = 3153600000000
61- return timezone.datetime(2022, 6, 1) + timezone.timedelta(
62- microseconds=hash(self.name) % ms_py
63- )
64-
65 def clean(self):
66 """Validate the model.
67
68@@ -591,6 +580,23 @@ class BootResource(CleanSave, TimestampedModel):
69 return resource_set
70 return None
71
72+ def get_last_deploy(self) -> datetime:
73+ from maasserver.models.event import Event
74+ from provisioningserver.events import EVENT_TYPES
75+
76+ deploy_msg = f"deployed {self.name}/{self.architecture}"
77+ try:
78+ return (
79+ Event.objects.filter(
80+ type__name=EVENT_TYPES.IMAGE_DEPLOYED,
81+ description=deploy_msg,
82+ )
83+ .latest("created")
84+ .created
85+ )
86+ except Event.DoesNotExist:
87+ pass
88+
89 def split_arch(self):
90 return self.architecture.split("/")
91
92diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
93index e1c6923..e9766d6 100644
94--- a/src/maasserver/models/node.py
95+++ b/src/maasserver/models/node.py
96@@ -1,4 +1,4 @@
97-# Copyright 2012-2021 Canonical Ltd. This software is licensed under the
98+# Copyright 2012-2022 Canonical Ltd. This software is licensed under the
99 # GNU Affero General Public License version 3 (see the file LICENSE).
100
101 """Node objects."""
102@@ -1789,11 +1789,21 @@ class Node(CleanSave, TimestampedModel):
103 self.update_status(NODE_STATUS.DEPLOYED)
104 if self.enable_hw_sync:
105 self.last_sync = datetime.now()
106+ self.update_deployment_time()
107 self.save()
108
109 # Create a status message for DEPLOYED.
110 Event.objects.create_node_event(self, EVENT_TYPES.DEPLOYED)
111
112+ def update_deployment_time(self) -> None:
113+ from maasserver.models.event import Event
114+
115+ Event.objects.create_node_event(
116+ self,
117+ EVENT_TYPES.IMAGE_DEPLOYED,
118+ event_description=f"deployed {self.osystem}/{self.distro_series}/{self.architecture}",
119+ )
120+
121 def ip_addresses(self, ifaces=None):
122 """IP addresses allocated to this node.
123
124diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
125index 3aba41b..4cf6669 100644
126--- a/src/maasserver/models/tests/test_node.py
127+++ b/src/maasserver/models/tests/test_node.py
128@@ -4823,9 +4823,12 @@ class TestNode(MAASServerTestCase):
129 self.disable_node_query()
130 node = factory.make_Node(status=NODE_STATUS.DEPLOYING)
131 node.end_deployment()
132- event = Event.objects.get(node=node)
133+ events = Event.objects.filter(node=node)
134 self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status)
135- self.assertEqual(event.type.name, EVENT_TYPES.DEPLOYED)
136+ self.assertEqual(
137+ {event.type.name for event in events},
138+ {EVENT_TYPES.IMAGE_DEPLOYED, EVENT_TYPES.DEPLOYED},
139+ )
140
141 def test_end_deployment_sets_first_last_sync_value(self):
142 self.disable_node_query()
143diff --git a/src/maasserver/websockets/handlers/bootresource.py b/src/maasserver/websockets/handlers/bootresource.py
144index 908af1e..fa4b17c 100644
145--- a/src/maasserver/websockets/handlers/bootresource.py
146+++ b/src/maasserver/websockets/handlers/bootresource.py
147@@ -5,6 +5,8 @@
148
149
150 from collections import defaultdict
151+from datetime import datetime
152+from typing import Optional
153
154 from distro_info import UbuntuDistroInfo
155 from django.core.exceptions import ValidationError
156@@ -376,7 +378,9 @@ class BootResourceHandler(Handler):
157 count += 1
158 return count
159
160- def pick_latest_datetime(self, time, other_time):
161+ def pick_latest_datetime(
162+ self, time: datetime, other_time: datetime
163+ ) -> datetime:
164 """Return the datetime that is the latest."""
165 if time is None:
166 return other_time
167@@ -410,7 +414,9 @@ class BootResourceHandler(Handler):
168 return False
169 return True
170
171- def get_last_update_for_resources(self, resources):
172+ def get_last_update_for_resources(
173+ self, resources: list[BootResource]
174+ ) -> datetime:
175 """Return the latest updated time for all resources."""
176 last_update = None
177 for resource in resources:
178@@ -431,6 +437,19 @@ class BootResourceHandler(Handler):
179 for resource in resources
180 )
181
182+ def get_last_deployed_for_resources(
183+ self, resources: list[BootResource]
184+ ) -> Optional[datetime]:
185+ """Return the most recent deploy time for all resources."""
186+ last_deployed = None
187+ for resource in resources:
188+ this_deploy = resource.get_last_deploy()
189+ if this_deploy is not None:
190+ last_deployed = self.pick_latest_datetime(
191+ last_deployed, this_deploy
192+ )
193+ return last_deployed
194+
195 def get_progress_for_resources(self, resources):
196 """Return the overall progress for all resources."""
197 size = 0
198@@ -479,6 +498,7 @@ class BootResourceHandler(Handler):
199 number_of_nodes = self.get_number_of_nodes_for_resources(group)
200 complete = self.are_all_resources_complete(group)
201 progress = self.get_progress_for_resources(group)
202+ last_deployed = self.get_last_deployed_for_resources(group)
203
204 # Set the computed attributes on the first resource as that will
205 # be the only one returned to the UI.
206@@ -489,6 +509,7 @@ class BootResourceHandler(Handler):
207 resource.size = human_readable_bytes(unique_size)
208 resource.last_update = last_update
209 resource.number_of_nodes = number_of_nodes
210+ resource.last_deployed = last_deployed
211 resource.complete = complete
212 if not complete:
213 if progress > 0:
214@@ -599,7 +620,9 @@ class BootResourceHandler(Handler):
215 ),
216 "lastDeployed": resource.last_deployed.strftime(
217 "%a, %d %b. %Y %H:%M:%S"
218- ),
219+ )
220+ if resource.last_deployed
221+ else None,
222 }
223 for resource in self.combine_resources(
224 BootResource.objects.filter(bootloader_type=None)
225diff --git a/src/maasserver/websockets/handlers/tests/test_bootresource.py b/src/maasserver/websockets/handlers/tests/test_bootresource.py
226index d1f401b..96c773f 100644
227--- a/src/maasserver/websockets/handlers/tests/test_bootresource.py
228+++ b/src/maasserver/websockets/handlers/tests/test_bootresource.py
229@@ -364,6 +364,32 @@ class TestBootResourcePoll(MAASServerTestCase, PatchOSInfoMixin):
230 resource = response["resources"][0]
231 self.assertEqual(version, resource["title"])
232
233+ def test_shows_last_deployment_time(self) -> None:
234+ owner = factory.make_admin()
235+ handler = BootResourceHandler(owner, {}, None)
236+ resource = factory.make_usable_boot_resource(
237+ rtype=BOOT_RESOURCE_TYPE.SYNCED
238+ )
239+ os_name, series = resource.name.split("/")
240+ # The polled datetime only has granularity of order seconds
241+ start_time = datetime.datetime.now().replace(microsecond=0)
242+ node = factory.make_Node(
243+ status=NODE_STATUS.DEPLOYED,
244+ osystem=os_name,
245+ distro_series=series,
246+ architecture=resource.architecture,
247+ )
248+ node.end_deployment()
249+ response = handler.poll({})
250+ resource = response["resources"][0]
251+ self.assertIn("lastDeployed", resource)
252+ self.assertGreaterEqual(
253+ datetime.datetime.strptime(
254+ resource["lastDeployed"], "%a, %d %b. %Y %H:%M:%S"
255+ ),
256+ start_time,
257+ )
258+
259 def test_shows_number_of_nodes_deployed_for_resource(self):
260 owner = factory.make_admin()
261 handler = BootResourceHandler(owner, {}, None)
262diff --git a/src/provisioningserver/events.py b/src/provisioningserver/events.py
263index 6d6cceb..bffd6ae 100644
264--- a/src/provisioningserver/events.py
265+++ b/src/provisioningserver/events.py
266@@ -147,6 +147,7 @@ class EVENT_TYPES:
267 READY = "READY"
268 DEPLOYING = "DEPLOYING"
269 DEPLOYED = "DEPLOYED"
270+ IMAGE_DEPLOYED = "IMAGE_DEPLOYED"
271 RELEASING = "RELEASING"
272 RELEASED = "RELEASED"
273 ENTERING_RESCUE_MODE = "ENTERING_RESCUE_MODE"
274@@ -411,6 +412,9 @@ EVENT_DETAILS = {
275 EVENT_TYPES.READY: EventDetail(description="Ready", level=INFO),
276 EVENT_TYPES.DEPLOYING: EventDetail(description="Deploying", level=INFO),
277 EVENT_TYPES.DEPLOYED: EventDetail(description="Deployed", level=INFO),
278+ EVENT_TYPES.IMAGE_DEPLOYED: EventDetail(
279+ description="Image Deployed", level=INFO
280+ ),
281 EVENT_TYPES.RELEASING: EventDetail(description="Releasing", level=INFO),
282 EVENT_TYPES.RELEASED: EventDetail(description="Released", level=INFO),
283 EVENT_TYPES.ENTERING_RESCUE_MODE: EventDetail(

Subscribers

People subscribed via source and target branches