Merge ~powersj/cloud-init:feature/cc-uaclient into cloud-init:master

Proposed by Joshua Powers
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)
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-advantage-tools version 19 has a different command line
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: 'thisismyubuntuadvantagetoken'
  enable: [esm, fips, livepatch]

Co-Authored-By: Daniel Watkins <email address hidden>
Author: Chad Smith <email address hidden>

To post a comment you must log in.
Revision history for this message
Joshua Powers (powersj) wrote :

Diff between Dan's branch and mine https://paste.ubuntu.com/p/v3gwtq7SQ9/

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:b05832a5c137048031b4484d6c43509112323ebc
https://jenkins.ubuntu.com/server/job/cloud-init-ci/665/
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://jenkins.ubuntu.com/server/job/cloud-init-ci/665/rebuild

review: Approve (continuous-integration)
Revision history for this message
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: |
    IyBVYnVudHUtQWR2YW50YWdlIGNsaWVudCBjb25maWcgZmlsZS4Kc3NvX2F1dGhfdXJsOiAnaHR0
    cHM6Ly9sb2dpbi51YnVudHUuY29tJwpjb250cmFjdF91cmw6ICdodHRwczovL2NvbnRyYWN0cy5z
    dGFnaW5nLmNhbm9uaWNhbC5jb20nCmRhdGFfZGlyOiAvdmFyL2xpYi91YnVudHUtYWR2YW50YWdl
    CmxvZ19sZXZlbDogaW5mbwpsb2dfZmlsZTogL3Zhci9sb2cvdWJ1bnR1LWFkdmFudGFnZS5sb2cK
  path: /etc/ubuntu-advantage/uaclient.conf

Revision history for this message
Chad Smith (chad.smith) wrote :

This is great. thanks for the bump on this and fixups.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py
2index 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
287diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py
288index 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')

Subscribers

People subscribed via source and target branches