← Back to team overview

sts-sponsors team mailing list archive

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

 

Jack Lloyd-Walters has proposed merging ~lloydwaltersj/maas:image-deployment-data into maas:master.

Commit message:
Add deployment information to boot resources

Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~lloydwaltersj/maas/+git/maas/+merge/434563
-- 
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/maasdb.dump b/maasdb.dump
new file mode 100644
index 0000000..f6ec7d0
Binary files /dev/null and b/maasdb.dump differ
diff --git a/src/maasserver/api/boot_resources.py b/src/maasserver/api/boot_resources.py
index 6100c89..92db55f 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):
         "name": resource.name,
         "architecture": resource.architecture,
         "resource_uri": reverse("boot_resource_handler", args=[resource.id]),
+        "last_deployed": resource.last_deployed,
     }
     dict_representation.update(resource.extra)
     if with_sets:
diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
index 3f9b55e..c5b67ab 100644
--- a/src/maasserver/api/machines.py
+++ b/src/maasserver/api/machines.py
@@ -192,6 +192,7 @@ DISPLAYED_MACHINE_FIELDS = (
     "virtualmachine_id",
     "workload_annotations",
     "last_sync",
+    "deploy_time",
     "sync_interval",
     "next_sync",
 )
diff --git a/src/maasserver/api/tests/test_boot_resources.py b/src/maasserver/api/tests/test_boot_resources.py
index a8ac72c..4b310d6 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):
             get_boot_resource_uri(resource),
             dict_representation["resource_uri"],
         )
+        self.assertEqual(
+            resource.last_deployed, dict_representation["last_deployed"]
+        )
         self.assertFalse("sets" in dict_representation)
 
     def test_boot_resource_to_dict_with_sets(self):
diff --git a/src/maasserver/migrations/maasserver/0291_auto_20221213_1017.py b/src/maasserver/migrations/maasserver/0291_auto_20221213_1017.py
new file mode 100644
index 0000000..d0e3903
--- /dev/null
+++ b/src/maasserver/migrations/maasserver/0291_auto_20221213_1017.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.12 on 2022-12-13 10:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("maasserver", "0290_migrate_node_power_parameters"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="bootresource",
+            name="last_deployed",
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name="node",
+            name="deploy_time",
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+    ]
diff --git a/src/maasserver/models/bootresource.py b/src/maasserver/models/bootresource.py
index 3ec9528..2f91195 100644
--- a/src/maasserver/models/bootresource.py
+++ b/src/maasserver/models/bootresource.py
@@ -1,4 +1,4 @@
-# Copyright 2014-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2014-2022 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Boot Resource."""
@@ -11,12 +11,12 @@ from django.db.models import (
     BooleanField,
     CharField,
     Count,
+    DateTimeField,
     IntegerField,
     Manager,
     Prefetch,
     Sum,
 )
