sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #04078
[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 Lander (maas-lander)
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/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/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py
index fe5f429..f9b514e 100644
--- a/src/maasserver/api/tests/test_enlistment.py
+++ b/src/maasserver/api/tests/test_enlistment.py
@@ -755,6 +755,7 @@ class TestSimpleUserLoggedInEnlistmentAPI(APITestCase.ForUser):
"last_sync",
"sync_interval",
"next_sync",
+ "deploy_time",
},
parsed_result.keys(),
)
@@ -980,6 +981,7 @@ class TestAdminLoggedInEnlistmentAPI(APITestCase.ForAdmin):
"last_sync",
"sync_interval",
"next_sync",
+ "deploy_time",
},
parsed_result.keys(),
)
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..0ef2fa2 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,25 @@ 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 not None:
+ 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 +450,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:
+ this_deploy = self.get_last_deployed_for(resource)
+ if this_deploy is not None:
+ last_deployed = self.pick_latest_datetime(last_deployed, this_deploy)
+ return last_deployed
+
def get_progress_for_resources(self, resources):
"""Return the overall progress for all resources."""
size = 0
@@ -479,6 +509,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 +520,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 +631,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 = {}
References