Merge ~powersj/cloud-init:feature/cc-uaclient into cloud-init:master
- Git
- lp:~powersj/cloud-init
- feature/cc-uaclient
- Merge into master
Status: | Merged |
---|---|
Approved by: | Chad Smith |
Approved revision: | b05832a5c137048031b4484d6c43509112323ebc |
Merge reported by: | Server Team CI bot |
Merged at revision: | not available |
Proposed branch: | ~powersj/cloud-init:feature/cc-uaclient |
Merge into: | cloud-init:master |
Diff against target: |
724 lines (+307/-265) 2 files modified
cloudinit/config/cc_ubuntu_advantage.py (+116/-109) cloudinit/config/tests/test_ubuntu_advantage.py (+191/-156) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chad Smith | Approve | ||
Server Team CI bot | continuous-integration | Approve | |
Review via email: mp+365549@code.launchpad.net |
Commit message
ubuntu_advantage: rewrite cloud-config module
ubuntu-
interface. Update cloud-init's config module to accept new
ubuntu_advantage configuration settings.
* Underscores better than hyphens: deprecate 'ubuntu-advantage'
cloud-config key in favor of 'ubuntu_advantage'
* Attach machines with either sso credentials of UA user_token
* Services are enabled by name though an 'enable' list
* Raise warnings if deprecated ubuntu-advantage config keys are
present, or errors if its config we cannott adapt to
Ubuntu Advantage support can now be configured via #cloud-config
with the following yaml:
ubuntu_advantage:
token: 'thisismyubuntu
enable: [esm, fips, livepatch]
Co-Authored-By: Daniel Watkins <email address hidden>
Author: Chad Smith <email address hidden>
Description of the change
Joshua Powers (powersj) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:b05832a5c13
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) wrote : | # |
Validated error and success behaviors on both containers and kvm with
#cloud-config
ubuntu_advantage:
token: <MYTOKEN>
enable: [livepatch]
write_files:
- encoding: b64
content: |
IyBVYnVudHU
cHM6Ly9sb2d
dGFnaW5nLmN
CmxvZ19sZXZ
path: /etc/ubuntu-
Chad Smith (chad.smith) wrote : | # |
This is great. thanks for the bump on this and fixups.
Preview Diff
1 | diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py |
2 | index 5e082bd..f488123 100644 |
3 | --- a/cloudinit/config/cc_ubuntu_advantage.py |
4 | +++ b/cloudinit/config/cc_ubuntu_advantage.py |
5 | @@ -1,150 +1,143 @@ |
6 | -# Copyright (C) 2018 Canonical Ltd. |
7 | -# |
8 | # This file is part of cloud-init. See LICENSE file for license information. |
9 | |
10 | -"""Ubuntu advantage: manage ubuntu-advantage offerings from Canonical.""" |
11 | +"""ubuntu_advantage: Configure Ubuntu Advantage support services""" |
12 | |
13 | -import sys |
14 | from textwrap import dedent |
15 | |
16 | -from cloudinit import log as logging |
17 | +import six |
18 | + |
19 | from cloudinit.config.schema import ( |
20 | get_schema_doc, validate_cloudconfig_schema) |
21 | +from cloudinit import log as logging |
22 | from cloudinit.settings import PER_INSTANCE |
23 | -from cloudinit.subp import prepend_base_command |
24 | from cloudinit import util |
25 | |
26 | |
27 | -distros = ['ubuntu'] |
28 | -frequency = PER_INSTANCE |
29 | +UA_URL = 'https://ubuntu.com/advantage' |
30 | |
31 | -LOG = logging.getLogger(__name__) |
32 | +distros = ['ubuntu'] |
33 | |
34 | schema = { |
35 | 'id': 'cc_ubuntu_advantage', |
36 | 'name': 'Ubuntu Advantage', |
37 | - 'title': 'Install, configure and manage ubuntu-advantage offerings', |
38 | + 'title': 'Configure Ubuntu Advantage support services', |
39 | 'description': dedent("""\ |
40 | - This module provides configuration options to setup ubuntu-advantage |
41 | - subscriptions. |
42 | - |
43 | - .. note:: |
44 | - Both ``commands`` value can be either a dictionary or a list. If |
45 | - the configuration provided is a dictionary, the keys are only used |
46 | - to order the execution of the commands and the dictionary is |
47 | - merged with any vendor-data ubuntu-advantage configuration |
48 | - provided. If a ``commands`` is provided as a list, any vendor-data |
49 | - ubuntu-advantage ``commands`` are ignored. |
50 | - |
51 | - Ubuntu-advantage ``commands`` is a dictionary or list of |
52 | - ubuntu-advantage commands to run on the deployed machine. |
53 | - These commands can be used to enable or disable subscriptions to |
54 | - various ubuntu-advantage products. See 'man ubuntu-advantage' for more |
55 | - information on supported subcommands. |
56 | - |
57 | - .. note:: |
58 | - Each command item can be a string or list. If the item is a list, |
59 | - 'ubuntu-advantage' can be omitted and it will automatically be |
60 | - inserted as part of the command. |
61 | + Attach machine to an existing Ubuntu Advantage support contract and |
62 | + enable or disable support services such as Livepatch, ESM, |
63 | + FIPS and FIPS Updates. When attaching a machine to Ubuntu Advantage, |
64 | + one can also specify services to enable. When the 'enable' |
65 | + list is present, any named service will be enabled and all absent |
66 | + services will remain disabled. |
67 | + |
68 | + Note that when enabling FIPS or FIPS updates you will need to schedule |
69 | + a reboot to ensure the machine is running the FIPS-compliant kernel. |
70 | + See :ref:`Power State Change` for information on how to configure |
71 | + cloud-init to perform this reboot. |
72 | """), |
73 | 'distros': distros, |
74 | 'examples': [dedent("""\ |
75 | - # Enable Extended Security Maintenance using your service auth token |
76 | + # Attach the machine to a Ubuntu Advantage support contract with a |
77 | + # UA contract token obtained from %s. |
78 | + ubuntu_advantage: |
79 | + token: <ua_contract_token> |
80 | + """ % UA_URL), dedent("""\ |
81 | + # Attach the machine to an Ubuntu Advantage support contract enabling |
82 | + # only fips and esm services. Services will only be enabled if |
83 | + # the environment supports said service. Otherwise warnings will |
84 | + # be logged for incompatible services specified. |
85 | ubuntu-advantage: |
86 | - commands: |
87 | - 00: ubuntu-advantage enable-esm <token> |
88 | + token: <ua_contract_token> |
89 | + enable: |
90 | + - fips |
91 | + - esm |
92 | """), dedent("""\ |
93 | - # Enable livepatch by providing your livepatch token |
94 | + # Attach the machine to an Ubuntu Advantage support contract and enable |
95 | + # the FIPS service. Perform a reboot once cloud-init has |
96 | + # completed. |
97 | + power_state: |
98 | + mode: reboot |
99 | ubuntu-advantage: |
100 | - commands: |
101 | - 00: ubuntu-advantage enable-livepatch <livepatch-token> |
102 | - |
103 | - """), dedent("""\ |
104 | - # Convenience: the ubuntu-advantage command can be omitted when |
105 | - # specifying commands as a list and 'ubuntu-advantage' will |
106 | - # automatically be prepended. |
107 | - # The following commands are equivalent |
108 | - ubuntu-advantage: |
109 | - commands: |
110 | - 00: ['enable-livepatch', 'my-token'] |
111 | - 01: ['ubuntu-advantage', 'enable-livepatch', 'my-token'] |
112 | - 02: ubuntu-advantage enable-livepatch my-token |
113 | - 03: 'ubuntu-advantage enable-livepatch my-token' |
114 | - """)], |
115 | + token: <ua_contract_token> |
116 | + enable: |
117 | + - fips |
118 | + """)], |
119 | 'frequency': PER_INSTANCE, |
120 | 'type': 'object', |
121 | 'properties': { |
122 | - 'ubuntu-advantage': { |
123 | + 'ubuntu_advantage': { |
124 | 'type': 'object', |
125 | 'properties': { |
126 | - 'commands': { |
127 | - 'type': ['object', 'array'], # Array of strings or dict |
128 | - 'items': { |
129 | - 'oneOf': [ |
130 | - {'type': 'array', 'items': {'type': 'string'}}, |
131 | - {'type': 'string'}] |
132 | - }, |
133 | - 'additionalItems': False, # Reject non-string & non-list |
134 | - 'minItems': 1, |
135 | - 'minProperties': 1, |
136 | + 'enable': { |
137 | + 'type': 'array', |
138 | + 'items': {'type': 'string'}, |
139 | + }, |
140 | + 'token': { |
141 | + 'type': 'string', |
142 | + 'description': ( |
143 | + 'A contract token obtained from %s.' % UA_URL) |
144 | } |
145 | }, |
146 | - 'additionalProperties': False, # Reject keys not in schema |
147 | - 'required': ['commands'] |
148 | + 'required': ['token'], |
149 | + 'additionalProperties': False |
150 | } |
151 | } |
152 | } |
153 | |
154 | -# TODO schema for 'assertions' and 'commands' are too permissive at the moment. |
155 | -# Once python-jsonschema supports schema draft 6 add support for arbitrary |
156 | -# object keys with 'patternProperties' constraint to validate string values. |
157 | - |
158 | __doc__ = get_schema_doc(schema) # Supplement python help() |
159 | |
160 | -UA_CMD = "ubuntu-advantage" |
161 | - |
162 | - |
163 | -def run_commands(commands): |
164 | - """Run the commands provided in ubuntu-advantage:commands config. |
165 | +LOG = logging.getLogger(__name__) |
166 | |
167 | - Commands are run individually. Any errors are collected and reported |
168 | - after attempting all commands. |
169 | |
170 | - @param commands: A list or dict containing commands to run. Keys of a |
171 | - dict will be used to order the commands provided as dict values. |
172 | - """ |
173 | - if not commands: |
174 | - return |
175 | - LOG.debug('Running user-provided ubuntu-advantage commands') |
176 | - if isinstance(commands, dict): |
177 | - # Sort commands based on dictionary key |
178 | - commands = [v for _, v in sorted(commands.items())] |
179 | - elif not isinstance(commands, list): |
180 | - raise TypeError( |
181 | - 'commands parameter was not a list or dict: {commands}'.format( |
182 | - commands=commands)) |
183 | - |
184 | - fixed_ua_commands = prepend_base_command('ubuntu-advantage', commands) |
185 | - |
186 | - cmd_failures = [] |
187 | - for command in fixed_ua_commands: |
188 | - shell = isinstance(command, str) |
189 | - try: |
190 | - util.subp(command, shell=shell, status_cb=sys.stderr.write) |
191 | - except util.ProcessExecutionError as e: |
192 | - cmd_failures.append(str(e)) |
193 | - if cmd_failures: |
194 | - msg = ( |
195 | - 'Failures running ubuntu-advantage commands:\n' |
196 | - '{cmd_failures}'.format( |
197 | - cmd_failures=cmd_failures)) |
198 | +def configure_ua(token=None, enable=None): |
199 | + """Call ua commandline client to attach or enable services.""" |
200 | + error = None |
201 | + if not token: |
202 | + error = ('ubuntu_advantage: token must be provided') |
203 | + LOG.error(error) |
204 | + raise RuntimeError(error) |
205 | + |
206 | + if enable is None: |
207 | + enable = [] |
208 | + elif isinstance(enable, six.string_types): |
209 | + LOG.warning('ubuntu_advantage: enable should be a list, not' |
210 | + ' a string; treating as a single enable') |
211 | + enable = [enable] |
212 | + elif not isinstance(enable, list): |
213 | + LOG.warning('ubuntu_advantage: enable should be a list, not' |
214 | + ' a %s; skipping enabling services', |
215 | + type(enable).__name__) |
216 | + enable = [] |
217 | + |
218 | + attach_cmd = ['ua', 'attach', token] |
219 | + LOG.debug('Attaching to Ubuntu Advantage. %s', ' '.join(attach_cmd)) |
220 | + try: |
221 | + util.subp(attach_cmd) |
222 | + except util.ProcessExecutionError as e: |
223 | + msg = 'Failure attaching Ubuntu Advantage:\n{error}'.format( |
224 | + error=str(e)) |
225 | util.logexc(LOG, msg) |
226 | raise RuntimeError(msg) |
227 | + enable_errors = [] |
228 | + for service in enable: |
229 | + try: |
230 | + cmd = ['ua', 'enable', service] |
231 | + util.subp(cmd, capture=True) |
232 | + except util.ProcessExecutionError as e: |
233 | + enable_errors.append((service, e)) |
234 | + if enable_errors: |
235 | + for service, error in enable_errors: |
236 | + msg = 'Failure enabling "{service}":\n{error}'.format( |
237 | + service=service, error=str(error)) |
238 | + util.logexc(LOG, msg) |
239 | + raise RuntimeError( |
240 | + 'Failure enabling Ubuntu Advantage service(s): {}'.format( |
241 | + ', '.join('"{}"'.format(service) |
242 | + for service, _ in enable_errors))) |
243 | |
244 | |
245 | def maybe_install_ua_tools(cloud): |
246 | """Install ubuntu-advantage-tools if not present.""" |
247 | - if util.which('ubuntu-advantage'): |
248 | + if util.which('ua'): |
249 | return |
250 | try: |
251 | cloud.distro.update_package_sources() |
252 | @@ -159,14 +152,28 @@ def maybe_install_ua_tools(cloud): |
253 | |
254 | |
255 | def handle(name, cfg, cloud, log, args): |
256 | - cfgin = cfg.get('ubuntu-advantage') |
257 | - if cfgin is None: |
258 | - LOG.debug(("Skipping module named %s," |
259 | - " no 'ubuntu-advantage' key in configuration"), name) |
260 | + ua_section = None |
261 | + if 'ubuntu-advantage' in cfg: |
262 | + LOG.warning('Deprecated configuration key "ubuntu-advantage" provided.' |
263 | + ' Expected underscore delimited "ubuntu_advantage"; will' |
264 | + ' attempt to continue.') |
265 | + ua_section = cfg['ubuntu-advantage'] |
266 | + if 'ubuntu_advantage' in cfg: |
267 | + ua_section = cfg['ubuntu_advantage'] |
268 | + if ua_section is None: |
269 | + LOG.debug("Skipping module named %s," |
270 | + " no 'ubuntu_advantage' configuration found", name) |
271 | return |
272 | - |
273 | validate_cloudconfig_schema(cfg, schema) |
274 | + if 'commands' in ua_section: |
275 | + msg = ( |
276 | + 'Deprecated configuration "ubuntu-advantage: commands" provided.' |
277 | + ' Expected "token"') |
278 | + LOG.error(msg) |
279 | + raise RuntimeError(msg) |
280 | + |
281 | maybe_install_ua_tools(cloud) |
282 | - run_commands(cfgin.get('commands', [])) |
283 | + configure_ua(token=ua_section.get('token'), |
284 | + enable=ua_section.get('enable')) |
285 | |
286 | # vi: ts=4 expandtab |
287 | diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py |
288 | index b7cf9be..8c4161e 100644 |
289 | --- a/cloudinit/config/tests/test_ubuntu_advantage.py |
290 | +++ b/cloudinit/config/tests/test_ubuntu_advantage.py |
291 | @@ -1,10 +1,7 @@ |
292 | # This file is part of cloud-init. See LICENSE file for license information. |
293 | |
294 | -import re |
295 | -from six import StringIO |
296 | - |
297 | from cloudinit.config.cc_ubuntu_advantage import ( |
298 | - handle, maybe_install_ua_tools, run_commands, schema) |
299 | + configure_ua, handle, maybe_install_ua_tools, schema) |
300 | from cloudinit.config.schema import validate_cloudconfig_schema |
301 | from cloudinit import util |
302 | from cloudinit.tests.helpers import ( |
303 | @@ -20,90 +17,120 @@ class FakeCloud(object): |
304 | self.distro = distro |
305 | |
306 | |
307 | -class TestRunCommands(CiTestCase): |
308 | +class TestConfigureUA(CiTestCase): |
309 | |
310 | with_logs = True |
311 | allowed_subp = [CiTestCase.SUBP_SHELL_TRUE] |
312 | |
313 | def setUp(self): |
314 | - super(TestRunCommands, self).setUp() |
315 | + super(TestConfigureUA, self).setUp() |
316 | self.tmp = self.tmp_dir() |
317 | |
318 | @mock.patch('%s.util.subp' % MPATH) |
319 | - def test_run_commands_on_empty_list(self, m_subp): |
320 | - """When provided with an empty list, run_commands does nothing.""" |
321 | - run_commands([]) |
322 | - self.assertEqual('', self.logs.getvalue()) |
323 | - m_subp.assert_not_called() |
324 | - |
325 | - def test_run_commands_on_non_list_or_dict(self): |
326 | - """When provided an invalid type, run_commands raises an error.""" |
327 | - with self.assertRaises(TypeError) as context_manager: |
328 | - run_commands(commands="I'm Not Valid") |
329 | + def test_configure_ua_attach_error(self, m_subp): |
330 | + """Errors from ua attach command are raised.""" |
331 | + m_subp.side_effect = util.ProcessExecutionError( |
332 | + 'Invalid token SomeToken') |
333 | + with self.assertRaises(RuntimeError) as context_manager: |
334 | + configure_ua(token='SomeToken') |
335 | self.assertEqual( |
336 | - "commands parameter was not a list or dict: I'm Not Valid", |
337 | + 'Failure attaching Ubuntu Advantage:\nUnexpected error while' |
338 | + ' running command.\nCommand: -\nExit code: -\nReason: -\n' |
339 | + 'Stdout: Invalid token SomeToken\nStderr: -', |
340 | str(context_manager.exception)) |
341 | |
342 | - def test_run_command_logs_commands_and_exit_codes_to_stderr(self): |
343 | - """All exit codes are logged to stderr.""" |
344 | - outfile = self.tmp_path('output.log', dir=self.tmp) |
345 | - |
346 | - cmd1 = 'echo "HI" >> %s' % outfile |
347 | - cmd2 = 'bogus command' |
348 | - cmd3 = 'echo "MOM" >> %s' % outfile |
349 | - commands = [cmd1, cmd2, cmd3] |
350 | - |
351 | - mock_path = '%s.sys.stderr' % MPATH |
352 | - with mock.patch(mock_path, new_callable=StringIO) as m_stderr: |
353 | - with self.assertRaises(RuntimeError) as context_manager: |
354 | - run_commands(commands=commands) |
355 | - |
356 | - self.assertIsNotNone( |
357 | - re.search(r'bogus: (command )?not found', |
358 | - str(context_manager.exception)), |
359 | - msg='Expected bogus command not found') |
360 | - expected_stderr_log = '\n'.join([ |
361 | - 'Begin run command: {cmd}'.format(cmd=cmd1), |
362 | - 'End run command: exit(0)', |
363 | - 'Begin run command: {cmd}'.format(cmd=cmd2), |
364 | - 'ERROR: End run command: exit(127)', |
365 | - 'Begin run command: {cmd}'.format(cmd=cmd3), |
366 | - 'End run command: exit(0)\n']) |
367 | - self.assertEqual(expected_stderr_log, m_stderr.getvalue()) |
368 | - |
369 | - def test_run_command_as_lists(self): |
370 | - """When commands are specified as a list, run them in order.""" |
371 | - outfile = self.tmp_path('output.log', dir=self.tmp) |
372 | - |
373 | - cmd1 = 'echo "HI" >> %s' % outfile |
374 | - cmd2 = 'echo "MOM" >> %s' % outfile |
375 | - commands = [cmd1, cmd2] |
376 | - with mock.patch('%s.sys.stderr' % MPATH, new_callable=StringIO): |
377 | - run_commands(commands=commands) |
378 | + @mock.patch('%s.util.subp' % MPATH) |
379 | + def test_configure_ua_attach_with_token(self, m_subp): |
380 | + """When token is provided, attach the machine to ua using the token.""" |
381 | + configure_ua(token='SomeToken') |
382 | + m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken']) |
383 | + self.assertEqual( |
384 | + 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', |
385 | + self.logs.getvalue()) |
386 | + |
387 | + @mock.patch('%s.util.subp' % MPATH) |
388 | + def test_configure_ua_attach_on_service_error(self, m_subp): |
389 | + """all services should be enabled and then any failures raised""" |
390 | |
391 | + def fake_subp(cmd, capture=None): |
392 | + fail_cmds = [['ua', 'enable', svc] for svc in ['esm', 'cc']] |
393 | + if cmd in fail_cmds and capture: |
394 | + svc = cmd[-1] |
395 | + raise util.ProcessExecutionError( |
396 | + 'Invalid {} credentials'.format(svc.upper())) |
397 | + |
398 | + m_subp.side_effect = fake_subp |
399 | + |
400 | + with self.assertRaises(RuntimeError) as context_manager: |
401 | + configure_ua(token='SomeToken', enable=['esm', 'cc', 'fips']) |
402 | + self.assertEqual( |
403 | + m_subp.call_args_list, |
404 | + [mock.call(['ua', 'attach', 'SomeToken']), |
405 | + mock.call(['ua', 'enable', 'esm'], capture=True), |
406 | + mock.call(['ua', 'enable', 'cc'], capture=True), |
407 | + mock.call(['ua', 'enable', 'fips'], capture=True)]) |
408 | self.assertIn( |
409 | - 'DEBUG: Running user-provided ubuntu-advantage commands', |
410 | + 'WARNING: Failure enabling "esm":\nUnexpected error' |
411 | + ' while running command.\nCommand: -\nExit code: -\nReason: -\n' |
412 | + 'Stdout: Invalid ESM credentials\nStderr: -\n', |
413 | self.logs.getvalue()) |
414 | - self.assertEqual('HI\nMOM\n', util.load_file(outfile)) |
415 | self.assertIn( |
416 | - 'WARNING: Non-ubuntu-advantage commands in ubuntu-advantage' |
417 | - ' config:', |
418 | + 'WARNING: Failure enabling "cc":\nUnexpected error' |
419 | + ' while running command.\nCommand: -\nExit code: -\nReason: -\n' |
420 | + 'Stdout: Invalid CC credentials\nStderr: -\n', |
421 | + self.logs.getvalue()) |
422 | + self.assertEqual( |
423 | + 'Failure enabling Ubuntu Advantage service(s): "esm", "cc"', |
424 | + str(context_manager.exception)) |
425 | + |
426 | + @mock.patch('%s.util.subp' % MPATH) |
427 | + def test_configure_ua_attach_with_empty_services(self, m_subp): |
428 | + """When services is an empty list, do not auto-enable attach.""" |
429 | + configure_ua(token='SomeToken', enable=[]) |
430 | + m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken']) |
431 | + self.assertEqual( |
432 | + 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', |
433 | self.logs.getvalue()) |
434 | |
435 | - def test_run_command_dict_sorted_as_command_script(self): |
436 | - """When commands are a dict, sort them and run.""" |
437 | - outfile = self.tmp_path('output.log', dir=self.tmp) |
438 | - cmd1 = 'echo "HI" >> %s' % outfile |
439 | - cmd2 = 'echo "MOM" >> %s' % outfile |
440 | - commands = {'02': cmd1, '01': cmd2} |
441 | - with mock.patch('%s.sys.stderr' % MPATH, new_callable=StringIO): |
442 | - run_commands(commands=commands) |
443 | + @mock.patch('%s.util.subp' % MPATH) |
444 | + def test_configure_ua_attach_with_specific_services(self, m_subp): |
445 | + """When services a list, only enable specific services.""" |
446 | + configure_ua(token='SomeToken', enable=['fips']) |
447 | + self.assertEqual( |
448 | + m_subp.call_args_list, |
449 | + [mock.call(['ua', 'attach', 'SomeToken']), |
450 | + mock.call(['ua', 'enable', 'fips'], capture=True)]) |
451 | + self.assertEqual( |
452 | + 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', |
453 | + self.logs.getvalue()) |
454 | + |
455 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock()) |
456 | + @mock.patch('%s.util.subp' % MPATH) |
457 | + def test_configure_ua_attach_with_string_services(self, m_subp): |
458 | + """When services a string, treat as singleton list and warn""" |
459 | + configure_ua(token='SomeToken', enable='fips') |
460 | + self.assertEqual( |
461 | + m_subp.call_args_list, |
462 | + [mock.call(['ua', 'attach', 'SomeToken']), |
463 | + mock.call(['ua', 'enable', 'fips'], capture=True)]) |
464 | + self.assertEqual( |
465 | + 'WARNING: ubuntu_advantage: enable should be a list, not a' |
466 | + ' string; treating as a single enable\n' |
467 | + 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', |
468 | + self.logs.getvalue()) |
469 | |
470 | - expected_messages = [ |
471 | - 'DEBUG: Running user-provided ubuntu-advantage commands'] |
472 | - for message in expected_messages: |
473 | - self.assertIn(message, self.logs.getvalue()) |
474 | - self.assertEqual('MOM\nHI\n', util.load_file(outfile)) |
475 | + @mock.patch('%s.util.subp' % MPATH) |
476 | + def test_configure_ua_attach_with_weird_services(self, m_subp): |
477 | + """When services not string or list, warn but still attach""" |
478 | + configure_ua(token='SomeToken', enable={'deffo': 'wont work'}) |
479 | + self.assertEqual( |
480 | + m_subp.call_args_list, |
481 | + [mock.call(['ua', 'attach', 'SomeToken'])]) |
482 | + self.assertEqual( |
483 | + 'WARNING: ubuntu_advantage: enable should be a list, not a' |
484 | + ' dict; skipping enabling services\n' |
485 | + 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', |
486 | + self.logs.getvalue()) |
487 | |
488 | |
489 | @skipUnlessJsonSchema() |
490 | @@ -112,90 +139,50 @@ class TestSchema(CiTestCase, SchemaTestCaseMixin): |
491 | with_logs = True |
492 | schema = schema |
493 | |
494 | - def test_schema_warns_on_ubuntu_advantage_not_as_dict(self): |
495 | - """If ubuntu-advantage configuration is not a dict, emit a warning.""" |
496 | - validate_cloudconfig_schema({'ubuntu-advantage': 'wrong type'}, schema) |
497 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH) |
498 | + @mock.patch('%s.configure_ua' % MPATH) |
499 | + def test_schema_warns_on_ubuntu_advantage_not_dict(self, _cfg, _): |
500 | + """If ubuntu_advantage configuration is not a dict, emit a warning.""" |
501 | + validate_cloudconfig_schema({'ubuntu_advantage': 'wrong type'}, schema) |
502 | self.assertEqual( |
503 | - "WARNING: Invalid config:\nubuntu-advantage: 'wrong type' is not" |
504 | + "WARNING: Invalid config:\nubuntu_advantage: 'wrong type' is not" |
505 | " of type 'object'\n", |
506 | self.logs.getvalue()) |
507 | |
508 | - @mock.patch('%s.run_commands' % MPATH) |
509 | - def test_schema_disallows_unknown_keys(self, _): |
510 | - """Unknown keys in ubuntu-advantage configuration emit warnings.""" |
511 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH) |
512 | + @mock.patch('%s.configure_ua' % MPATH) |
513 | + def test_schema_disallows_unknown_keys(self, _cfg, _): |
514 | + """Unknown keys in ubuntu_advantage configuration emit warnings.""" |
515 | validate_cloudconfig_schema( |
516 | - {'ubuntu-advantage': {'commands': ['ls'], 'invalid-key': ''}}, |
517 | + {'ubuntu_advantage': {'token': 'winner', 'invalid-key': ''}}, |
518 | schema) |
519 | self.assertIn( |
520 | - 'WARNING: Invalid config:\nubuntu-advantage: Additional properties' |
521 | + 'WARNING: Invalid config:\nubuntu_advantage: Additional properties' |
522 | " are not allowed ('invalid-key' was unexpected)", |
523 | self.logs.getvalue()) |
524 | |
525 | - def test_warn_schema_requires_commands(self): |
526 | - """Warn when ubuntu-advantage configuration lacks commands.""" |
527 | - validate_cloudconfig_schema( |
528 | - {'ubuntu-advantage': {}}, schema) |
529 | - self.assertEqual( |
530 | - "WARNING: Invalid config:\nubuntu-advantage: 'commands' is a" |
531 | - " required property\n", |
532 | - self.logs.getvalue()) |
533 | - |
534 | - @mock.patch('%s.run_commands' % MPATH) |
535 | - def test_warn_schema_commands_is_not_list_or_dict(self, _): |
536 | - """Warn when ubuntu-advantage:commands config is not a list or dict.""" |
537 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH) |
538 | + @mock.patch('%s.configure_ua' % MPATH) |
539 | + def test_warn_schema_requires_token(self, _cfg, _): |
540 | + """Warn if ubuntu_advantage configuration lacks token.""" |
541 | validate_cloudconfig_schema( |
542 | - {'ubuntu-advantage': {'commands': 'broken'}}, schema) |
543 | + {'ubuntu_advantage': {'enable': ['esm']}}, schema) |
544 | self.assertEqual( |
545 | - "WARNING: Invalid config:\nubuntu-advantage.commands: 'broken' is" |
546 | - " not of type 'object', 'array'\n", |
547 | - self.logs.getvalue()) |
548 | + "WARNING: Invalid config:\nubuntu_advantage:" |
549 | + " 'token' is a required property\n", self.logs.getvalue()) |
550 | |
551 | - @mock.patch('%s.run_commands' % MPATH) |
552 | - def test_warn_schema_when_commands_is_empty(self, _): |
553 | - """Emit warnings when ubuntu-advantage:commands is empty.""" |
554 | - validate_cloudconfig_schema( |
555 | - {'ubuntu-advantage': {'commands': []}}, schema) |
556 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH) |
557 | + @mock.patch('%s.configure_ua' % MPATH) |
558 | + def test_warn_schema_services_is_not_list_or_dict(self, _cfg, _): |
559 | + """Warn when ubuntu_advantage:enable config is not a list.""" |
560 | validate_cloudconfig_schema( |
561 | - {'ubuntu-advantage': {'commands': {}}}, schema) |
562 | + {'ubuntu_advantage': {'enable': 'needslist'}}, schema) |
563 | self.assertEqual( |
564 | - "WARNING: Invalid config:\nubuntu-advantage.commands: [] is too" |
565 | - " short\nWARNING: Invalid config:\nubuntu-advantage.commands: {}" |
566 | - " does not have enough properties\n", |
567 | + "WARNING: Invalid config:\nubuntu_advantage: 'token' is a" |
568 | + " required property\nubuntu_advantage.enable: 'needslist'" |
569 | + " is not of type 'array'\n", |
570 | self.logs.getvalue()) |
571 | |
572 | - @mock.patch('%s.run_commands' % MPATH) |
573 | - def test_schema_when_commands_are_list_or_dict(self, _): |
574 | - """No warnings when ubuntu-advantage:commands are a list or dict.""" |
575 | - validate_cloudconfig_schema( |
576 | - {'ubuntu-advantage': {'commands': ['valid']}}, schema) |
577 | - validate_cloudconfig_schema( |
578 | - {'ubuntu-advantage': {'commands': {'01': 'also valid'}}}, schema) |
579 | - self.assertEqual('', self.logs.getvalue()) |
580 | - |
581 | - def test_duplicates_are_fine_array_array(self): |
582 | - """Duplicated commands array/array entries are allowed.""" |
583 | - self.assertSchemaValid( |
584 | - {'commands': [["echo", "bye"], ["echo" "bye"]]}, |
585 | - "command entries can be duplicate.") |
586 | - |
587 | - def test_duplicates_are_fine_array_string(self): |
588 | - """Duplicated commands array/string entries are allowed.""" |
589 | - self.assertSchemaValid( |
590 | - {'commands': ["echo bye", "echo bye"]}, |
591 | - "command entries can be duplicate.") |
592 | - |
593 | - def test_duplicates_are_fine_dict_array(self): |
594 | - """Duplicated commands dict/array entries are allowed.""" |
595 | - self.assertSchemaValid( |
596 | - {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}}, |
597 | - "command entries can be duplicate.") |
598 | - |
599 | - def test_duplicates_are_fine_dict_string(self): |
600 | - """Duplicated commands dict/string entries are allowed.""" |
601 | - self.assertSchemaValid( |
602 | - {'commands': {'00': "echo bye", '01': "echo bye"}}, |
603 | - "command entries can be duplicate.") |
604 | - |
605 | |
606 | class TestHandle(CiTestCase): |
607 | |
608 | @@ -205,41 +192,89 @@ class TestHandle(CiTestCase): |
609 | super(TestHandle, self).setUp() |
610 | self.tmp = self.tmp_dir() |
611 | |
612 | - @mock.patch('%s.run_commands' % MPATH) |
613 | @mock.patch('%s.validate_cloudconfig_schema' % MPATH) |
614 | - def test_handle_no_config(self, m_schema, m_run): |
615 | + def test_handle_no_config(self, m_schema): |
616 | """When no ua-related configuration is provided, nothing happens.""" |
617 | cfg = {} |
618 | handle('ua-test', cfg=cfg, cloud=None, log=self.logger, args=None) |
619 | self.assertIn( |
620 | - "DEBUG: Skipping module named ua-test, no 'ubuntu-advantage' key" |
621 | - " in config", |
622 | + "DEBUG: Skipping module named ua-test, no 'ubuntu_advantage'" |
623 | + ' configuration found', |
624 | self.logs.getvalue()) |
625 | m_schema.assert_not_called() |
626 | - m_run.assert_not_called() |
627 | |
628 | + @mock.patch('%s.configure_ua' % MPATH) |
629 | @mock.patch('%s.maybe_install_ua_tools' % MPATH) |
630 | - def test_handle_tries_to_install_ubuntu_advantage_tools(self, m_install): |
631 | + def test_handle_tries_to_install_ubuntu_advantage_tools( |
632 | + self, m_install, m_cfg): |
633 | """If ubuntu_advantage is provided, try installing ua-tools package.""" |
634 | - cfg = {'ubuntu-advantage': {}} |
635 | + cfg = {'ubuntu_advantage': {'token': 'valid'}} |
636 | mycloud = FakeCloud(None) |
637 | handle('nomatter', cfg=cfg, cloud=mycloud, log=self.logger, args=None) |
638 | m_install.assert_called_once_with(mycloud) |
639 | |
640 | + @mock.patch('%s.configure_ua' % MPATH) |
641 | @mock.patch('%s.maybe_install_ua_tools' % MPATH) |
642 | - def test_handle_runs_commands_provided(self, m_install): |
643 | - """When commands are specified as a list, run them.""" |
644 | - outfile = self.tmp_path('output.log', dir=self.tmp) |
645 | + def test_handle_passes_credentials_and_services_to_configure_ua( |
646 | + self, m_install, m_configure_ua): |
647 | + """All ubuntu_advantage config keys are passed to configure_ua.""" |
648 | + cfg = {'ubuntu_advantage': {'token': 'token', 'enable': ['esm']}} |
649 | + handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) |
650 | + m_configure_ua.assert_called_once_with( |
651 | + token='token', enable=['esm']) |
652 | + |
653 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock()) |
654 | + @mock.patch('%s.configure_ua' % MPATH) |
655 | + def test_handle_warns_on_deprecated_ubuntu_advantage_key_w_config( |
656 | + self, m_configure_ua): |
657 | + """Warning when ubuntu-advantage key is present with new config""" |
658 | + cfg = {'ubuntu-advantage': {'token': 'token', 'enable': ['esm']}} |
659 | + handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) |
660 | + self.assertEqual( |
661 | + 'WARNING: Deprecated configuration key "ubuntu-advantage"' |
662 | + ' provided. Expected underscore delimited "ubuntu_advantage";' |
663 | + ' will attempt to continue.', |
664 | + self.logs.getvalue().splitlines()[0]) |
665 | + m_configure_ua.assert_called_once_with( |
666 | + token='token', enable=['esm']) |
667 | + |
668 | + def test_handle_error_on_deprecated_commands_key_dashed(self): |
669 | + """Error when commands is present in ubuntu-advantage key.""" |
670 | + cfg = {'ubuntu-advantage': {'commands': 'nogo'}} |
671 | + with self.assertRaises(RuntimeError) as context_manager: |
672 | + handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) |
673 | + self.assertEqual( |
674 | + 'Deprecated configuration "ubuntu-advantage: commands" provided.' |
675 | + ' Expected "token"', |
676 | + str(context_manager.exception)) |
677 | + |
678 | + def test_handle_error_on_deprecated_commands_key_underscored(self): |
679 | + """Error when commands is present in ubuntu_advantage key.""" |
680 | + cfg = {'ubuntu_advantage': {'commands': 'nogo'}} |
681 | + with self.assertRaises(RuntimeError) as context_manager: |
682 | + handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) |
683 | + self.assertEqual( |
684 | + 'Deprecated configuration "ubuntu-advantage: commands" provided.' |
685 | + ' Expected "token"', |
686 | + str(context_manager.exception)) |
687 | |
688 | + @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock()) |
689 | + @mock.patch('%s.configure_ua' % MPATH) |
690 | + def test_handle_prefers_new_style_config( |
691 | + self, m_configure_ua): |
692 | + """ubuntu_advantage should be preferred over ubuntu-advantage""" |
693 | cfg = { |
694 | - 'ubuntu-advantage': {'commands': ['echo "HI" >> %s' % outfile, |
695 | - 'echo "MOM" >> %s' % outfile]}} |
696 | - mock_path = '%s.sys.stderr' % MPATH |
697 | - with self.allow_subp([CiTestCase.SUBP_SHELL_TRUE]): |
698 | - with mock.patch(mock_path, new_callable=StringIO): |
699 | - handle('nomatter', cfg=cfg, cloud=None, log=self.logger, |
700 | - args=None) |
701 | - self.assertEqual('HI\nMOM\n', util.load_file(outfile)) |
702 | + 'ubuntu-advantage': {'token': 'nope', 'enable': ['wrong']}, |
703 | + 'ubuntu_advantage': {'token': 'token', 'enable': ['esm']}, |
704 | + } |
705 | + handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) |
706 | + self.assertEqual( |
707 | + 'WARNING: Deprecated configuration key "ubuntu-advantage"' |
708 | + ' provided. Expected underscore delimited "ubuntu_advantage";' |
709 | + ' will attempt to continue.', |
710 | + self.logs.getvalue().splitlines()[0]) |
711 | + m_configure_ua.assert_called_once_with( |
712 | + token='token', enable=['esm']) |
713 | |
714 | |
715 | class TestMaybeInstallUATools(CiTestCase): |
716 | @@ -253,7 +288,7 @@ class TestMaybeInstallUATools(CiTestCase): |
717 | @mock.patch('%s.util.which' % MPATH) |
718 | def test_maybe_install_ua_tools_noop_when_ua_tools_present(self, m_which): |
719 | """Do nothing if ubuntu-advantage-tools already exists.""" |
720 | - m_which.return_value = '/usr/bin/ubuntu-advantage' # already installed |
721 | + m_which.return_value = '/usr/bin/ua' # already installed |
722 | distro = mock.MagicMock() |
723 | distro.update_package_sources.side_effect = RuntimeError( |
724 | 'Some apt error') |
Diff between Dan's branch and mine https:/ /paste. ubuntu. com/p/v3gwtq7SQ 9/