-from django.utils import timezone
 
 from maasserver.enum import (
     BOOT_RESOURCE_FILE_TYPE,
@@ -501,6 +501,8 @@ class BootResource(CleanSave, TimestampedModel):
     # BootResource kernel and instructs Curtin to install the meta-package.
     rolling = BooleanField(blank=False, null=False, default=False)
 
+    last_deployed = DateTimeField(blank=True, null=True)
+
     extra = JSONObjectField(blank=True, default="", editable=False)
 
     def __str__(self):
@@ -516,16 +518,6 @@ class BootResource(CleanSave, TimestampedModel):
         """Return rtype text as displayed to the user."""
         return BOOT_RESOURCE_TYPE_CHOICES_DICT[self.rtype]
 
-    @property
-    def last_deployed(self) -> timezone.datetime:
-        """Returns the most recent time of deplyment for an image."""
-        # Mock data: Generates a random time based on the hash of the
-        # resource name.
-        ms_py = 3153600000000
-        return timezone.datetime(2022, 6, 1) + timezone.timedelta(
-            microseconds=hash(self.name) % ms_py
-        )
-
     def clean(self):
         """Validate the model.
 
diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
index e1c6923..73c7583 100644
--- a/src/maasserver/models/node.py
+++ b/src/maasserver/models/node.py
@@ -1261,6 +1261,7 @@ class Node(CleanSave, TimestampedModel):
     enable_hw_sync = BooleanField(default=False)
     sync_interval = IntegerField(blank=True, null=True)
     last_sync = DateTimeField(blank=True, null=True)
+    deploy_time = DateTimeField(blank=True, null=True)
 
     # Note that the ordering of the managers is meaningful.  More precisely,
     # the first manager defined is important: see
@@ -1789,11 +1790,16 @@ class Node(CleanSave, TimestampedModel):
         self.update_status(NODE_STATUS.DEPLOYED)
         if self.enable_hw_sync:
             self.last_sync = datetime.now()
+        self.update_deployment_time()
         self.save()
 
         # Create a status message for DEPLOYED.
         Event.objects.create_node_event(self, EVENT_TYPES.DEPLOYED)
 
+    def update_deployment_time(self) -> None:
+        self.deploy_time = datetime.now()
+        self.save()
+
     def ip_addresses(self, ifaces=None):
         """IP addresses allocated to this node.
 
@@ -3724,6 +3730,7 @@ class Node(CleanSave, TimestampedModel):
         self.enable_hw_sync = False
         self.sync_interval = None
         self.last_sync = None
+        self.deploy_time = None
         self.save()
 
         # Create a status message for RELEASING.
diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py
index 65414ca..2bce162 100644
--- a/src/maasserver/testing/factory.py
+++ b/src/maasserver/testing/factory.py
@@ -494,6 +494,9 @@ class Factory(maastesting.factory.Factory):
             node.boot_disk = root_partition.partition_table.block_device
             node.save()
 
+        if status == NODE_STATUS.DEPLOYED:
+            node.update_deployment_time()
+
         # Setup the BMC connected to rack controller if a BMC is created.
         if bmc_connected_to is not None:
             if power_type != "virsh":
diff --git a/src/maasserver/websockets/handlers/bootresource.py b/src/maasserver/websockets/handlers/bootresource.py
index 908af1e..8ca019c 100644
--- a/src/maasserver/websockets/handlers/bootresource.py
+++ b/src/maasserver/websockets/handlers/bootresource.py
@@ -5,6 +5,7 @@
 
 
 from collections import defaultdict
+from datetime import datetime
 
 from distro_info import UbuntuDistroInfo
 from django.core.exceptions import ValidationError
@@ -376,7 +377,26 @@ class BootResourceHandler(Handler):
                     count += 1
         return count
 
-    def pick_latest_datetime(self, time, other_time):
+    def get_last_deployed_for(self, resource: BootResource) -> datetime:
+        """Return the last deployed time for a given os, series, and architechture."""
+        if resource.rtype == BOOT_RESOURCE_TYPE.UPLOADED:
+            osystem = "custom"
+            distro_series = resource.name
+        else:
+            osystem, distro_series = resource.name.split("/")
+        last_deployed = None
+        for node in self.nodes.filter(
+            osystem=osystem, distro_series=distro_series
+        ):
+            if self.node_has_architecture_for_resource(node, resource):
+                if node.deploy_time is None:
+                    continue
+                last_deployed = self.pick_latest_datetime(
+                    last_deployed, node.deploy_time
+                )
+        return last_deployed
+
+    def pick_latest_datetime(self, time, other_time) -> datetime:
         """Return the datetime that is the latest."""
         if time is None:
             return other_time
@@ -431,6 +451,17 @@ class BootResourceHandler(Handler):
             for resource in resources
         )
 
+    def get_last_deployed_for_resources(
+        self, resources: list[BootResource]
+    ) -> datetime:
+        """Return the most recent deploy time for all resources."""
+        last_deployed = None
+        for resource in resources:
+            last_deployed = self.pick_latest_datetime(
+                last_deployed, self.get_last_deployed_for(resource)
+            )
+        return last_deployed
+
     def get_progress_for_resources(self, resources):
         """Return the overall progress for all resources."""
         size = 0
@@ -479,6 +510,7 @@ class BootResourceHandler(Handler):
         number_of_nodes = self.get_number_of_nodes_for_resources(group)
         complete = self.are_all_resources_complete(group)
         progress = self.get_progress_for_resources(group)
+        last_deployed = self.get_last_deployed_for_resources(group)
 
         # Set the computed attributes on the first resource as that will
         # be the only one returned to the UI.
