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
diff --git a/src/maasserver/api/boot_resources.py b/src/maasserver/api/boot_resources.py
index 6100c89..770fdf1 100644
--- a/src/maasserver/api/boot_resources.py
+++ b/src/maasserver/api/boot_resources.py
@@ -108,6 +108,7 @@ def boot_resource_to_dict(resource, with_sets=False):
108 "name": resource.name,108 "name": resource.name,
109 "architecture": resource.architecture,109 "architecture": resource.architecture,
110 "resource_uri": reverse("boot_resource_handler", args=[resource.id]),110 "resource_uri": reverse("boot_resource_handler", args=[resource.id]),
111 "last_deployed": resource.get_last_deploy(),
111 }112 }
112 dict_representation.update(resource.extra)113 dict_representation.update(resource.extra)
113 if with_sets:114 if with_sets:
diff --git a/src/maasserver/api/tests/test_boot_resources.py b/src/maasserver/api/tests/test_boot_resources.py
index a8ac72c..a4e0dc5 100644
--- a/src/maasserver/api/tests/test_boot_resources.py
+++ b/src/maasserver/api/tests/test_boot_resources.py
@@ -101,6 +101,9 @@ class TestHelpers(APITestCase.ForUser):
101 get_boot_resource_uri(resource),101 get_boot_resource_uri(resource),
102 dict_representation["resource_uri"],102 dict_representation["resource_uri"],
103 )103 )
104 self.assertEqual(
105 resource.get_last_deploy(), dict_representation["last_deployed"]
106 )
104 self.assertFalse("sets" in dict_representation)107 self.assertFalse("sets" in dict_representation)
105108
106 def test_boot_resource_to_dict_with_sets(self):109 def test_boot_resource_to_dict_with_sets(self):
diff --git a/src/maasserver/models/bootresource.py b/src/maasserver/models/bootresource.py
index 3ec9528..7fbb860 100644
--- a/src/maasserver/models/bootresource.py
+++ b/src/maasserver/models/bootresource.py
@@ -1,9 +1,9 @@
1# Copyright 2014-2018 Canonical Ltd. This software is licensed under the1# Copyright 2014-2022 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Boot Resource."""4"""Boot Resource."""
55
66from datetime import datetime
7from operator import attrgetter7from operator import attrgetter
88
9from django.core.exceptions import ValidationError9from django.core.exceptions import ValidationError
@@ -16,7 +16,6 @@ from django.db.models import (
16 Prefetch,16 Prefetch,
17 Sum,17 Sum,
18)18)
19from django.utils import timezone
2019
21from maasserver.enum import (20from maasserver.enum import (
22 BOOT_RESOURCE_FILE_TYPE,21 BOOT_RESOURCE_FILE_TYPE,
@@ -516,16 +515,6 @@ class BootResource(CleanSave, TimestampedModel):
516 """Return rtype text as displayed to the user."""515 """Return rtype text as displayed to the user."""
517 return BOOT_RESOURCE_TYPE_CHOICES_DICT[self.rtype]516 return BOOT_RESOURCE_TYPE_CHOICES_DICT[self.rtype]
518517
519 @property
520 def last_deployed(self) -> timezone.datetime:
521 """Returns the most recent time of deplyment for an image."""
522 # Mock data: Generates a random time based on the hash of the
523 # resource name.
524 ms_py = 3153600000000
525 return timezone.datetime(2022, 6, 1) + timezone.timedelta(
526 microseconds=hash(self.name) % ms_py
527 )
528
529 def clean(self):518 def clean(self):
530 """Validate the model.519 """Validate the model.
531520
@@ -591,6 +580,23 @@ class BootResource(CleanSave, TimestampedModel):
591 return resource_set580 return resource_set
592 return None581 return None
593582
583 def get_last_deploy(self) -> datetime:
584 from maasserver.models.event import Event
585 from provisioningserver.events import EVENT_TYPES
586
587 deploy_msg = f"deployed {self.name}/{self.architecture}"
588 try:
589 return (
590 Event.objects.filter(
591 type__name=EVENT_TYPES.IMAGE_DEPLOYED,
592 description=deploy_msg,
593 )
594 .latest("created")
595 .created
596 )
597 except Event.DoesNotExist:
598 pass
599
594 def split_arch(self):600 def split_arch(self):
595 return self.architecture.split("/")601 return self.architecture.split("/")
596602
diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
index e1c6923..e9766d6 100644
--- a/src/maasserver/models/node.py
+++ b/src/maasserver/models/node.py
@@ -1,4 +1,4 @@
1# Copyright 2012-2021 Canonical Ltd. This software is licensed under the1# Copyright 2012-2022 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Node objects."""4"""Node objects."""
@@ -1789,11 +1789,21 @@ class Node(CleanSave, TimestampedModel):
1789 self.update_status(NODE_STATUS.DEPLOYED)1789 self.update_status(NODE_STATUS.DEPLOYED)
1790 if self.enable_hw_sync:1790 if self.enable_hw_sync:
1791 self.last_sync = datetime.now()1791 self.last_sync = datetime.now()
1792 self.update_deployment_time()
1792 self.save()1793 self.save()
17931794
1794 # Create a status message for DEPLOYED.1795 # Create a status message for DEPLOYED.
1795 Event.objects.create_node_event(self, EVENT_TYPES.DEPLOYED)1796 Event.objects.create_node_event(self, EVENT_TYPES.DEPLOYED)
17961797
1798 def update_deployment_time(self) -> None:
1799 from maasserver.models.event import Event
1800
1801 Event.objects.create_node_event(
1802 self,
1803 EVENT_TYPES.IMAGE_DEPLOYED,
1804 event_description=f"deployed {self.osystem}/{self.distro_series}/{self.architecture}",
1805 )
1806
1797 def ip_addresses(self, ifaces=None):1807 def ip_addresses(self, ifaces=None):
1798 """IP addresses allocated to this node.1808 """IP addresses allocated to this node.
17991809
diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
index 3aba41b..4cf6669 100644
--- a/src/maasserver/models/tests/test_node.py
+++ b/src/maasserver/models/tests/test_node.py
@@ -4823,9 +4823,12 @@ class TestNode(MAASServerTestCase):
4823 self.disable_node_query()4823 self.disable_node_query()
4824 node = factory.make_Node(status=NODE_STATUS.DEPLOYING)4824 node = factory.make_Node(status=NODE_STATUS.DEPLOYING)
4825 node.end_deployment()4825 node.end_deployment()
4826 event = Event.objects.get(node=node)4826 events = Event.objects.filter(node=node)
4827 self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status)4827 self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status)
4828 self.assertEqual(event.type.name, EVENT_TYPES.DEPLOYED)4828 self.assertEqual(
4829 {event.type.name for event in events},
4830 {EVENT_TYPES.IMAGE_DEPLOYED, EVENT_TYPES.DEPLOYED},
4831 )
48294832
4830 def test_end_deployment_sets_first_last_sync_value(self):4833 def test_end_deployment_sets_first_last_sync_value(self):
4831 self.disable_node_query()4834 self.disable_node_query()
diff --git a/src/maasserver/websockets/handlers/bootresource.py b/src/maasserver/websockets/handlers/bootresource.py
index 908af1e..fa4b17c 100644
--- a/src/maasserver/websockets/handlers/bootresource.py
+++ b/src/maasserver/websockets/handlers/bootresource.py
@@ -5,6 +5,8 @@
55
66
7from collections import defaultdict7from collections import defaultdict
8from datetime import datetime
9from typing import Optional
810
9from distro_info import UbuntuDistroInfo11from distro_info import UbuntuDistroInfo
10from django.core.exceptions import ValidationError12from django.core.exceptions import ValidationError
@@ -376,7 +378,9 @@ class BootResourceHandler(Handler):
376 count += 1378 count += 1
377 return count379 return count
378380
379 def pick_latest_datetime(self, time, other_time):381 def pick_latest_datetime(
382 self, time: datetime, other_time: datetime
383 ) -> datetime:
380 """Return the datetime that is the latest."""384 """Return the datetime that is the latest."""
381 if time is None:385 if time is None:
382 return other_time386 return other_time
@@ -410,7 +414,9 @@ class BootResourceHandler(Handler):
410 return False414 return False
411 return True415 return True
412416
413 def get_last_update_for_resources(self, resources):417 def get_last_update_for_resources(
418 self, resources: list[BootResource]
419 ) -> datetime:
414 """Return the latest updated time for all resources."""420 """Return the latest updated time for all resources."""
415 last_update = None421 last_update = None
416 for resource in resources:422 for resource in resources:
@@ -431,6 +437,19 @@ class BootResourceHandler(Handler):
431 for resource in resources437 for resource in resources
432 )438 )
433439
440 def get_last_deployed_for_resources(
441 self, resources: list[BootResource]
442 ) -> Optional[datetime]:
443 """Return the most recent deploy time for all resources."""
444 last_deployed = None
445 for resource in resources:
446 this_deploy = resource.get_last_deploy()
447 if this_deploy is not None:
448 last_deployed = self.pick_latest_datetime(
449 last_deployed, this_deploy
450 )
451 return last_deployed
452
434 def get_progress_for_resources(self, resources):453 def get_progress_for_resources(self, resources):
435 """Return the overall progress for all resources."""454 """Return the overall progress for all resources."""
436 size = 0455 size = 0
@@ -479,6 +498,7 @@ class BootResourceHandler(Handler):
479 number_of_nodes = self.get_number_of_nodes_for_resources(group)498 number_of_nodes = self.get_number_of_nodes_for_resources(group)
480 complete = self.are_all_resources_complete(group)499 complete = self.are_all_resources_complete(group)
481 progress = self.get_progress_for_resources(group)500 progress = self.get_progress_for_resources(group)
501 last_deployed = self.get_last_deployed_for_resources(group)
482502
483 # Set the computed attributes on the first resource as that will503 # Set the computed attributes on the first resource as that will
484 # be the only one returned to the UI.504 # be the only one returned to the UI.
@@ -489,6 +509,7 @@ class BootResourceHandler(Handler):
489 resource.size = human_readable_bytes(unique_size)509 resource.size = human_readable_bytes(unique_size)
490 resource.last_update = last_update510 resource.last_update = last_update
491 resource.number_of_nodes = number_of_nodes511 resource.number_of_nodes = number_of_nodes
512 resource.last_deployed = last_deployed
492 resource.complete = complete513 resource.complete = complete
493 if not complete:514 if not complete:
494 if progress > 0:515 if progress > 0:
@@ -599,7 +620,9 @@ class BootResourceHandler(Handler):
599 ),620 ),
600 "lastDeployed": resource.last_deployed.strftime(621 "lastDeployed": resource.last_deployed.strftime(
601 "%a, %d %b. %Y %H:%M:%S"622 "%a, %d %b. %Y %H:%M:%S"
602 ),623 )
624 if resource.last_deployed
625 else None,
603 }626 }
604 for resource in self.combine_resources(627 for resource in self.combine_resources(
605 BootResource.objects.filter(bootloader_type=None)628 BootResource.objects.filter(bootloader_type=None)
diff --git a/src/maasserver/websockets/handlers/tests/test_bootresource.py b/src/maasserver/websockets/handlers/tests/test_bootresource.py
index d1f401b..96c773f 100644
--- a/src/maasserver/websockets/handlers/tests/test_bootresource.py
+++ b/src/maasserver/websockets/handlers/tests/test_bootresource.py
@@ -364,6 +364,32 @@ class TestBootResourcePoll(MAASServerTestCase, PatchOSInfoMixin):
364 resource = response["resources"][0]364 resource = response["resources"][0]
365 self.assertEqual(version, resource["title"])365 self.assertEqual(version, resource["title"])
366366
367 def test_shows_last_deployment_time(self) -> None:
368 owner = factory.make_admin()
369 handler = BootResourceHandler(owner, {}, None)
370 resource = factory.make_usable_boot_resource(
371 rtype=BOOT_RESOURCE_TYPE.SYNCED
372 )
373 os_name, series = resource.name.split("/")
374 # The polled datetime only has granularity of order seconds
375 start_time = datetime.datetime.now().replace(microsecond=0)
376 node = factory.make_Node(
377 status=NODE_STATUS.DEPLOYED,
378 osystem=os_name,
379 distro_series=series,
380 architecture=resource.architecture,
381 )
382 node.end_deployment()
383 response = handler.poll({})
384 resource = response["resources"][0]
385 self.assertIn("lastDeployed", resource)
386 self.assertGreaterEqual(
387 datetime.datetime.strptime(
388 resource["lastDeployed"], "%a, %d %b. %Y %H:%M:%S"
389 ),
390 start_time,
391 )
392
367 def test_shows_number_of_nodes_deployed_for_resource(self):393 def test_shows_number_of_nodes_deployed_for_resource(self):
368 owner = factory.make_admin()394 owner = factory.make_admin()
369 handler = BootResourceHandler(owner, {}, None)395 handler = BootResourceHandler(owner, {}, None)
diff --git a/src/provisioningserver/events.py b/src/provisioningserver/events.py
index 6d6cceb..bffd6ae 100644
--- a/src/provisioningserver/events.py
+++ b/src/provisioningserver/events.py
@@ -147,6 +147,7 @@ class EVENT_TYPES:
147 READY = "READY"147 READY = "READY"
148 DEPLOYING = "DEPLOYING"148 DEPLOYING = "DEPLOYING"
149 DEPLOYED = "DEPLOYED"149 DEPLOYED = "DEPLOYED"
150 IMAGE_DEPLOYED = "IMAGE_DEPLOYED"
150 RELEASING = "RELEASING"151 RELEASING = "RELEASING"
151 RELEASED = "RELEASED"152 RELEASED = "RELEASED"
152 ENTERING_RESCUE_MODE = "ENTERING_RESCUE_MODE"153 ENTERING_RESCUE_MODE = "ENTERING_RESCUE_MODE"
@@ -411,6 +412,9 @@ EVENT_DETAILS = {
411 EVENT_TYPES.READY: EventDetail(description="Ready", level=INFO),412 EVENT_TYPES.READY: EventDetail(description="Ready", level=INFO),
412 EVENT_TYPES.DEPLOYING: EventDetail(description="Deploying", level=INFO),413 EVENT_TYPES.DEPLOYING: EventDetail(description="Deploying", level=INFO),
413 EVENT_TYPES.DEPLOYED: EventDetail(description="Deployed", level=INFO),414 EVENT_TYPES.DEPLOYED: EventDetail(description="Deployed", level=INFO),
415 EVENT_TYPES.IMAGE_DEPLOYED: EventDetail(
416 description="Image Deployed", level=INFO
417 ),
414 EVENT_TYPES.RELEASING: EventDetail(description="Releasing", level=INFO),418 EVENT_TYPES.RELEASING: EventDetail(description="Releasing", level=INFO),
415 EVENT_TYPES.RELEASED: EventDetail(description="Released", level=INFO),419 EVENT_TYPES.RELEASED: EventDetail(description="Released", level=INFO),
416 EVENT_TYPES.ENTERING_RESCUE_MODE: EventDetail(420 EVENT_TYPES.ENTERING_RESCUE_MODE: EventDetail(

Subscribers

People subscribed via source and target branches