Merge ~cgrabowski/maas:track_frequency_of_hw_sync_for_specific_machine into maas:master
- Git
- lp:~cgrabowski/maas
- track_frequency_of_hw_sync_for_specific_machine
- Merge into master
Status: | Merged |
---|---|
Approved by: | Christian Grabowski |
Approved revision: | 19f51cf3f6a8824925d5e628645d0f509d272be1 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~cgrabowski/maas:track_frequency_of_hw_sync_for_specific_machine |
Merge into: | maas:master |
Diff against target: |
391 lines (+201/-0) 10 files modified
src/maasserver/api/machines.py (+3/-0) src/maasserver/api/tests/test_enlistment.py (+6/-0) src/maasserver/api/tests/test_machine.py (+16/-0) src/maasserver/migrations/maasserver/0267_add_machine_specific_sync_interval_fields.py (+23/-0) src/maasserver/models/node.py (+19/-0) src/maasserver/models/tests/test_node.py (+38/-0) src/maasserver/utils/converters.py (+16/-0) src/maasserver/utils/tests/test_converters.py (+41/-0) src/maasserver/websockets/handlers/node.py (+13/-0) src/maasserver/websockets/handlers/tests/test_machine.py (+26/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
Alexsander de Souza | Approve | ||
Review via email: mp+416690@code.launchpad.net |
Commit message
ensure hardware sync values exposed via api
ensure sync values exposed via websocket
add model fields to track sync interval on machine basis
Description of the change
Alexsander de Souza (alexsander-souza) wrote : | # |
next_sync could have a value before the first time the update runs, so the user can see something right after he deploys the machine
- 19f51cf... by Christian Grabowski
-
initialize last_sync as time deployment finishes
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b track_frequency
STATUS: FAILED
LOG: http://
COMMIT: 604666498341c0f
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b track_frequency
STATUS: FAILED
LOG: http://
COMMIT: 8e061c5c65c5b5d
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b track_frequency
STATUS: FAILED
LOG: http://
COMMIT: fdd7054bdb77e17
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b track_frequency
STATUS: SUCCESS
COMMIT: 19f51cf3f6a8824
Preview Diff
1 | diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py |
2 | index 0b23d28..9e51f0b 100644 |
3 | --- a/src/maasserver/api/machines.py |
4 | +++ b/src/maasserver/api/machines.py |
5 | @@ -190,6 +190,9 @@ DISPLAYED_MACHINE_FIELDS = ( |
6 | ("numanode_set", DISPLAYED_NUMANODE_FIELDS), |
7 | "virtualmachine_id", |
8 | "workload_annotations", |
9 | + "last_sync", |
10 | + "sync_interval", |
11 | + "next_sync", |
12 | ) |
13 | |
14 | # Limited set of machine fields exposed on the anonymous API. |
15 | diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py |
16 | index 44133ad..cc02116 100644 |
17 | --- a/src/maasserver/api/tests/test_enlistment.py |
18 | +++ b/src/maasserver/api/tests/test_enlistment.py |
19 | @@ -757,6 +757,9 @@ class SimpleUserLoggedInEnlistmentAPITest(APITestCase.ForUser): |
20 | "interface_test_status_name", |
21 | "virtualmachine_id", |
22 | "workload_annotations", |
23 | + "last_sync", |
24 | + "sync_interval", |
25 | + "next_sync", |
26 | }, |
27 | parsed_result.keys(), |
28 | ) |
29 | @@ -978,6 +981,9 @@ class AdminLoggedInEnlistmentAPITest(APITestCase.ForAdmin): |
30 | "interface_test_status_name", |
31 | "virtualmachine_id", |
32 | "workload_annotations", |
33 | + "last_sync", |
34 | + "sync_interval", |
35 | + "next_sync", |
36 | }, |
37 | parsed_result.keys(), |
38 | ) |
39 | diff --git a/src/maasserver/api/tests/test_machine.py b/src/maasserver/api/tests/test_machine.py |
40 | index 863b640..65eaa1d 100644 |
41 | --- a/src/maasserver/api/tests/test_machine.py |
42 | +++ b/src/maasserver/api/tests/test_machine.py |
43 | @@ -2,6 +2,7 @@ |
44 | # GNU Affero General Public License version 3 (see the file LICENSE). |
45 | |
46 | from base64 import b64encode |
47 | +from datetime import datetime |
48 | import http.client |
49 | import logging |
50 | from random import choice |
51 | @@ -250,6 +251,21 @@ class TestMachineAPI(APITestCase.ForUser): |
52 | parsed_result["boot_interface"]["mac_address"], |
53 | ) |
54 | |
55 | + def test_GET_returns_hardware_sync_values(self): |
56 | + machine = factory.make_Node(enable_hw_sync=True) |
57 | + machine.last_sync = datetime.now() |
58 | + machine.save() |
59 | + response = self.client.get(self.get_machine_uri(machine)) |
60 | + self.assertEqual(http.client.OK, response.status_code) |
61 | + parsed_result = json_load_bytes(response.content) |
62 | + self.assertEqual( |
63 | + machine.last_sync.isoformat()[:-3], parsed_result["last_sync"] |
64 | + ) |
65 | + self.assertEqual(machine.sync_interval, parsed_result["sync_interval"]) |
66 | + self.assertEqual( |
67 | + machine.next_sync.isoformat()[:-3], parsed_result["next_sync"] |
68 | + ) |
69 | + |
70 | def test_GET_refuses_to_access_nonexistent_machine(self): |
71 | # When fetching a Machine, the api returns a 'Not Found' (404) error |
72 | # if no machine is found. |
73 | diff --git a/src/maasserver/migrations/maasserver/0267_add_machine_specific_sync_interval_fields.py b/src/maasserver/migrations/maasserver/0267_add_machine_specific_sync_interval_fields.py |
74 | new file mode 100644 |
75 | index 0000000..99d3d99 |
76 | --- /dev/null |
77 | +++ b/src/maasserver/migrations/maasserver/0267_add_machine_specific_sync_interval_fields.py |
78 | @@ -0,0 +1,23 @@ |
79 | +# Generated by Django 2.2.12 on 2022-03-09 03:54 |
80 | + |
81 | +from django.db import migrations, models |
82 | + |
83 | + |
84 | +class Migration(migrations.Migration): |
85 | + |
86 | + dependencies = [ |
87 | + ("maasserver", "0266_nodedevice_unlink_node"), |
88 | + ] |
89 | + |
90 | + operations = [ |
91 | + migrations.AddField( |
92 | + model_name="node", |
93 | + name="last_sync", |
94 | + field=models.DateTimeField(blank=True, null=True), |
95 | + ), |
96 | + migrations.AddField( |
97 | + model_name="node", |
98 | + name="sync_interval", |
99 | + field=models.IntegerField(blank=True, null=True), |
100 | + ), |
101 | + ] |
102 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py |
103 | index 00bc489..a4761b2 100644 |
104 | --- a/src/maasserver/models/node.py |
105 | +++ b/src/maasserver/models/node.py |
106 | @@ -161,6 +161,7 @@ from maasserver.storage_layouts import ( |
107 | VMFS6StorageLayout, |
108 | VMFS7StorageLayout, |
109 | ) |
110 | +from maasserver.utils.converters import parse_systemd_interval |
111 | from maasserver.utils.dns import validate_hostname |
112 | from maasserver.utils.mac import get_vendor_for_mac |
113 | from maasserver.utils.orm import ( |
114 | @@ -1259,7 +1260,10 @@ class Node(CleanSave, TimestampedModel): |
115 | "NodeConfig", null=True, on_delete=CASCADE, related_name="+" |
116 | ) |
117 | |
118 | + # hardware updates |
119 | enable_hw_sync = BooleanField(default=False) |
120 | + sync_interval = IntegerField(blank=True, null=True) |
121 | + last_sync = DateTimeField(blank=True, null=True) |
122 | |
123 | # Note that the ordering of the managers is meaningful. More precisely, |
124 | # the first manager defined is important: see |
125 | @@ -1360,6 +1364,12 @@ class Node(CleanSave, TimestampedModel): |
126 | def is_pod(self): |
127 | return self.get_hosted_pods().exists() |
128 | |
129 | + @property |
130 | + def next_sync(self): |
131 | + if self.last_sync and self.sync_interval: |
132 | + return self.last_sync + timedelta(seconds=self.sync_interval) |
133 | + return None |
134 | + |
135 | def is_commissioning(self): |
136 | return self.status not in (NODE_STATUS.DEPLOYED, NODE_STATUS.DEPLOYING) |
137 | |
138 | @@ -1741,6 +1751,8 @@ class Node(CleanSave, TimestampedModel): |
139 | from maasserver.models.event import Event |
140 | |
141 | self.status = NODE_STATUS.DEPLOYED |
142 | + if self.enable_hw_sync: |
143 | + self.last_sync = datetime.now() |
144 | self.save() |
145 | |
146 | # Create a status message for DEPLOYED. |
147 | @@ -2004,6 +2016,11 @@ class Node(CleanSave, TimestampedModel): |
148 | ): |
149 | kwargs["update_fields"].append("status_expires") |
150 | |
151 | + if self.enable_hw_sync and self.sync_interval is None: |
152 | + self.sync_interval = parse_systemd_interval( |
153 | + Config.objects.get_config("hardware_sync_interval") |
154 | + ) |
155 | + |
156 | super().save(*args, **kwargs) |
157 | |
158 | # We let hostname be blank for the initial save, but fix it before the |
159 | @@ -3690,6 +3707,8 @@ class Node(CleanSave, TimestampedModel): |
160 | self.install_kvm = False |
161 | self.register_vmhost = False |
162 | self.enable_hw_sync = False |
163 | + self.sync_interval = None |
164 | + self.last_sync = None |
165 | self.save() |
166 | |
167 | # Create a status message for RELEASING. |
168 | diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py |
169 | index 6bace21..b461bc6 100644 |
170 | --- a/src/maasserver/models/tests/test_node.py |
171 | +++ b/src/maasserver/models/tests/test_node.py |
172 | @@ -2892,6 +2892,35 @@ class TestNode(MAASServerTestCase): |
173 | node.release() |
174 | self.assertFalse(node.enable_hw_sync) |
175 | |
176 | + def test_release_sets_sync_interval_to_None(self): |
177 | + node = factory.make_Node( |
178 | + status=NODE_STATUS.ALLOCATED, enable_hw_sync=True |
179 | + ) |
180 | + self.patch(node, "_stop") |
181 | + self.patch(node, "_set_status_expires") |
182 | + self.assertEqual( |
183 | + node.sync_interval, timedelta(minutes=15).total_seconds() |
184 | + ) |
185 | + with post_commit_hooks: |
186 | + node.release() |
187 | + self.assertIsNone(node.sync_interval) |
188 | + |
189 | + def test_sync_interval_is_set_when_enable_hw_sync_is_True(self): |
190 | + node = factory.make_Node( |
191 | + status=NODE_STATUS.ALLOCATED, enable_hw_sync=True |
192 | + ) |
193 | + self.assertEqual( |
194 | + node.sync_interval, timedelta(minutes=15).total_seconds() |
195 | + ) |
196 | + |
197 | + def test_next_sync_returns_time_after_last_sync(self): |
198 | + node = factory.make_Node( |
199 | + status=NODE_STATUS.ALLOCATED, enable_hw_sync=True |
200 | + ) |
201 | + node.last_sync = datetime.now() |
202 | + expected_interval = timedelta(minutes=15) |
203 | + self.assertEqual(node.next_sync, node.last_sync + expected_interval) |
204 | + |
205 | def test_dynamic_ip_addresses_from_ip_address_table(self): |
206 | node = factory.make_Node() |
207 | interfaces = [ |
208 | @@ -4867,6 +4896,15 @@ class TestNode(MAASServerTestCase): |
209 | self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status) |
210 | self.assertEqual(event.type.name, EVENT_TYPES.DEPLOYED) |
211 | |
212 | + def test_end_deployment_sets_first_last_sync_value(self): |
213 | + self.disable_node_query() |
214 | + node = factory.make_Node( |
215 | + status=NODE_STATUS.DEPLOYING, enable_hw_sync=True |
216 | + ) |
217 | + self.assertIsNone(node.last_sync) |
218 | + node.end_deployment() |
219 | + self.assertIsNotNone(node.last_sync) |
220 | + |
221 | def test_start_deployment_changes_state_and_creates_sts_msg(self): |
222 | node = factory.make_Node_with_Interface_on_Subnet( |
223 | status=NODE_STATUS.ALLOCATED |
224 | diff --git a/src/maasserver/utils/converters.py b/src/maasserver/utils/converters.py |
225 | index 690fb61..9aaa91b 100644 |
226 | --- a/src/maasserver/utils/converters.py |
227 | +++ b/src/maasserver/utils/converters.py |
228 | @@ -4,7 +4,9 @@ |
229 | """Conversion utilities.""" |
230 | |
231 | |
232 | +from datetime import timedelta |
233 | import json |
234 | +import re |
235 | |
236 | from django.conf import settings |
237 | from lxml import etree |
238 | @@ -132,3 +134,17 @@ def json_load_bytes(input: bytes, encoding=None): |
239 | settings.DEFAULT_CHARSET if encoding is None else encoding |
240 | ) |
241 | ) |
242 | + |
243 | + |
244 | +_duration_re = re.compile( |
245 | + r"((?P<hours>\d+?)(\s?(hour(s?)|hr|h))\s?)?((?P<minutes>\d+?)(\s?(minute(s?)|min|m))\s?)?((?P<seconds>\d+?)(\s?(second(s?)|sec|s))\s?)?" |
246 | +) |
247 | + |
248 | + |
249 | +def parse_systemd_interval(interval): |
250 | + duration = _duration_re.match(interval) |
251 | + if not duration: |
252 | + raise ValueError("value is not a valid interval") |
253 | + duration = duration.groupdict() |
254 | + params = {name: int(t) for name, t in duration.items() if t} |
255 | + return timedelta(**params).total_seconds() |
256 | diff --git a/src/maasserver/utils/tests/test_converters.py b/src/maasserver/utils/tests/test_converters.py |
257 | index 3b24096..960fb57 100644 |
258 | --- a/src/maasserver/utils/tests/test_converters.py |
259 | +++ b/src/maasserver/utils/tests/test_converters.py |
260 | @@ -4,11 +4,13 @@ |
261 | """Tests for converters utilities.""" |
262 | |
263 | |
264 | +from datetime import timedelta |
265 | from textwrap import dedent |
266 | |
267 | from maasserver.utils.converters import ( |
268 | human_readable_bytes, |
269 | machine_readable_bytes, |
270 | + parse_systemd_interval, |
271 | round_size_to_nearest_block, |
272 | XMLToYAML, |
273 | ) |
274 | @@ -121,3 +123,42 @@ class TestRoundSizeToNearestBlock(MAASTestCase): |
275 | round_size_to_nearest_block(size, block_size, False), |
276 | "Shouldn't remove a block from the size.", |
277 | ) |
278 | + |
279 | + |
280 | +class TestParseSystemdInterval(MAASTestCase): |
281 | + def _assert_parsed( |
282 | + self, interval: str, hours: int = 0, minutes: int = 0, seconds: int = 0 |
283 | + ): |
284 | + expected_seconds = timedelta( |
285 | + hours=hours, minutes=minutes, seconds=seconds |
286 | + ).total_seconds() |
287 | + out = parse_systemd_interval(interval) |
288 | + self.assertEqual(expected_seconds, out) |
289 | + |
290 | + def test_parses_full_plural_word_durations(self): |
291 | + self._assert_parsed( |
292 | + "2 hours 5 minutes 10 seconds", hours=2, minutes=5, seconds=10 |
293 | + ) |
294 | + |
295 | + def test_parses_full_word_durations(self): |
296 | + self._assert_parsed( |
297 | + "1 hour 1 minute 1 second", hours=1, minutes=1, seconds=1 |
298 | + ) |
299 | + |
300 | + def test_parses_abbreviated_durations(self): |
301 | + self._assert_parsed("1hr 15min 5sec", hours=1, minutes=15, seconds=5) |
302 | + |
303 | + def test_parses_initials_durations(self): |
304 | + self._assert_parsed("3h 5m 2s", hours=3, minutes=5, seconds=2) |
305 | + |
306 | + def test_parses_single_full_plural_duration(self): |
307 | + self._assert_parsed("5 minutes", minutes=5) |
308 | + |
309 | + def test_parses_single_full_duration(self): |
310 | + self._assert_parsed("1 hours", hours=1) |
311 | + |
312 | + def test_parses_single_abbreviated_duration(self): |
313 | + self._assert_parsed("5min", minutes=5) |
314 | + |
315 | + def test_parses_single_initials_duration(self): |
316 | + self._assert_parsed("15s", seconds=15) |
317 | diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py |
318 | index 8877140..74bae70 100644 |
319 | --- a/src/maasserver/websockets/handlers/node.py |
320 | +++ b/src/maasserver/websockets/handlers/node.py |
321 | @@ -304,6 +304,19 @@ class NodeHandler(TimestampedModelHandler): |
322 | else: |
323 | blockdevices = [] |
324 | |
325 | + if obj.enable_hw_sync: |
326 | + data.update( |
327 | + { |
328 | + "last_sync": obj.last_sync, |
329 | + "sync_interval": obj.sync_interval, |
330 | + "next_sync": obj.next_sync, |
331 | + } |
332 | + ) |
333 | + else: |
334 | + for k in ("last_sync", "sync_interval", "next_sync"): |
335 | + if k in data: |
336 | + del data[k] |
337 | + |
338 | if obj.node_type != NODE_TYPE.DEVICE: |
339 | # These values are not defined on a device. |
340 | data["architecture"] = obj.architecture |
341 | diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py |
342 | index fc3af86..f0a04be 100644 |
343 | --- a/src/maasserver/websockets/handlers/tests/test_machine.py |
344 | +++ b/src/maasserver/websockets/handlers/tests/test_machine.py |
345 | @@ -2,6 +2,7 @@ |
346 | # GNU Affero General Public License version 3 (see the file LICENSE). |
347 | |
348 | |
349 | +from datetime import datetime, timedelta |
350 | from functools import partial |
351 | import json |
352 | import logging |
353 | @@ -515,6 +516,15 @@ class TestMachineHandler(MAASServerTestCase): |
354 | node_script_results |
355 | ) |
356 | |
357 | + if node.enable_hw_sync: |
358 | + data.update( |
359 | + { |
360 | + "last_sync": node.last_sync, |
361 | + "sync_interval": node.sync_interval, |
362 | + "next_sync": node.next_sync, |
363 | + } |
364 | + ) |
365 | + |
366 | # Clear cache |
367 | handler._script_results = {} |
368 | |
369 | @@ -2059,6 +2069,22 @@ class TestMachineHandler(MAASServerTestCase): |
370 | expected = self.dehydrate_node(node, handler) |
371 | self.assertEqual(observed, expected) |
372 | |
373 | + def test_get_hardware_sync_fields(self): |
374 | + user = factory.make_User() |
375 | + handler = MachineHandler(user, {}, None) |
376 | + Config.objects.set_config(name="hardware_sync_interval", value="10m") |
377 | + node = factory.make_Node_with_Interface_on_Subnet(enable_hw_sync=True) |
378 | + node.last_sync = datetime.now() |
379 | + node.save() |
380 | + observed = handler.get({"system_id": node.system_id}) |
381 | + self.assertEqual(observed["last_sync"], node.last_sync) |
382 | + self.assertEqual( |
383 | + observed["sync_interval"], timedelta(minutes=10).total_seconds() |
384 | + ) |
385 | + self.assertEqual( |
386 | + observed["next_sync"], node.last_sync + timedelta(minutes=10) |
387 | + ) |
388 | + |
389 | def test_get_driver_for_series(self): |
390 | user = factory.make_User() |
391 | handler = MachineHandler(user, {}, None) |
UNIT TESTS _of_hw_ sync_for_ specific_ machine lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas
-b track_frequency
STATUS: FAILED maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 12080/console a739dec939baf01 80a7d571ab
LOG: http://
COMMIT: 96a8dee0db6e7de