@@ -489,6 +521,7 @@ class BootResourceHandler(Handler):
         resource.size = human_readable_bytes(unique_size)
         resource.last_update = last_update
         resource.number_of_nodes = number_of_nodes
+        resource.last_deployed = last_deployed
         resource.complete = complete
         if not complete:
             if progress > 0:
@@ -599,7 +632,9 @@ class BootResourceHandler(Handler):
                 ),
                 "lastDeployed": resource.last_deployed.strftime(
                     "%a, %d %b. %Y %H:%M:%S"
-                ),
+                )
+                if resource.last_deployed
+                else None,
             }
             for resource in self.combine_resources(
                 BootResource.objects.filter(bootloader_type=None)
diff --git a/src/maasserver/websockets/handlers/device.py b/src/maasserver/websockets/handlers/device.py
index 4d98b8c..ef6d3c0 100644
--- a/src/maasserver/websockets/handlers/device.py
+++ b/src/maasserver/websockets/handlers/device.py
@@ -153,6 +153,7 @@ class DeviceHandler(NodeHandler):
         list_fields = [
             "id",
             "system_id",
+            "deploy_time",
             "hostname",
             "owner",
             "domain",
diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
index 9d6c548..02bf3fe 100644
--- a/src/maasserver/websockets/handlers/node.py
+++ b/src/maasserver/websockets/handlers/node.py
@@ -271,6 +271,7 @@ class NodeHandler(TimestampedModelHandler):
         data["node_type_display"] = obj.get_node_type_display()
         data["link_type"] = NODE_TYPE_TO_LINK_TYPE[obj.node_type]
         data["tags"] = [tag.id for tag in obj.tags.all()]
+        data["deploy_time"] = dehydrate_datetime(obj.deploy_time)
         if obj.node_type == NODE_TYPE.MACHINE or (
             obj.is_controller and not for_list
         ):
diff --git a/src/maasserver/websockets/handlers/tests/test_bootresource.py b/src/maasserver/websockets/handlers/tests/test_bootresource.py
index d1f401b..784bcb5 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):
         resource = response["resources"][0]
         self.assertEqual(version, resource["title"])
 
+    def test_shows_last_deployment_time(self) -> None:
+        owner = factory.make_admin()
+        handler = BootResourceHandler(owner, {}, None)
+        resource = factory.make_usable_boot_resource(
+            rtype=BOOT_RESOURCE_TYPE.SYNCED
+        )
+        os_name, series = resource.name.split("/")
+        start_time = datetime.datetime.now()
+        node = factory.make_Node(
+            status=NODE_STATUS.DEPLOYED,
+            osystem=os_name,
+            distro_series=series,
+            architecture=resource.architecture,
+        )
+        end_time = datetime.datetime.now()
+        response = handler.poll({})
+        resource = response["resources"][0]
+        self.assertIn("lastDeployed", resource)
+        self.assertTrue(
+            node.deploy_time > start_time and node.deploy_time < end_time
+        )
+        self.assertEqual(
+            resource["lastDeployed"],
+            node.deploy_time.strftime("%a, %d %b. %Y %H:%M:%S"),
+        )
+
     def test_shows_number_of_nodes_deployed_for_resource(self):
         owner = factory.make_admin()
         handler = BootResourceHandler(owner, {}, None)
diff --git a/src/maasserver/websockets/handlers/tests/test_device.py b/src/maasserver/websockets/handlers/tests/test_device.py
index 871f0c7..c4b1825 100644
--- a/src/maasserver/websockets/handlers/tests/test_device.py
+++ b/src/maasserver/websockets/handlers/tests/test_device.py
@@ -132,6 +132,7 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
         data = {
             "actions": list(compile_node_actions(node, user).keys()),
             "created": dehydrate_datetime(node.created),
+            "deploy_time": dehydrate_datetime(node.deploy_time),
             "domain": {"id": node.domain.id, "name": node.domain.name},
             "extra_macs": [
                 "%s" % mac_address.mac_address
diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
index 08afc62..5733047 100644
--- a/src/maasserver/websockets/handlers/tests/test_machine.py
+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
@@ -538,6 +538,8 @@ class TestMachineHandler(MAASServerTestCase):
                 }
             )
 
+        data["deploy_time"] = dehydrate_datetime(node.deploy_time)
+
         # Clear cache
         handler._script_results = {}
 

Follow ups