Merge ~ltrager/maas:health_status into maas:master
- Git
- lp:~ltrager/maas
- health_status
- Merge into master
Status: | Merged |
---|---|
Approved by: | Lee Trager |
Approved revision: | 1f3b9c2ff223ab7b6aa235d4cea0e92c632df7be |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~ltrager/maas:health_status |
Merge into: | maas:master |
Diff against target: |
846 lines (+387/-116) 18 files modified
src/maasserver/api/machines.py (+10/-0) src/maasserver/api/nodes.py (+94/-1) src/maasserver/api/rackcontrollers.py (+10/-0) src/maasserver/api/regioncontrollers.py (+10/-0) src/maasserver/api/tests/test_enlistment.py (+20/-0) src/maasserver/api/tests/test_machines.py (+5/-5) src/maasserver/api/tests/test_node.py (+69/-1) src/maasserver/api/tests/test_rackcontroller.py (+10/-0) src/maasserver/api/tests/test_regioncontroller.py (+10/-0) src/maasserver/api/tests/test_tag.py (+10/-10) src/maasserver/static/js/angular/controllers/node_details.js (+1/-1) src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+1/-1) src/maasserver/static/partials/machines-table.html (+8/-2) src/maasserver/static/partials/node-details.html (+3/-3) src/maasserver/websockets/handlers/machine.py (+1/-43) src/maasserver/websockets/handlers/node.py (+81/-18) src/maasserver/websockets/handlers/tests/test_device.py (+8/-7) src/maasserver/websockets/handlers/tests/test_machine.py (+36/-24) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Needs Fixing | ||
Andres Rodriguez (community) | Approve | ||
Review via email: mp+332770@code.launchpad.net |
Commit message
LP: #1721824, #1721823 - Add health status
The UI now shows the health status of the node when the spinner isn't being
shown. The health status is the overall status of all commissioning and
testing scripts. Like the health status icons for CPU, memory, and storage
the health status icon will only display if there is a problem.
The commissioning, testing, CPU, memory, storage, and overall health are now
reported on the API. Like the websocket ScriptResults are cached so they are
only loaded once.
Description of the change
Screenshot: https:/
A tooltip is also set but isn't being shown due to LP: #1718776
Lee Trager (ltrager) wrote : | # |
I've split including commissioning results in the overall health status into its own review.
https:/
Andres Rodriguez (andreserl) wrote : | # |
Still needs fixing. This branch still includes commissioning status related stuff.
Andres Rodriguez (andreserl) wrote : | # |
more comments
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b health_status lp:~ltrager/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: c2a9049d9bedfd5
Lee Trager (ltrager) wrote : | # |
As discussed this branch no longer calculates the overall health status by looking at the testing and commissioning results. Instead the health status is the testing status.
To produce the testing_status, cpu_status, memory_status and storage_status in the API I need to cache and iterate through all ScriptResults on the node. As I'm doing that I capture the overall commissioning status and output everything in the API. This gives the API and UI feature parity with regards to status.
In the websocket I noticed we were not calculating the testing_status or commissioning_
Andres Rodriguez (andreserl) : | # |
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b health_status lp:~ltrager/maas into -b master lp:~maas-committers/maas
STATUS: FAILED
LOG: http://
COMMIT: 6a8ddb9741edc18
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b health_status lp:~ltrager/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
- 1f3b9c2... by Lee Trager
-
Fix lint
Preview Diff
1 | diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py |
2 | index d68687a..4218bdc 100644 |
3 | --- a/src/maasserver/api/machines.py |
4 | +++ b/src/maasserver/api/machines.py |
5 | @@ -146,6 +146,16 @@ DISPLAYED_MACHINE_FIELDS = ( |
6 | 'current_commissioning_result_id', |
7 | 'current_testing_result_id', |
8 | 'current_installation_result_id', |
9 | + 'commissioning_status', |
10 | + 'commissioning_status_name', |
11 | + 'testing_status', |
12 | + 'testing_status_name', |
13 | + 'cpu_test_status', |
14 | + 'cpu_test_status_name', |
15 | + 'memory_test_status', |
16 | + 'memory_test_status_name', |
17 | + 'storage_test_status', |
18 | + 'storage_test_status_name', |
19 | ) |
20 | |
21 | # Limited set of machine fields exposed on the anonymous API. |
22 | diff --git a/src/maasserver/api/nodes.py b/src/maasserver/api/nodes.py |
23 | index 8743375..13d904b 100644 |
24 | --- a/src/maasserver/api/nodes.py |
25 | +++ b/src/maasserver/api/nodes.py |
26 | @@ -1,4 +1,4 @@ |
27 | -# Copyright 2012-2016 Canonical Ltd. This software is licensed under the |
28 | +# Copyright 2012-2017 Canonical Ltd. This software is licensed under the |
29 | # GNU Affero General Public License version 3 (see the file LICENSE). |
30 | |
31 | __all__ = [ |
32 | @@ -56,6 +56,13 @@ from maasserver.models import ( |
33 | ) |
34 | from maasserver.models.nodeprobeddetails import get_single_probed_details |
35 | from maasserver.utils.orm import prefetch_queryset |
36 | +from metadataserver.enum import ( |
37 | + HARDWARE_TYPE, |
38 | + RESULT_TYPE, |
39 | + SCRIPT_STATUS, |
40 | + SCRIPT_STATUS_CHOICES, |
41 | +) |
42 | +from metadataserver.models.scriptset import get_status_from_qs |
43 | from piston3.utils import rc |
44 | from provisioningserver.drivers.power import UNKNOWN_POWER_TYPE |
45 | |
46 | @@ -246,6 +253,38 @@ def is_registered(request): |
47 | return interfaces.exists() |
48 | |
49 | |
50 | +def get_cached_script_results(node): |
51 | + """Load script results into cache and return the cached list.""" |
52 | + if not hasattr(node, '_cached_script_results'): |
53 | + node._cached_script_results = list(node.get_latest_script_results) |
54 | + node._cached_commissioning_script_results = [] |
55 | + node._cached_testing_script_results = [] |
56 | + for script_result in node._cached_script_results: |
57 | + if (script_result.script_set.result_type == |
58 | + RESULT_TYPE.INSTALLATION): |
59 | + # Don't include installation results in the health |
60 | + # status. |
61 | + continue |
62 | + elif script_result.status == SCRIPT_STATUS.ABORTED: |
63 | + # LP: #1724235 - Ignore aborted scripts. |
64 | + continue |
65 | + elif (script_result.script_set.result_type == |
66 | + RESULT_TYPE.COMMISSIONING): |
67 | + node._cached_commissioning_script_results.append(script_result) |
68 | + elif (script_result.script_set.result_type == |
69 | + RESULT_TYPE.TESTING): |
70 | + node._cached_testing_script_results.append(script_result) |
71 | + |
72 | + return node._cached_script_results |
73 | + |
74 | + |
75 | +def get_script_status_name(script_status): |
76 | + for id, name in SCRIPT_STATUS_CHOICES: |
77 | + if id == script_status: |
78 | + return name |
79 | + return 'Unknown' |
80 | + |
81 | + |
82 | class NodeHandler(OperationsHandler): |
83 | """Manage an individual Node. |
84 | |
85 | @@ -281,6 +320,60 @@ class NodeHandler(OperationsHandler): |
86 | def current_installation_result_id(handler, node): |
87 | return node.current_installation_script_set_id |
88 | |
89 | + @classmethod |
90 | + def commissioning_status(handler, node): |
91 | + get_cached_script_results(node) |
92 | + return get_status_from_qs(node._cached_commissioning_script_results) |
93 | + |
94 | + @classmethod |
95 | + def commissioning_status_name(handler, node): |
96 | + return get_script_status_name(handler.commissioning_status(node)) |
97 | + |
98 | + @classmethod |
99 | + def testing_status(handler, node): |
100 | + get_cached_script_results(node) |
101 | + return get_status_from_qs(node._cached_testing_script_results) |
102 | + |
103 | + @classmethod |
104 | + def testing_status_name(handler, node): |
105 | + return get_script_status_name(handler.testing_status(node)) |
106 | + |
107 | + @classmethod |
108 | + def cpu_test_status(handler, node): |
109 | + get_cached_script_results(node) |
110 | + return get_status_from_qs([ |
111 | + script_result for script_result |
112 | + in node._cached_testing_script_results |
113 | + if script_result.script.hardware_type == HARDWARE_TYPE.CPU]) |
114 | + |
115 | + @classmethod |
116 | + def cpu_test_status_name(handler, node): |
117 | + return get_script_status_name(handler.cpu_test_status(node)) |
118 | + |
119 | + @classmethod |
120 | + def memory_test_status(handler, node): |
121 | + get_cached_script_results(node) |
122 | + return get_status_from_qs([ |
123 | + script_result for script_result |
124 | + in node._cached_testing_script_results |
125 | + if script_result.script.hardware_type == HARDWARE_TYPE.MEMORY]) |
126 | + |
127 | + @classmethod |
128 | + def memory_test_status_name(handler, node): |
129 | + return get_script_status_name(handler.memory_test_status(node)) |
130 | + |
131 | + @classmethod |
132 | + def storage_test_status(handler, node): |
133 | + get_cached_script_results(node) |
134 | + return get_status_from_qs([ |
135 | + script_result for script_result |
136 | + in node._cached_testing_script_results |
137 | + if script_result.script.hardware_type == HARDWARE_TYPE.STORAGE]) |
138 | + |
139 | + @classmethod |
140 | + def storage_test_status_name(handler, node): |
141 | + return get_script_status_name(handler.storage_test_status(node)) |
142 | + |
143 | def read(self, request, system_id): |
144 | """Read a specific Node. |
145 | |
146 | diff --git a/src/maasserver/api/rackcontrollers.py b/src/maasserver/api/rackcontrollers.py |
147 | index 5799f3a..d643f04 100644 |
148 | --- a/src/maasserver/api/rackcontrollers.py |
149 | +++ b/src/maasserver/api/rackcontrollers.py |
150 | @@ -58,6 +58,16 @@ DISPLAYED_RACK_CONTROLLER_FIELDS = ( |
151 | 'current_testing_result_id', |
152 | 'current_installation_result_id', |
153 | 'version', |
154 | + 'commissioning_status', |
155 | + 'commissioning_status_name', |
156 | + 'testing_status', |
157 | + 'testing_status_name', |
158 | + 'cpu_test_status', |
159 | + 'cpu_test_status_name', |
160 | + 'memory_test_status', |
161 | + 'memory_test_status_name', |
162 | + 'storage_test_status', |
163 | + 'storage_test_status_name', |
164 | ) |
165 | |
166 | |
167 | diff --git a/src/maasserver/api/regioncontrollers.py b/src/maasserver/api/regioncontrollers.py |
168 | index 9a2d244..69c7b08 100644 |
169 | --- a/src/maasserver/api/regioncontrollers.py |
170 | +++ b/src/maasserver/api/regioncontrollers.py |
171 | @@ -41,6 +41,16 @@ DISPLAYED_REGION_CONTROLLER_FIELDS = ( |
172 | 'current_testing_result_id', |
173 | 'current_installation_result_id', |
174 | 'version', |
175 | + 'commissioning_status', |
176 | + 'commissioning_status_name', |
177 | + 'testing_status', |
178 | + 'testing_status_name', |
179 | + 'cpu_test_status', |
180 | + 'cpu_test_status_name', |
181 | + 'memory_test_status', |
182 | + 'memory_test_status_name', |
183 | + 'storage_test_status', |
184 | + 'storage_test_status_name', |
185 | ) |
186 | |
187 | |
188 | diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py |
189 | index 471eeb7..16bec8d 100644 |
190 | --- a/src/maasserver/api/tests/test_enlistment.py |
191 | +++ b/src/maasserver/api/tests/test_enlistment.py |
192 | @@ -559,6 +559,16 @@ class SimpleUserLoggedInEnlistmentAPITest(APITestCase.ForUser): |
193 | 'current_commissioning_result_id', |
194 | 'current_testing_result_id', |
195 | 'current_installation_result_id', |
196 | + 'commissioning_status', |
197 | + 'commissioning_status_name', |
198 | + 'testing_status', |
199 | + 'testing_status_name', |
200 | + 'cpu_test_status', |
201 | + 'cpu_test_status_name', |
202 | + 'memory_test_status', |
203 | + 'memory_test_status_name', |
204 | + 'storage_test_status', |
205 | + 'storage_test_status_name', |
206 | ], |
207 | list(parsed_result)) |
208 | |
209 | @@ -736,6 +746,16 @@ class AdminLoggedInEnlistmentAPITest(APITestCase.ForAdmin): |
210 | 'current_commissioning_result_id', |
211 | 'current_testing_result_id', |
212 | 'current_installation_result_id', |
213 | + 'commissioning_status', |
214 | + 'commissioning_status_name', |
215 | + 'testing_status', |
216 | + 'testing_status_name', |
217 | + 'cpu_test_status', |
218 | + 'cpu_test_status_name', |
219 | + 'memory_test_status', |
220 | + 'memory_test_status_name', |
221 | + 'storage_test_status', |
222 | + 'storage_test_status_name', |
223 | ], |
224 | list(parsed_result)) |
225 | |
226 | diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py |
227 | index c8199cd..0ebb9e1 100644 |
228 | --- a/src/maasserver/api/tests/test_machines.py |
229 | +++ b/src/maasserver/api/tests/test_machines.py |
230 | @@ -342,12 +342,12 @@ class TestMachinesAPI(APITestCase.ForUser): |
231 | len(extract_system_ids(parsed_result_2)), |
232 | ]) |
233 | |
234 | - # Because of fields `status_action`, `status_message`, and |
235 | - # `default_gateways`. The number of queries is not the same but it is |
236 | - # proportional to the number of machines. |
237 | + # Because of fields `status_action`, `status_message`, |
238 | + # `default_gateways`, and `health_status` the number of queries is not |
239 | + # the same but it is proportional to the number of machines. |
240 | DEFAULT_NUM = 57 |
241 | - self.assertEqual(DEFAULT_NUM + (10 * 3), num_queries1) |
242 | - self.assertEqual(DEFAULT_NUM + (20 * 3), num_queries2) |
243 | + self.assertEqual(DEFAULT_NUM + (10 * 4), num_queries1) |
244 | + self.assertEqual(DEFAULT_NUM + (20 * 4), num_queries2) |
245 | |
246 | def test_GET_without_machines_returns_empty_list(self): |
247 | # If there are no machines to list, the "read" op still works but |
248 | diff --git a/src/maasserver/api/tests/test_node.py b/src/maasserver/api/tests/test_node.py |
249 | index 706a3a0..4fecb15 100644 |
250 | --- a/src/maasserver/api/tests/test_node.py |
251 | +++ b/src/maasserver/api/tests/test_node.py |
252 | @@ -1,10 +1,11 @@ |
253 | -# Copyright 2013-2016 Canonical Ltd. This software is licensed under the |
254 | +# Copyright 2013-2017 Canonical Ltd. This software is licensed under the |
255 | # GNU Affero General Public License version 3 (see the file LICENSE). |
256 | |
257 | """Tests for the Node API.""" |
258 | |
259 | __all__ = [] |
260 | |
261 | +from functools import partial |
262 | import http.client |
263 | import random |
264 | from unittest.mock import ( |
265 | @@ -40,11 +41,14 @@ from maastesting.matchers import ( |
266 | MockNotCalled, |
267 | ) |
268 | from metadataserver.enum import ( |
269 | + HARDWARE_TYPE, |
270 | RESULT_TYPE, |
271 | SCRIPT_STATUS, |
272 | + SCRIPT_STATUS_CHOICES, |
273 | SCRIPT_TYPE, |
274 | ) |
275 | from metadataserver.models import NodeKey |
276 | +from metadataserver.models.scriptset import get_status_from_qs |
277 | from metadataserver.nodeinituser import get_node_init_user |
278 | from provisioningserver.refresh.node_info_scripts import ( |
279 | LLDP_OUTPUT_NAME, |
280 | @@ -184,6 +188,70 @@ class TestNodeAPI(APITestCase.ForUser): |
281 | args=[parsed_result['system_id']]), |
282 | parsed_result['resource_uri']) |
283 | |
284 | + def test_health_status(self): |
285 | + self.become_admin() |
286 | + machine = factory.make_Machine(owner=self.user) |
287 | + commissioning_script_set = factory.make_ScriptSet( |
288 | + result_type=RESULT_TYPE.COMMISSIONING, node=machine) |
289 | + testing_script_set = factory.make_ScriptSet( |
290 | + result_type=RESULT_TYPE.TESTING, node=machine) |
291 | + make_script_result = partial( |
292 | + factory.make_ScriptResult, script_set=testing_script_set, |
293 | + status=factory.pick_choice( |
294 | + SCRIPT_STATUS_CHOICES, but_not=[SCRIPT_STATUS.ABORTED])) |
295 | + commissioning_script_result = make_script_result( |
296 | + script_set=commissioning_script_set, script=factory.make_Script( |
297 | + script_type=SCRIPT_TYPE.COMMISSIONING)) |
298 | + cpu_script_result = make_script_result( |
299 | + script=factory.make_Script( |
300 | + script_type=SCRIPT_TYPE.TESTING, |
301 | + hardware_type=HARDWARE_TYPE.CPU)) |
302 | + memory_script_result = make_script_result( |
303 | + script=factory.make_Script( |
304 | + script_type=SCRIPT_TYPE.TESTING, |
305 | + hardware_type=HARDWARE_TYPE.MEMORY)) |
306 | + storage_script_result = make_script_result( |
307 | + script=factory.make_Script( |
308 | + script_type=SCRIPT_TYPE.TESTING, |
309 | + hardware_type=HARDWARE_TYPE.STORAGE)) |
310 | + testing_script_results = ( |
311 | + machine.get_latest_testing_script_results.exclude( |
312 | + status=SCRIPT_STATUS.ABORTED)) |
313 | + testing_status = get_status_from_qs(testing_script_results) |
314 | + |
315 | + response = self.client.get(self.get_node_uri(machine)) |
316 | + parsed_result = json_load_bytes(response.content) |
317 | + |
318 | + status = lambda s: get_status_from_qs([s]) |
319 | + status_name = lambda s: SCRIPT_STATUS_CHOICES[status(s)][1] |
320 | + self.assertThat(response, HasStatusCode(http.client.OK)) |
321 | + self.assertEquals( |
322 | + status(commissioning_script_result), |
323 | + parsed_result['commissioning_status']) |
324 | + self.assertEquals( |
325 | + status_name(commissioning_script_result), |
326 | + parsed_result['commissioning_status_name']) |
327 | + self.assertEquals(testing_status, parsed_result['testing_status']) |
328 | + self.assertEquals( |
329 | + SCRIPT_STATUS_CHOICES[testing_status][1], |
330 | + parsed_result['testing_status_name']) |
331 | + self.assertEquals( |
332 | + status(cpu_script_result), parsed_result['cpu_test_status']) |
333 | + self.assertEquals( |
334 | + status_name(cpu_script_result), |
335 | + parsed_result['cpu_test_status_name']) |
336 | + self.assertEquals( |
337 | + status(memory_script_result), parsed_result['memory_test_status']) |
338 | + self.assertEquals( |
339 | + status_name(memory_script_result), |
340 | + parsed_result['memory_test_status_name']) |
341 | + self.assertEquals( |
342 | + status(storage_script_result), |
343 | + parsed_result['storage_test_status']) |
344 | + self.assertEquals( |
345 | + status_name(storage_script_result), |
346 | + parsed_result['storage_test_status_name']) |
347 | + |
348 | def test_DELETE_deletes_node(self): |
349 | # The api allows to delete a Node. |
350 | self.become_admin() |
351 | diff --git a/src/maasserver/api/tests/test_rackcontroller.py b/src/maasserver/api/tests/test_rackcontroller.py |
352 | index a631d6c..5105c6d 100644 |
353 | --- a/src/maasserver/api/tests/test_rackcontroller.py |
354 | +++ b/src/maasserver/api/tests/test_rackcontroller.py |
355 | @@ -135,6 +135,16 @@ class TestRackControllersAPI(APITestCase.ForUser): |
356 | 'current_testing_result_id', |
357 | 'current_installation_result_id', |
358 | 'version', |
359 | + 'commissioning_status', |
360 | + 'commissioning_status_name', |
361 | + 'testing_status', |
362 | + 'testing_status_name', |
363 | + 'cpu_test_status', |
364 | + 'cpu_test_status_name', |
365 | + 'memory_test_status', |
366 | + 'memory_test_status_name', |
367 | + 'storage_test_status', |
368 | + 'storage_test_status_name', |
369 | ], |
370 | list(parsed_result[0])) |
371 | |
372 | diff --git a/src/maasserver/api/tests/test_regioncontroller.py b/src/maasserver/api/tests/test_regioncontroller.py |
373 | index fd02644..3854e6f 100644 |
374 | --- a/src/maasserver/api/tests/test_regioncontroller.py |
375 | +++ b/src/maasserver/api/tests/test_regioncontroller.py |
376 | @@ -83,5 +83,15 @@ class TestRegionControllersAPI(APITestCase.ForUser): |
377 | 'current_testing_result_id', |
378 | 'current_installation_result_id', |
379 | 'version', |
380 | + 'commissioning_status', |
381 | + 'commissioning_status_name', |
382 | + 'testing_status', |
383 | + 'testing_status_name', |
384 | + 'cpu_test_status', |
385 | + 'cpu_test_status_name', |
386 | + 'memory_test_status', |
387 | + 'memory_test_status_name', |
388 | + 'storage_test_status', |
389 | + 'storage_test_status_name', |
390 | ], |
391 | list(parsed_result[0])) |
392 | diff --git a/src/maasserver/api/tests/test_tag.py b/src/maasserver/api/tests/test_tag.py |
393 | index ef0d546..4397dd3 100644 |
394 | --- a/src/maasserver/api/tests/test_tag.py |
395 | +++ b/src/maasserver/api/tests/test_tag.py |
396 | @@ -197,10 +197,10 @@ class TestTagAPI(APITestCase.ForUser): |
397 | len(extract_system_ids(parsed_result_2)), |
398 | ]) |
399 | |
400 | - # Because of fields `status_action`, `status_message`, and |
401 | - # `default_gateways`. The number of queries is not the same but it is |
402 | - # proportional to the number of machines. |
403 | - self.assertEquals(num_queries1, num_queries2 - (3 * 3)) |
404 | + # Because of fields `status_action`, `status_message`, |
405 | + # `default_gateways`, and `health_status` the number of queries is not |
406 | + # the same but it is proportional to the number of machines. |
407 | + self.assertEquals(num_queries1, num_queries2 - (3 * 4)) |
408 | |
409 | def test_GET_machines_returns_machines(self): |
410 | tag = factory.make_Tag() |
411 | @@ -257,10 +257,10 @@ class TestTagAPI(APITestCase.ForUser): |
412 | len(extract_system_ids(parsed_result_2)), |
413 | ]) |
414 | |
415 | - # Because of fields `status_action`, `status_message`, and |
416 | - # `default_gateways`. The number of queries is not the same but it is |
417 | - # proportional to the number of machines. |
418 | - self.assertEquals(num_queries1, num_queries2 - (3 * 3)) |
419 | + # Because of fields `status_action`, `status_message`, |
420 | + # `default_gateways`, and `health_status` the number of queries is not |
421 | + # the same but it is proportional to the number of machines. |
422 | + self.assertEquals(num_queries1, num_queries2 - (3 * 4)) |
423 | |
424 | def test_GET_devices_returns_devices(self): |
425 | tag = factory.make_Tag() |
426 | @@ -375,7 +375,7 @@ class TestTagAPI(APITestCase.ForUser): |
427 | len(extract_system_ids(parsed_result_1)), |
428 | len(extract_system_ids(parsed_result_2)), |
429 | ]) |
430 | - self.assertEquals(num_queries1, num_queries2 - (3 * 2)) |
431 | + self.assertEquals(num_queries1, num_queries2 - (3 * 3)) |
432 | |
433 | def test_GET_rack_controllers_returns_no_rack_controllers_nonadmin(self): |
434 | tag = factory.make_Tag() |
435 | @@ -456,7 +456,7 @@ class TestTagAPI(APITestCase.ForUser): |
436 | len(extract_system_ids(parsed_result_1)), |
437 | len(extract_system_ids(parsed_result_2)), |
438 | ]) |
439 | - self.assertEquals(num_queries1, num_queries2 - 3) |
440 | + self.assertEquals(num_queries1, num_queries2 - 6) |
441 | |
442 | def test_GET_region_controllers_returns_no_controllers_nonadmin(self): |
443 | tag = factory.make_Tag() |
444 | diff --git a/src/maasserver/static/js/angular/controllers/node_details.js b/src/maasserver/static/js/angular/controllers/node_details.js |
445 | index 503d30a..ac96431 100644 |
446 | --- a/src/maasserver/static/js/angular/controllers/node_details.js |
447 | +++ b/src/maasserver/static/js/angular/controllers/node_details.js |
448 | @@ -1000,7 +1000,7 @@ angular.module('MAAS').controller('NodeDetailsController', [ |
449 | var results = $scope.node.installation_results; |
450 | if(!angular.isArray(results) || |
451 | results.length === 0 || results[0].output === "") { |
452 | - switch($scope.node.installation_script_set_status) { |
453 | + switch($scope.node.installation_status) { |
454 | case 0: |
455 | return "System is booting..."; |
456 | case 1: |
457 | diff --git a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js |
458 | index ceeaef8..f670338 100644 |
459 | --- a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js |
460 | +++ b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js |
461 | @@ -2197,7 +2197,7 @@ describe("NodeDetailsController", function() { |
462 | it("returns status message when no output and status", function() { |
463 | var controller = makeController(); |
464 | $scope.node = makeNode(); |
465 | - $scope.node.installation_script_set_status = makeInteger(0, 5); |
466 | + $scope.node.installation_status = makeInteger(0, 5); |
467 | expect($scope.getInstallationData()).not.toBe(""); |
468 | }); |
469 | }); |
470 | diff --git a/src/maasserver/static/partials/machines-table.html b/src/maasserver/static/partials/machines-table.html |
471 | index e47b10d..24fb55a 100644 |
472 | --- a/src/maasserver/static/partials/machines-table.html |
473 | +++ b/src/maasserver/static/partials/machines-table.html |
474 | @@ -57,8 +57,14 @@ |
475 | <td class="powerstate u-upper-case--first" aria-label="Power state"> |
476 | <span title="{$ node.power_state $}" data-ng-if="node.power_state != 'unknown'" class="icon icon--power-{$ node.power_state $} u-margin--right-tiny"></span> {$ node.power_state $} |
477 | </td> |
478 | - <td class="table-col--3 u-display--desktop"><i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-display--desktop"></i></td> |
479 | - <td class="status u-text--truncate" aria-label="{$ node.status_tooltip $}">{$ getStatusText(node) $} <span class="u-display--mobile"><i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-margin--left-tiny"></i></span></td> |
480 | + <td class="table-col--3 u-display--desktop"> |
481 | + <i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-display--desktop"></i> |
482 | + <i data-ng-if="!showSpinner(node) && node.testing_status !== 2" data-maas-script-status="script-status" data-script-status="node.testing_status" aria-label="{$ node.testing_status_tooltip $}"></i> |
483 | + </td> |
484 | + <td class="status u-text--truncate" aria-label="{$ node.status_tooltip $}">{$ getStatusText(node) $} <span class="u-display--mobile"> |
485 | + <i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-margin--left-tiny"></i> |
486 | + <i data-ng-if="!showSpinner(node) && node.testing_status !== 2" data-maas-script-status="script-status" data-script-status="node.testing_status" aria-label="{$ node.testing_status_tooltip $}"></i> |
487 | + </span></td> |
488 | <td class="table-col--10" aria-label="Owner">{$ node.owner $}</td> |
489 | <td class="u-align--right u-display--desktop" aria-label="CPU"> |
490 | <span class="tooltip" data-maas-script-status="script-status" data-script-status="node.cpu_test_status" aria-label="{$ node.cpu_test_status_tooltip $}" data-ng-if="node.cpu_test_status !== 2"></span> |
491 | diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html |
492 | index f8d8087..324f3d1 100755 |
493 | --- a/src/maasserver/static/partials/node-details.html |
494 | +++ b/src/maasserver/static/partials/node-details.html |
495 | @@ -206,13 +206,13 @@ |
496 | data-ng-click="section.area = 'storage'">Storage</button> |
497 | <button role="tab" class="page-navigation__link" data-ng-if="node.commissioning_script_count > 0" |
498 | data-ng-class="{ 'is-active': section.area === 'commissioning'}" |
499 | - data-ng-click="section.area = 'commissioning'"><span data-maas-script-status="script-status" data-script-status="node.commissioning_script_set_status"></span> Commissioning</button> |
500 | + data-ng-click="section.area = 'commissioning'"><span data-maas-script-status="script-status" data-script-status="node.commissioning_status"></span> Commissioning</button> |
501 | <button role="tab" class="page-navigation__link" data-ng-if="node.testing_script_count > 0" |
502 | data-ng-class="{ 'is-active': section.area === 'testing'}" |
503 | - data-ng-click="section.area = 'testing'"><span data-maas-script-status="script-status" data-script-status="node.testing_script_set_status"></span> Hardware tests</button> |
504 | + data-ng-click="section.area = 'testing'"><span data-maas-script-status="script-status" data-script-status="node.testing_status"></span> Hardware tests</button> |
505 | <button role="tab" class="page-navigation__link" data-ng-if="machine_output.viewable" |
506 | data-ng-class="{ 'is-active': section.area === 'logs'}" |
507 | - data-ng-click="section.area = 'logs'"><span data-maas-script-status="script-status" data-script-status="node.installation_script_set_status" data-ng-if="node.installation_results.length > 0"></span> Logs</button> |
508 | + data-ng-click="section.area = 'logs'"><span data-maas-script-status="script-status" data-script-status="node.installation_status" data-ng-if="node.installation_results.length > 0"></span> Logs</button> |
509 | <button role="tab" class="page-navigation__link" data-ng-if="!isDevice" |
510 | data-ng-class="{ 'is-active': section.area === 'events'}" |
511 | data-ng-click="section.area = 'events'">Events</button> |
512 | diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py |
513 | index ac101fc..da51686 100644 |
514 | --- a/src/maasserver/websockets/handlers/machine.py |
515 | +++ b/src/maasserver/websockets/handlers/machine.py |
516 | @@ -79,11 +79,7 @@ from maasserver.websockets.handlers.node import ( |
517 | node_prefetch, |
518 | NodeHandler, |
519 | ) |
520 | -from metadataserver.enum import ( |
521 | - HARDWARE_TYPE, |
522 | - SCRIPT_STATUS, |
523 | - SCRIPT_STATUS_CHOICES, |
524 | -) |
525 | +from metadataserver.enum import HARDWARE_TYPE |
526 | from metadataserver.models import ScriptResult |
527 | from metadataserver.models.scriptset import get_status_from_qs |
528 | from provisioningserver.logger import LegacyLogger |
529 | @@ -179,44 +175,6 @@ class MachineHandler(NodeHandler): |
530 | return Machine.objects.get_nodes( |
531 | self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset) |
532 | |
533 | - def dehydrate_hardware_status_tooltip(self, script_results): |
534 | - script_statuses = {} |
535 | - for script_result in script_results: |
536 | - if script_result.status in script_statuses: |
537 | - script_statuses[script_result.status].add(script_result.name) |
538 | - else: |
539 | - script_statuses[script_result.status] = {script_result.name} |
540 | - |
541 | - tooltip = '' |
542 | - for status, scripts in script_statuses.items(): |
543 | - len_scripts = len(scripts) |
544 | - if status in { |
545 | - SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING, |
546 | - SCRIPT_STATUS.INSTALLING}: |
547 | - verb = 'is' if len_scripts == 1 else 'are' |
548 | - elif status in { |
549 | - SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, |
550 | - SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.FAILED_INSTALLING}: |
551 | - verb = 'has' if len_scripts == 1 else 'have' |
552 | - else: |
553 | - # Covers SCRIPT_STATUS.ABORTED, an else is used incase new |
554 | - # statuses are ever added. |
555 | - verb = 'was' if len_scripts == 1 else 'were' |
556 | - |
557 | - if tooltip != '': |
558 | - tooltip += ' ' |
559 | - if len_scripts == 1: |
560 | - tooltip += '1 test ' |
561 | - else: |
562 | - tooltip += '%s tests ' % len_scripts |
563 | - tooltip += '%s %s.' % ( |
564 | - verb, SCRIPT_STATUS_CHOICES[status][1].lower()) |
565 | - |
566 | - if tooltip == '': |
567 | - tooltip = 'No tests have been run.' |
568 | - |
569 | - return tooltip |
570 | - |
571 | def list(self, params): |
572 | """List objects. |
573 | |
574 | diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py |
575 | index 8266b2a..81910f0 100644 |
576 | --- a/src/maasserver/websockets/handlers/node.py |
577 | +++ b/src/maasserver/websockets/handlers/node.py |
578 | @@ -45,8 +45,10 @@ from maasserver.websockets.handlers.timestampedmodel import ( |
579 | TimestampedModelHandler, |
580 | ) |
581 | from metadataserver.enum import ( |
582 | + HARDWARE_TYPE, |
583 | RESULT_TYPE, |
584 | SCRIPT_STATUS, |
585 | + SCRIPT_STATUS_CHOICES, |
586 | ) |
587 | from metadataserver.models.scriptset import get_status_from_qs |
588 | from provisioningserver.tags import merge_details_cleanly |
589 | @@ -118,6 +120,44 @@ class NodeHandler(TimestampedModelHandler): |
590 | """Return power_parameters None if empty.""" |
591 | return None if power_parameters == '' else power_parameters |
592 | |
593 | + def dehydrate_hardware_status_tooltip(self, script_results): |
594 | + script_statuses = {} |
595 | + for script_result in script_results: |
596 | + if script_result.status in script_statuses: |
597 | + script_statuses[script_result.status].add(script_result.name) |
598 | + else: |
599 | + script_statuses[script_result.status] = {script_result.name} |
600 | + |
601 | + tooltip = '' |
602 | + for status, scripts in script_statuses.items(): |
603 | + len_scripts = len(scripts) |
604 | + if status in { |
605 | + SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING, |
606 | + SCRIPT_STATUS.INSTALLING}: |
607 | + verb = 'is' if len_scripts == 1 else 'are' |
608 | + elif status in { |
609 | + SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, |
610 | + SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.FAILED_INSTALLING}: |
611 | + verb = 'has' if len_scripts == 1 else 'have' |
612 | + else: |
613 | + # Covers SCRIPT_STATUS.ABORTED, an else is used incase new |
614 | + # statuses are ever added. |
615 | + verb = 'was' if len_scripts == 1 else 'were' |
616 | + |
617 | + if tooltip != '': |
618 | + tooltip += ' ' |
619 | + if len_scripts == 1: |
620 | + tooltip += '1 test ' |
621 | + else: |
622 | + tooltip += '%s tests ' % len_scripts |
623 | + tooltip += '%s %s.' % ( |
624 | + verb, SCRIPT_STATUS_CHOICES[status][1].lower()) |
625 | + |
626 | + if tooltip == '': |
627 | + tooltip = 'No tests have been run.' |
628 | + |
629 | + return tooltip |
630 | + |
631 | def dehydrate(self, obj, data, for_list=False): |
632 | """Add extra fields to `data`.""" |
633 | data["fqdn"] = obj.fqdn |
634 | @@ -174,6 +214,40 @@ class NodeHandler(TimestampedModelHandler): |
635 | data["distro_series"] = obj.get_distro_series( |
636 | default=self.default_distro_series) |
637 | data["dhcp_on"] = self.get_providing_dhcp(obj) |
638 | + |
639 | + if obj.node_type != NODE_TYPE.DEVICE: |
640 | + commissioning_script_results = [] |
641 | + testing_script_results = [] |
642 | + for hw_type in self._script_results.get(obj.id, {}).values(): |
643 | + for script_result in hw_type: |
644 | + if (script_result.script_set.result_type == |
645 | + RESULT_TYPE.INSTALLATION): |
646 | + # Don't include installation results in the health |
647 | + # status. |
648 | + continue |
649 | + elif script_result.status == SCRIPT_STATUS.ABORTED: |
650 | + # LP: #1724235 - Ignore aborted scripts. |
651 | + continue |
652 | + elif (script_result.script_set.result_type == |
653 | + RESULT_TYPE.COMMISSIONING): |
654 | + commissioning_script_results.append(script_result) |
655 | + elif (script_result.script_set.result_type == |
656 | + RESULT_TYPE.TESTING): |
657 | + testing_script_results.append(script_result) |
658 | + |
659 | + data["commissioning_script_count"] = len( |
660 | + commissioning_script_results) |
661 | + data["commissioning_status"] = get_status_from_qs( |
662 | + commissioning_script_results) |
663 | + data["commissioning_status_tooltip"] = ( |
664 | + self.dehydrate_hardware_status_tooltip( |
665 | + commissioning_script_results).replace( |
666 | + 'test', 'commissioning script')) |
667 | + data["testing_script_count"] = len(testing_script_results) |
668 | + data["testing_status"] = get_status_from_qs(testing_script_results) |
669 | + data["testing_status_tooltip"] = ( |
670 | + self.dehydrate_hardware_status_tooltip(testing_script_results)) |
671 | + |
672 | if not for_list: |
673 | data["on_network"] = obj.on_network() |
674 | if obj.node_type != NODE_TYPE.DEVICE: |
675 | @@ -216,23 +290,12 @@ class NodeHandler(TimestampedModelHandler): |
676 | # Events |
677 | data["events"] = self.dehydrate_events(obj) |
678 | |
679 | - # Machine output |
680 | + # Machine logs |
681 | data = self.dehydrate_summary_output(obj, data) |
682 | - data["commissioning_script_count"] = ( |
683 | - obj.get_latest_commissioning_script_results.count()) |
684 | - data["commissioning_script_set_status"] = get_status_from_qs( |
685 | - obj.get_latest_commissioning_script_results.exclude( |
686 | - status=SCRIPT_STATUS.ABORTED)) |
687 | - data["testing_script_count"] = ( |
688 | - obj.get_latest_testing_script_results.count()) |
689 | - data["testing_script_set_status"] = get_status_from_qs( |
690 | - obj.get_latest_testing_script_results.exclude( |
691 | - status=SCRIPT_STATUS.ABORTED)) |
692 | + data["installation_status"] = self.dehydrate_script_set_status( |
693 | + obj.current_installation_script_set) |
694 | data["installation_results"] = self.dehydrate_script_set( |
695 | obj.current_installation_script_set) |
696 | - data["installation_script_set_status"] = ( |
697 | - self.dehydrate_script_set_status( |
698 | - obj.current_installation_script_set)) |
699 | |
700 | # Third party drivers |
701 | if Config.objects.get_config('enable_third_party_drivers'): |
702 | @@ -255,11 +318,11 @@ class NodeHandler(TimestampedModelHandler): |
703 | qs = qs.exclude(status=SCRIPT_STATUS.ABORTED) |
704 | cleared_node_ids = [] |
705 | for script_result in qs: |
706 | - # Builtin commissioning scripts are not stored in the database. |
707 | - if script_result.script is None: |
708 | - continue |
709 | node_id = script_result.script_set.node_id |
710 | - hardware_type = script_result.script.hardware_type |
711 | + if script_result.script is not None: |
712 | + hardware_type = script_result.script.hardware_type |
713 | + else: |
714 | + hardware_type = HARDWARE_TYPE.NODE |
715 | |
716 | if node_id not in cleared_node_ids: |
717 | self._script_results[node_id] = {} |
718 | diff --git a/src/maasserver/websockets/handlers/tests/test_device.py b/src/maasserver/websockets/handlers/tests/test_device.py |
719 | index b6a879b..b0b4490 100644 |
720 | --- a/src/maasserver/websockets/handlers/tests/test_device.py |
721 | +++ b/src/maasserver/websockets/handlers/tests/test_device.py |
722 | @@ -186,18 +186,19 @@ class TestDeviceHandler(MAASTransactionServerTestCase): |
723 | if for_list: |
724 | allowed_fields = DeviceHandler.Meta.list_fields + [ |
725 | "actions", |
726 | - "fqdn", |
727 | "extra_macs", |
728 | - "metadata", |
729 | - "tags", |
730 | - "primary_mac", |
731 | + "fabrics", |
732 | + "fqdn", |
733 | + "installation_status", |
734 | "ip_address", |
735 | "ip_assignment", |
736 | - "node_type_display", |
737 | "link_type", |
738 | - "subnets", |
739 | + "metadata", |
740 | + "node_type_display", |
741 | + "primary_mac", |
742 | "spaces", |
743 | - "fabrics", |
744 | + "subnets", |
745 | + "tags", |
746 | ] |
747 | for key in list(data): |
748 | if key not in allowed_fields: |
749 | diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py |
750 | index 0dd01f2..15ab758 100644 |
751 | --- a/src/maasserver/websockets/handlers/tests/test_machine.py |
752 | +++ b/src/maasserver/websockets/handlers/tests/test_machine.py |
753 | @@ -166,30 +166,36 @@ class TestMachineHandler(MAASServerTestCase): |
754 | ] |
755 | disks = sorted(disks, key=itemgetter("name")) |
756 | subnets = handler.get_all_subnets(node) |
757 | + commissioning_scripts = node.get_latest_commissioning_script_results |
758 | + commissioning_scripts = commissioning_scripts.exclude( |
759 | + status=SCRIPT_STATUS.ABORTED) |
760 | + testing_scripts = node.get_latest_testing_script_results |
761 | + testing_scripts = testing_scripts.exclude( |
762 | + status=SCRIPT_STATUS.ABORTED) |
763 | data = { |
764 | "actions": list(compile_node_actions(node, handler.user).keys()), |
765 | "architecture": node.architecture, |
766 | "bmc": node.bmc_id, |
767 | "boot_disk": node.boot_disk, |
768 | "bios_boot_method": node.bios_boot_method, |
769 | - "commissioning_script_count": ( |
770 | - node.get_latest_commissioning_script_results.count()), |
771 | - "commissioning_script_set_status": get_status_from_qs( |
772 | - node.get_latest_commissioning_script_results.exclude( |
773 | - status=SCRIPT_STATUS.ABORTED)), |
774 | + "commissioning_script_count": commissioning_scripts.count(), |
775 | + "commissioning_status": get_status_from_qs(commissioning_scripts), |
776 | + "commissioning_status_tooltip": ( |
777 | + handler.dehydrate_hardware_status_tooltip( |
778 | + commissioning_scripts).replace( |
779 | + 'test', 'commissioning script')), |
780 | "current_commissioning_script_set": ( |
781 | node.current_commissioning_script_set_id), |
782 | - "testing_script_count": ( |
783 | - node.get_latest_testing_script_results.count()), |
784 | - "testing_script_set_status": get_status_from_qs( |
785 | - node.get_latest_testing_script_results.exclude( |
786 | - status=SCRIPT_STATUS.ABORTED)), |
787 | + "testing_script_count": testing_scripts.count(), |
788 | + "testing_status": get_status_from_qs(testing_scripts), |
789 | + "testing_status_tooltip": ( |
790 | + handler.dehydrate_hardware_status_tooltip(testing_scripts)), |
791 | "current_testing_script_set": node.current_testing_script_set_id, |
792 | "installation_results": handler.dehydrate_script_set( |
793 | node.current_installation_script_set), |
794 | "current_installation_script_set": ( |
795 | node.current_installation_script_set_id), |
796 | - "installation_script_set_status": ( |
797 | + "installation_status": ( |
798 | handler.dehydrate_script_set_status( |
799 | node.current_installation_script_set)), |
800 | "cpu_count": node.cpu_count, |
801 | @@ -286,26 +292,32 @@ class TestMachineHandler(MAASServerTestCase): |
802 | allowed_fields = MachineHandler.Meta.list_fields + [ |
803 | "actions", |
804 | "architecture", |
805 | + "commissioning_script_count", |
806 | + "commissioning_status", |
807 | + "commissioning_status_tooltip", |
808 | + "dhcp_on", |
809 | + "distro_series", |
810 | + "extra_macs", |
811 | + "fabrics", |
812 | "fqdn", |
813 | + "link_type", |
814 | "metadata", |
815 | - "status", |
816 | - "status_code", |
817 | + "node_type_display", |
818 | + "osystem", |
819 | + "physical_disk_count", |
820 | + "pod", |
821 | "pxe_mac", |
822 | "pxe_mac_vendor", |
823 | - "extra_macs", |
824 | - "tags", |
825 | - "subnets", |
826 | - "fabrics", |
827 | "spaces", |
828 | - "physical_disk_count", |
829 | + "status", |
830 | + "status_code", |
831 | "storage", |
832 | "storage_tags", |
833 | - "node_type_display", |
834 | - "osystem", |
835 | - "distro_series", |
836 | - "dhcp_on", |
837 | - "pod", |
838 | - "link_type", |
839 | + "subnets", |
840 | + "tags", |
841 | + "testing_script_count", |
842 | + "testing_status", |
843 | + "testing_status_tooltip", |
844 | ] |
845 | for key in list(data): |
846 | if key not in allowed_fields: |
Let's minimize this branch to *only* include hardware test stuff.
You can put a different branch for commissioning, but thinking about it, we wont really need provided that commissioning will always have to finish successfully for a machine to be marked ready. e.g. there are no instances where a failed commissioning script would allow a machine to transition to ready state.