Merge ~ltrager/maas:pass_script_parameters into maas:master
- Git
- lp:~ltrager/maas
- pass_script_parameters
- Merge into master
Proposed by
Lee Trager
Status: | Merged |
---|---|
Approved by: | Lee Trager |
Approved revision: | a541906923d31a5ac762a75b7dd2dace4324153c |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~ltrager/maas:pass_script_parameters |
Merge into: | maas:master |
Diff against target: |
350 lines (+245/-13) 2 files modified
src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py (+84/-7) src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py (+161/-6) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Данило Шеган (community) | Approve | ||
Review via email: mp+329983@code.launchpad.net |
Commit message
Pass parameters to scripts.
Paramaters are now passed to commissioning and testing scripts. If the
parameter is a storage type maas_run_
the storage device by model and serial to get the block device path.
Description of the change
To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote : | # |
You've also got a typo in your commit message ("ParamAters" :)
Revision history for this message
Lee Trager (ltrager) wrote : | # |
Thanks for the review and catching those late night spelling errors ;) All corrections have been made.
Revision history for this message
Andres Rodriguez (andreserl) wrote : | # |
This branch has been reverted as it broke testing in MAAS. http://<maas-ci>
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py b/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py |
2 | index b0237a5..d938174 100644 |
3 | --- a/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py |
4 | +++ b/src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py |
5 | @@ -26,7 +26,9 @@ from io import BytesIO |
6 | import json |
7 | import os |
8 | import re |
9 | +import shlex |
10 | from subprocess import ( |
11 | + check_output, |
12 | PIPE, |
13 | Popen, |
14 | TimeoutExpired, |
15 | @@ -210,6 +212,58 @@ def install_dependencies( |
16 | return True |
17 | |
18 | |
19 | +# Cache the block devices so we only have to query once. |
20 | +_block_devices = None |
21 | + |
22 | + |
23 | +def get_block_devices(): |
24 | + """If needed, query lsblk for all known block devices and store.""" |
25 | + global _block_devices |
26 | + if _block_devices is None: |
27 | + _block_devices = [] |
28 | + block_list = check_output([ |
29 | + 'lsblk', '--exclude', '1,2,7', '-d', '-P', '-o', |
30 | + 'NAME,MODEL,SERIAL']).decode('utf-8') |
31 | + for blockdev in block_list.splitlines(): |
32 | + tokens = shlex.split(blockdev) |
33 | + current_block = {} |
34 | + for token in tokens: |
35 | + k, v = token.split("=", 1) |
36 | + current_block[k] = v.strip() |
37 | + _block_devices.append(current_block) |
38 | + |
39 | + return _block_devices |
40 | + |
41 | + |
42 | +def parse_parameters(script, scripts_dir): |
43 | + """Return a list containg script path and parameters to be passed to it.""" |
44 | + ret = [os.path.join(scripts_dir, script['path'])] |
45 | + for param in script.get('parameters', {}).values(): |
46 | + param_type = param.get('type') |
47 | + if param_type == 'runtime': |
48 | + argument_format = param.get('argument_format', '--runtime={input}') |
49 | + ret += argument_format.format(input=param['value']).split() |
50 | + elif param_type == 'storage': |
51 | + value = param['value'] |
52 | + if not (value.get('model') and value.get('serial')): |
53 | + # If no model or serial were included trust that id_path |
54 | + # is correct. This is needed for VirtIO devices. |
55 | + value['path'] = value['input'] = value['id_path'] |
56 | + else: |
57 | + # Map the current path of the device to what it currently is |
58 | + # for the device model and serial. This is needed as the |
59 | + # the device name may have changed since commissioning. |
60 | + for blockdev in get_block_devices(): |
61 | + if (value['model'] == blockdev['MODEL'] and |
62 | + value['serial'] == blockdev['SERIAL']): |
63 | + value['path'] = value['input'] = "/dev/%s" % blockdev[ |
64 | + 'NAME'] |
65 | + argument_format = param.get( |
66 | + 'argument_format', '--storage={path}') |
67 | + ret += argument_format.format(**value).split() |
68 | + return ret |
69 | + |
70 | + |
71 | def run_scripts(url, creds, scripts_dir, out_dir, scripts): |
72 | """Run and report results for the given scripts.""" |
73 | total_scripts = len(scripts) |
74 | @@ -227,15 +281,23 @@ def run_scripts(url, creds, scripts_dir, out_dir, scripts): |
75 | if script_version_id is not None: |
76 | args['script_version_id'] = script_version_id |
77 | timeout_seconds = script.get('timeout_seconds') |
78 | - |
79 | - script_path = os.path.join(scripts_dir, script['path']) |
80 | - combined_path = os.path.join(out_dir, script['name']) |
81 | + for param in script.get('parameters', {}).values(): |
82 | + if param.get('type') == 'runtime': |
83 | + timeout_seconds = param['value'] |
84 | + break |
85 | + |
86 | + # Create a seperate output directory for each script being run as |
87 | + # multiple scripts with the same name may be run. |
88 | + script_out_dir = os.path.join(out_dir, '%s.%s' % ( |
89 | + script['name'], script['script_result_id'])) |
90 | + os.makedirs(script_out_dir, exist_ok=True) |
91 | + combined_path = os.path.join(script_out_dir, script['name']) |
92 | stdout_name = '%s.out' % script['name'] |
93 | - stdout_path = os.path.join(out_dir, stdout_name) |
94 | + stdout_path = os.path.join(script_out_dir, stdout_name) |
95 | stderr_name = '%s.err' % script['name'] |
96 | - stderr_path = os.path.join(out_dir, stderr_name) |
97 | + stderr_path = os.path.join(script_out_dir, stderr_name) |
98 | result_name = '%s.yaml' % script['name'] |
99 | - result_path = os.path.join(out_dir, result_name) |
100 | + result_path = os.path.join(script_out_dir, result_name) |
101 | download_path = os.path.join(scripts_dir, 'downloads', script['name']) |
102 | |
103 | if not install_dependencies( |
104 | @@ -257,6 +319,21 @@ def run_scripts(url, creds, scripts_dir, out_dir, scripts): |
105 | env['DOWNLOAD_PATH'] = download_path |
106 | |
107 | try: |
108 | + script_arguments = parse_parameters(script, scripts_dir) |
109 | + except KeyError: |
110 | + # 2 is the return code bash gives when it can't execute. |
111 | + args['exit_status'] = 2 |
112 | + args['files'] = { |
113 | + script['name']: b'Unable to map parameters', |
114 | + stderr_name: b'Unable to map parameters', |
115 | + } |
116 | + signal_wrapper( |
117 | + error='Failed to execute %s [%d/%d]: %d' % ( |
118 | + script['name'], i, total_scripts, args['exit_status']), |
119 | + **args) |
120 | + continue |
121 | + |
122 | + try: |
123 | # This script sets its own niceness value to the highest(-20) below |
124 | # to help ensure the heartbeat keeps running. When launching the |
125 | # script we need to lower the nice value as a child process |
126 | @@ -266,7 +343,7 @@ def run_scripts(url, creds, scripts_dir, out_dir, scripts): |
127 | # to the provided value. Since the runner uses a nice value of -20 |
128 | # setting it to 40 gives the actual nice value of 20. |
129 | proc = Popen( |
130 | - script_path, stdout=PIPE, stderr=PIPE, env=env, |
131 | + script_arguments, stdout=PIPE, stderr=PIPE, env=env, |
132 | preexec_fn=lambda: os.nice(40)) |
133 | capture_script_output( |
134 | proc, combined_path, stdout_path, stderr_path, timeout_seconds) |
135 | diff --git a/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py b/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py |
136 | index 0e25501..90c858a 100644 |
137 | --- a/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py |
138 | +++ b/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py |
139 | @@ -28,7 +28,9 @@ from maastesting.testcase import MAASTestCase |
140 | from snippets import maas_run_remote_scripts |
141 | from snippets.maas_run_remote_scripts import ( |
142 | download_and_extract_tar, |
143 | + get_block_devices, |
144 | install_dependencies, |
145 | + parse_parameters, |
146 | run_and_check, |
147 | run_scripts, |
148 | run_scripts_from_metadata, |
149 | @@ -52,19 +54,23 @@ class TestMaasRunRemoteScripts(MAASTestCase): |
150 | |
151 | def make_script_output(self, scripts, scripts_dir, with_result=False): |
152 | for script in scripts: |
153 | + script_out_dir = os.path.join(scripts_dir, '%s.%s' % ( |
154 | + script['name'], script['script_result_id'])) |
155 | + os.makedirs(script_out_dir) |
156 | output = factory.make_string() |
157 | stdout = factory.make_string() |
158 | stderr = factory.make_string() |
159 | script['output'] = output.encode() |
160 | script['stdout'] = stdout.encode() |
161 | script['stderr'] = stderr.encode() |
162 | - script['output_path'] = os.path.join(scripts_dir, script['name']) |
163 | + script['output_path'] = os.path.join( |
164 | + script_out_dir, script['name']) |
165 | script['stdout_path'] = os.path.join( |
166 | - scripts_dir, '%s.out' % script['name']) |
167 | + script_out_dir, '%s.out' % script['name']) |
168 | script['stderr_path'] = os.path.join( |
169 | - scripts_dir, '%s.err' % script['name']) |
170 | + script_out_dir, '%s.err' % script['name']) |
171 | script['download_path'] = os.path.join( |
172 | - scripts_dir, 'downloads', script['name']) |
173 | + script_out_dir, 'downloads', script['name']) |
174 | os.makedirs(script['download_path'], exist_ok=True) |
175 | open(script['output_path'], 'w').write(output) |
176 | open(script['stdout_path'], 'w').write(stdout) |
177 | @@ -73,7 +79,7 @@ class TestMaasRunRemoteScripts(MAASTestCase): |
178 | result = factory.make_string() |
179 | script['result'] = result.encode() |
180 | script['result_path'] = os.path.join( |
181 | - scripts_dir, '%s.yaml' % script['name']) |
182 | + script_out_dir, '%s.yaml' % script['name']) |
183 | open(script['result_path'], 'w').write(result) |
184 | |
185 | def make_index_json( |
186 | @@ -732,6 +738,152 @@ class TestMaasRunRemoteScripts(MAASTestCase): |
187 | args['error'] = 'Finished %s [2/2]: 1' % scripts[1]['name'] |
188 | self.assertThat(mock_signal, MockAnyCall(**args)) |
189 | |
190 | + def test_get_block_devices(self): |
191 | + expected_blockdevs = [ |
192 | + { |
193 | + 'NAME': factory.make_name('NAME'), |
194 | + 'MODEL': factory.make_name('MODEL'), |
195 | + 'SERIAL': factory.make_name('SERIAL'), |
196 | + } for _ in range(3) |
197 | + ] |
198 | + mock_check_output = self.patch(maas_run_remote_scripts, 'check_output') |
199 | + mock_check_output.return_value = ''.join([ |
200 | + 'NAME="{NAME}" MODEL="{MODEL}" SERIAL="{SERIAL}"\n'.format( |
201 | + **blockdev) for blockdev in expected_blockdevs]).encode() |
202 | + maas_run_remote_scripts._block_devices = None |
203 | + |
204 | + self.assertItemsEqual(expected_blockdevs, get_block_devices()) |
205 | + |
206 | + def test_get_block_devices_cached(self): |
207 | + block_devices = factory.make_name('block_devices') |
208 | + mock_check_output = self.patch(maas_run_remote_scripts, 'check_output') |
209 | + maas_run_remote_scripts._block_devices = block_devices |
210 | + |
211 | + self.assertItemsEqual(block_devices, get_block_devices()) |
212 | + self.assertThat(mock_check_output, MockNotCalled()) |
213 | + |
214 | + def test_parse_parameters(self): |
215 | + scripts_dir = factory.make_name('scripts_dir') |
216 | + script = { |
217 | + 'path': os.path.join('path_to', factory.make_name('script_name')), |
218 | + 'parameters': { |
219 | + 'runtime': { |
220 | + 'type': 'runtime', |
221 | + 'value': random.randint(0, 1000), |
222 | + }, |
223 | + 'storage_virtio': { |
224 | + 'type': 'storage', |
225 | + 'value': { |
226 | + 'name': factory.make_name('name'), |
227 | + 'model': '', |
228 | + 'serial': '', |
229 | + 'id_path': '/dev/%s' % factory.make_name('id_path'), |
230 | + }, |
231 | + }, |
232 | + 'storage': { |
233 | + 'type': 'storage', |
234 | + 'value': { |
235 | + 'name': factory.make_name('name'), |
236 | + 'model': factory.make_name('model'), |
237 | + 'serial': factory.make_name('serial'), |
238 | + 'id_path': '/dev/%s' % factory.make_name('id_path'), |
239 | + }, |
240 | + }, |
241 | + }, |
242 | + } |
243 | + mock_check_output = self.patch(maas_run_remote_scripts, 'check_output') |
244 | + mock_check_output.return_value = ''.join([ |
245 | + 'NAME="{name}" MODEL="{model}" SERIAL="{serial}"\n'.format( |
246 | + **param['value']) |
247 | + for param_name, param in script['parameters'].items() |
248 | + if 'storage' in param_name]).encode() |
249 | + maas_run_remote_scripts._block_devices = None |
250 | + |
251 | + self.assertItemsEqual( |
252 | + [ |
253 | + os.path.join(scripts_dir, script['path']), |
254 | + '--runtime=%s' % script['parameters']['runtime']['value'], |
255 | + '--storage=%s' % script['parameters']['storage_virtio'][ |
256 | + 'value']['id_path'], |
257 | + '--storage=/dev/%s' % script['parameters']['storage']['value'][ |
258 | + 'name'], |
259 | + ], parse_parameters(script, scripts_dir)) |
260 | + |
261 | + def test_parse_parameters_argument_format(self): |
262 | + scripts_dir = factory.make_name('scripts_dir') |
263 | + script = { |
264 | + 'path': os.path.join('path_to', factory.make_name('script_name')), |
265 | + 'parameters': { |
266 | + 'runtime': { |
267 | + 'type': 'runtime', |
268 | + 'value': random.randint(0, 1000), |
269 | + 'argument_format': '--foo --timeout {input}', |
270 | + }, |
271 | + 'storage': { |
272 | + 'type': 'storage', |
273 | + 'value': { |
274 | + 'name': factory.make_name('name'), |
275 | + 'model': factory.make_name('model'), |
276 | + 'serial': factory.make_name('serial'), |
277 | + 'id_path': '/dev/%s' % factory.make_name('id_path'), |
278 | + }, |
279 | + 'argument_format': ( |
280 | + '--bar {name} {model} {serial} {path} {input}'), |
281 | + }, |
282 | + }, |
283 | + } |
284 | + mock_check_output = self.patch(maas_run_remote_scripts, 'check_output') |
285 | + mock_check_output.return_value = ''.join([ |
286 | + 'NAME="{name}" MODEL="{model}" SERIAL="{serial}"\n'.format( |
287 | + **param['value']) |
288 | + for param_name, param in script['parameters'].items() |
289 | + if 'storage' in param_name]).encode() |
290 | + maas_run_remote_scripts._block_devices = None |
291 | + |
292 | + self.assertItemsEqual( |
293 | + [ |
294 | + os.path.join(scripts_dir, script['path']), |
295 | + '--foo', '--timeout', |
296 | + str(script['parameters']['runtime']['value']), |
297 | + '--bar', script['parameters']['storage']['value']['name'], |
298 | + script['parameters']['storage']['value']['model'], |
299 | + script['parameters']['storage']['value']['serial'], |
300 | + '/dev/%s' % script['parameters']['storage']['value']['name'], |
301 | + '/dev/%s' % script['parameters']['storage']['value']['name'], |
302 | + ], parse_parameters(script, scripts_dir)) |
303 | + |
304 | + def test_run_scripts_errors_with_bad_param(self): |
305 | + scripts_dir = self.useFixture(TempDirectory()).path |
306 | + mock_signal = self.patch(maas_run_remote_scripts, 'signal') |
307 | + mock_popen = self.patch(maas_run_remote_scripts, 'Popen') |
308 | + self.patch(maas_run_remote_scripts, 'install_dependencies') |
309 | + scripts = self.make_scripts() |
310 | + scripts[0]['parameters'] = {'storage': { |
311 | + 'type': 'storage', |
312 | + 'argument_format': '{bad}', |
313 | + }} |
314 | + self.make_script_output(scripts, scripts_dir, with_result=True) |
315 | + |
316 | + self.assertEquals( |
317 | + 0, run_scripts(None, None, scripts_dir, scripts_dir, scripts)) |
318 | + |
319 | + args = { |
320 | + 'url': None, 'creds': None, 'status': 'WORKING', |
321 | + 'script_result_id': scripts[0]['script_result_id'], |
322 | + 'script_version_id': scripts[0]['script_version_id'], |
323 | + 'error': 'Starting %s [1/1]' % scripts[0]['name'], |
324 | + } |
325 | + self.assertThat(mock_signal, MockAnyCall(**args)) |
326 | + self.assertThat(mock_popen, MockNotCalled()) |
327 | + # This is a MagicMock |
328 | + args['exit_status'] = 2 |
329 | + args['files'] = { |
330 | + scripts[0]['name']: b'Unable to map parameters', |
331 | + '%s.err' % scripts[0]['name']: b'Unable to map parameters', |
332 | + } |
333 | + args['error'] = 'Failed to execute %s [1/1]: 2' % scripts[0]['name'] |
334 | + self.assertThat(mock_signal, MockAnyCall(**args)) |
335 | + |
336 | def test_run_scripts_sends_result_when_available(self): |
337 | scripts_dir = self.useFixture(TempDirectory()).path |
338 | mock_signal = self.patch(maas_run_remote_scripts, 'signal') |
339 | @@ -788,7 +940,10 @@ class TestMaasRunRemoteScripts(MAASTestCase): |
340 | 1, run_scripts(None, None, scripts_dir, scripts_dir, scripts)) |
341 | |
342 | env = mock_popen.call_args[1]['env'] |
343 | - base = os.path.join(scripts_dir, scripts[0]['name']) |
344 | + base = os.path.join( |
345 | + scripts_dir, '%s.%s' % ( |
346 | + scripts[0]['name'], scripts[0]['script_result_id']), |
347 | + scripts[0]['name']) |
348 | self.assertEquals(base, env['OUTPUT_COMBINED_PATH']) |
349 | self.assertEquals('%s.out' % base, env['OUTPUT_STDOUT_PATH']) |
350 | self.assertEquals('%s.err' % base, env['OUTPUT_STDERR_PATH']) |
Looks good, a few minor nits inline (mostly typos and test improvement suggestions), so approving.