Merge ~ltrager/maas:script_result_skip into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Andres Rodriguez
Approved revision: f7b546b38aee71d52902b23f60f4910c28ce53aa
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:script_result_skip
Merge into: maas:master
Diff against target: 295 lines (+74/-17)
10 files modified
src/maasserver/static/js/angular/directives/script_status.js (+5/-1)
src/maasserver/static/js/angular/directives/tests/test_script_status.js (+6/-0)
src/maasserver/static/partials/script-results-list.html (+4/-4)
src/metadataserver/builtin_scripts/smartctl.py (+7/-0)
src/metadataserver/builtin_scripts/tests/test_smartctl.py (+10/-1)
src/metadataserver/enum.py (+3/-1)
src/metadataserver/migrations/builtin/0018_script_result_skipped.py (+23/-0)
src/metadataserver/models/scriptresult.py (+7/-4)
src/metadataserver/models/scriptset.py (+2/-0)
src/metadataserver/models/tests/test_scriptresult.py (+7/-6)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
MAAS Lander Approve
Review via email: mp+343139@code.launchpad.net

Commit message

LP: #1760919 - Allow ScriptResults to be skipped

Scripts may now signal 'skipped' in the result YAML. This tells MAAS the
script was unable to run but does not change the state of the node. The SMART
tests use 'skipped' when SMART is unavailable on a device.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b script_result_skip lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: f7b546b38aee71d52902b23f60f4910c28ce53aa

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/static/js/angular/directives/script_status.js b/src/maasserver/static/js/angular/directives/script_status.js
2index 10d6316..fd8e793 100644
3--- a/src/maasserver/static/js/angular/directives/script_status.js
4+++ b/src/maasserver/static/js/angular/directives/script_status.js
5@@ -1,4 +1,4 @@
6-/* Copyright 2017 Canonical Ltd. This software is licensed under the
7+/* Copyright 2017-2018 Canonical Ltd. This software is licensed under the
8 * GNU Affero General Public License version 3 (see the file LICENSE).
9 *
10 * Script status icon select directive.
11@@ -51,6 +51,10 @@ angular.module('MAAS').directive('maasScriptStatus', function() {
12 case 4:
13 $scope.icon = 'p-icon--timed-out';
14 break;
15+ // SCRIPT_STATUS.SKIPPED
16+ case 9:
17+ $scope.icon = 'p-icon--warning';
18+ break;
19 case -1:
20 // No scripts have been run.
21 $scope.show = false;
22diff --git a/src/maasserver/static/js/angular/directives/tests/test_script_status.js b/src/maasserver/static/js/angular/directives/tests/test_script_status.js
23index c43d192..d51b0d4 100644
24--- a/src/maasserver/static/js/angular/directives/tests/test_script_status.js
25+++ b/src/maasserver/static/js/angular/directives/tests/test_script_status.js
26@@ -87,6 +87,12 @@ describe("maasScriptStatus", function() {
27 expect(select.attr("class")).toBe("p-icon--timed-out");
28 });
29
30+ it("SCRIPT_STATUS.SKIPPED", function() {
31+ var directive = compileDirective("9");
32+ var select = directive.find("span");
33+ expect(select.attr("class")).toBe("p-icon--warning");
34+ });
35+
36 it("NONE", function() {
37 var directive = compileDirective("-1");
38 var select = directive.find("span");
39diff --git a/src/maasserver/static/partials/script-results-list.html b/src/maasserver/static/partials/script-results-list.html
40index 1c1fa4c..187a8cb 100644
41--- a/src/maasserver/static/partials/script-results-list.html
42+++ b/src/maasserver/static/partials/script-results-list.html
43@@ -32,8 +32,8 @@
44 <td class="col-3" aria-label="Date"><span data-ng-hide="result.showing_history">{$ result.updated $}</span></td>
45 <td class="col-2" aria-label="Result">
46 <span data-ng-hide="result.showing_history">
47- <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing)-->
48- {$ result.status_name $} <a data-ng-if="result.status === 2 || result.status === 3 || result.status === 4 || result.status === 6 || result.status === 8" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ result.id $}">View log</a>
49+ <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing), 9(skipped)-->
50+ {$ result.status_name $} <a data-ng-if="result.status === 2 || result.status === 3 || result.status === 4 || result.status === 6 || result.status === 8 || result.status === 9" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ result.id $}">View log</a>
51 </span>
52 </td>
53 <td class="col-1">
54@@ -84,8 +84,8 @@
55 <td class="col-1" aria-label="Runtime"><span data-maas-script-run-time="script-runtime" data-start-time="item.starttime" data-run-time="{{item.runtime}}" data-estimated-run-time="{{item.estimated_runtime}}" data-script-status="item.status"></span></td>
56 <td class="col-3" aria-label="Date">{$ item.updated $}</td>
57 <td class="col-2" aria-label="Status">
58- <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing)-->
59- {$ item.status_name $} <a data-ng-if="item.status === 2 || item.status === 3 || item.status === 4 || item.status === 6 || item.status === 8" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ item.id $}">View log</a>
60+ <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing), 9(skipped)-->
61+ {$ item.status_name $} <a data-ng-if="item.status === 2 || item.status === 3 || item.status === 4 || item.status === 6 || item.status === 8 || item.status === 9" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ item.id $}">View log</a>
62 </td>
63 </tr>
64 </tbody>
65diff --git a/src/metadataserver/builtin_scripts/smartctl.py b/src/metadataserver/builtin_scripts/smartctl.py
66index 5cff6d8..6b747f7 100644
67--- a/src/metadataserver/builtin_scripts/smartctl.py
68+++ b/src/metadataserver/builtin_scripts/smartctl.py
69@@ -35,6 +35,7 @@
70 # --- End MAAS 1.0 script metadata ---
71
72 import argparse
73+import os
74 import re
75 from subprocess import (
76 CalledProcessError,
77@@ -49,6 +50,8 @@ from subprocess import (
78 import sys
79 from time import sleep
80
81+import yaml
82+
83 # We're just reading the SMART data or asking the drive to run a self test.
84 # If this takes more then a minute there is something wrong the with drive.
85 TIMEOUT = 60
86@@ -84,6 +87,10 @@ def check_SMART_support(storage):
87 print(
88 'INFO: Unable to run test. The following drive '
89 'does not support SMART: %s\n' % storage)
90+ result_path = os.environ.get('RESULT_PATH')
91+ if result_path is not None:
92+ with open(result_path, 'w') as results_file:
93+ yaml.safe_dump({'status': 'skipped'}, results_file)
94 sys.exit()
95 print('INFO: SMART support is available; continuing...')
96
97diff --git a/src/metadataserver/builtin_scripts/tests/test_smartctl.py b/src/metadataserver/builtin_scripts/tests/test_smartctl.py
98index 9781549..11767a3 100644
99--- a/src/metadataserver/builtin_scripts/tests/test_smartctl.py
100+++ b/src/metadataserver/builtin_scripts/tests/test_smartctl.py
101@@ -1,10 +1,11 @@
102-# Copyright 2017 Canonical Ltd. This software is licensed under the
103+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
104 # GNU Affero General Public License version 3 (see the file LICENSE).
105
106 """Test smartctl functions."""
107
108 __all__ = []
109
110+import io
111 from subprocess import (
112 CalledProcessError,
113 DEVNULL,
114@@ -81,6 +82,11 @@ class TestSmartCTL(MAASTestCase):
115 mock_check_output = self.patch(smartctl, "check_output")
116 mock_check_output.return_value = b"SMART support is not available."
117 mock_print = self.patch(smartctl, "print")
118+ result_path = factory.make_name("result_path")
119+ self.patch(smartctl.os, "environ", {"RESULT_PATH": result_path})
120+ mock_open = self.patch(smartctl, "open")
121+ mock_open.return_value = io.StringIO()
122+ mock_yaml_safe_dump = self.patch(smartctl.yaml, "safe_dump")
123
124 self.assertRaises(SystemExit, smartctl.check_SMART_support, storage)
125 self.assertThat(mock_check_output, MockCalledOnceWith(
126@@ -95,6 +101,9 @@ class TestSmartCTL(MAASTestCase):
127 'sudo -n smartctl --all %s\n' % storage),
128 call('INFO: Unable to run test. The following drive does '
129 'not support SMART: %s\n' % storage)))
130+ self.assertThat(mock_open, MockCalledOnceWith(result_path, "w"))
131+ self.assertThat(mock_yaml_safe_dump, MockCalledOnceWith(
132+ {'status': 'skipped'}, mock_open.return_value))
133
134 def test_run_smartctl_selftest(self):
135 storage = factory.make_name('storage')
136diff --git a/src/metadataserver/enum.py b/src/metadataserver/enum.py
137index 7188914..d9a362c 100644
138--- a/src/metadataserver/enum.py
139+++ b/src/metadataserver/enum.py
140@@ -76,6 +76,7 @@ class SCRIPT_STATUS:
141 DEGRADED = 6
142 INSTALLING = 7
143 FAILED_INSTALLING = 8
144+ SKIPPED = 9
145
146
147 SCRIPT_STATUS_CHOICES = (
148@@ -87,7 +88,8 @@ SCRIPT_STATUS_CHOICES = (
149 (SCRIPT_STATUS.ABORTED, "Aborted"),
150 (SCRIPT_STATUS.DEGRADED, "Degraded"),
151 (SCRIPT_STATUS.INSTALLING, "Installing dependencies"),
152- (SCRIPT_STATUS.FAILED_INSTALLING, "Failed installing dependencies")
153+ (SCRIPT_STATUS.FAILED_INSTALLING, "Failed installing dependencies"),
154+ (SCRIPT_STATUS.SKIPPED, "Skipped"),
155 )
156
157
158diff --git a/src/metadataserver/migrations/builtin/0018_script_result_skipped.py b/src/metadataserver/migrations/builtin/0018_script_result_skipped.py
159new file mode 100644
160index 0000000..0e3eca6
161--- /dev/null
162+++ b/src/metadataserver/migrations/builtin/0018_script_result_skipped.py
163@@ -0,0 +1,23 @@
164+# -*- coding: utf-8 -*-
165+# Generated by Django 1.11.11 on 2018-04-12 20:44
166+from __future__ import unicode_literals
167+
168+from django.db import (
169+ migrations,
170+ models,
171+)
172+
173+
174+class Migration(migrations.Migration):
175+
176+ dependencies = [
177+ ('metadataserver', '0017_store_requested_scripts'),
178+ ]
179+
180+ operations = [
181+ migrations.AlterField(
182+ model_name='scriptresult',
183+ name='status',
184+ field=models.IntegerField(choices=[(0, 'Pending'), (1, 'Running'), (2, 'Passed'), (3, 'Failed'), (4, 'Timed out'), (5, 'Aborted'), (6, 'Degraded'), (7, 'Installing dependencies'), (8, 'Failed installing dependencies'), (9, 'Skipped')], default=0),
185+ ),
186+ ]
187diff --git a/src/metadataserver/models/scriptresult.py b/src/metadataserver/models/scriptresult.py
188index 9cd657f..add7c94 100644
189--- a/src/metadataserver/models/scriptresult.py
190+++ b/src/metadataserver/models/scriptresult.py
191@@ -189,10 +189,10 @@ class ScriptResult(CleanSave, TimestampedModel):
192 raise ValidationError('YAML must be a dictionary.')
193
194 if parsed_yaml.get('status') not in [
195- 'passed', 'failed', 'degraded', 'timedout', None]:
196+ 'passed', 'failed', 'degraded', 'timedout', 'skipped', None]:
197 raise ValidationError(
198- 'status must be "passed", "failed", "degraded", or '
199- '"timedout".')
200+ 'status must be "passed", "failed", "degraded", '
201+ '"timedout", or "skipped".')
202
203 results = parsed_yaml.get('results')
204 if results is None:
205@@ -274,6 +274,8 @@ class ScriptResult(CleanSave, TimestampedModel):
206 self.status = SCRIPT_STATUS.DEGRADED
207 elif status == 'timedout':
208 self.status = SCRIPT_STATUS.TIMEDOUT
209+ elif status == 'skipped':
210+ self.status = SCRIPT_STATUS.SKIPPED
211
212 if self.script:
213 if script_version_id is not None:
214@@ -351,7 +353,8 @@ class ScriptResult(CleanSave, TimestampedModel):
215 elif self.ended is None and self.status in {
216 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
217 SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED,
218- SCRIPT_STATUS.DEGRADED, SCRIPT_STATUS.FAILED_INSTALLING}:
219+ SCRIPT_STATUS.DEGRADED, SCRIPT_STATUS.FAILED_INSTALLING,
220+ SCRIPT_STATUS.SKIPPED}:
221 self.ended = datetime.now()
222 if 'update_fields' in kwargs:
223 kwargs['update_fields'].append('ended')
224diff --git a/src/metadataserver/models/scriptset.py b/src/metadataserver/models/scriptset.py
225index 6836b57..f84399f 100644
226--- a/src/metadataserver/models/scriptset.py
227+++ b/src/metadataserver/models/scriptset.py
228@@ -68,6 +68,8 @@ def get_status_from_qs(qs):
229 if count == 0:
230 return -1
231 # The status order below represents the order of precedence.
232+ # Skipped is omitted here otherwise one skipped test will show
233+ # a warning icon in the UI on the test tab.
234 for status in (
235 SCRIPT_STATUS.RUNNING, SCRIPT_STATUS.INSTALLING,
236 SCRIPT_STATUS.PENDING, SCRIPT_STATUS.ABORTED,
237diff --git a/src/metadataserver/models/tests/test_scriptresult.py b/src/metadataserver/models/tests/test_scriptresult.py
238index fedeb6d..ca72e72 100644
239--- a/src/metadataserver/models/tests/test_scriptresult.py
240+++ b/src/metadataserver/models/tests/test_scriptresult.py
241@@ -196,6 +196,7 @@ class TestScriptResult(MAASServerTestCase):
242 'failed': SCRIPT_STATUS.FAILED,
243 'degraded': SCRIPT_STATUS.DEGRADED,
244 'timedout': SCRIPT_STATUS.TIMEDOUT,
245+ 'skipped': SCRIPT_STATUS.SKIPPED,
246 }
247 status = random.choice(list(status_choices.keys()))
248 status_yaml = {'status': status}
249@@ -351,7 +352,7 @@ class TestScriptResult(MAASServerTestCase):
250 script_result.status = random.choice([
251 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT,
252 SCRIPT_STATUS.ABORTED, SCRIPT_STATUS.DEGRADED,
253- SCRIPT_STATUS.FAILED_INSTALLING])
254+ SCRIPT_STATUS.FAILED_INSTALLING, SCRIPT_STATUS.SKIPPED])
255 script_result.save(update_fields=['status'])
256 self.assertIsNotNone(reload_object(script_result).ended)
257
258@@ -362,7 +363,7 @@ class TestScriptResult(MAASServerTestCase):
259 script_result.status = random.choice([
260 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT,
261 SCRIPT_STATUS.ABORTED, SCRIPT_STATUS.DEGRADED,
262- SCRIPT_STATUS.FAILED_INSTALLING])
263+ SCRIPT_STATUS.FAILED_INSTALLING, SCRIPT_STATUS.SKIPPED])
264 script_result.save(update_fields=['status'])
265 script_result = reload_object(script_result)
266 self.assertIsNotNone(script_result.started)
267@@ -502,7 +503,7 @@ class TestScriptResult(MAASServerTestCase):
268 def test_read_results(self):
269 results = {
270 'status': random.choice(
271- ['passed', 'failed', 'degraded', 'timedout']),
272+ ['passed', 'failed', 'degraded', 'timedout', 'skipped']),
273 'results': {
274 factory.make_name('key'): factory.make_name('value'),
275 factory.make_name('key'): [
276@@ -524,7 +525,7 @@ class TestScriptResult(MAASServerTestCase):
277
278 def test_read_results_does_not_require_results(self):
279 result = {'status': random.choice(
280- ['passed', 'failed', 'degraded', 'timedout'])}
281+ ['passed', 'failed', 'degraded', 'timedout', 'skipped'])}
282 script_result = factory.make_ScriptResult(
283 result=yaml.safe_dump(result).encode())
284 self.assertDictEqual(result, script_result.read_results())
285@@ -547,8 +548,8 @@ class TestScriptResult(MAASServerTestCase):
286 result=yaml.safe_dump(result).encode())
287 with self.assertRaisesRegex(
288 ValidationError,
289- 'status must be "passed", "failed", "degraded", or '
290- '"timedout".'):
291+ 'status must be "passed", "failed", "degraded", '
292+ '"timedout", or "skipped".'):
293 script_result.read_results()
294
295 def test_read_results_errors_when_dict_keys_not_str(self):

Subscribers

People subscribed via source and target branches