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
diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py
index 5e082bd..f488123 100644
--- a/cloudinit/config/cc_ubuntu_advantage.py
+++ b/cloudinit/config/cc_ubuntu_advantage.py
@@ -1,150 +1,143 @@
1# Copyright (C) 2018 Canonical Ltd.
2#
3# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
42
5"""Ubuntu advantage: manage ubuntu-advantage offerings from Canonical."""3"""ubuntu_advantage: Configure Ubuntu Advantage support services"""
64
7import sys
8from textwrap import dedent5from textwrap import dedent
96
10from cloudinit import log as logging7import six
8
11from cloudinit.config.schema import (9from cloudinit.config.schema import (
12 get_schema_doc, validate_cloudconfig_schema)10 get_schema_doc, validate_cloudconfig_schema)
11from cloudinit import log as logging
13from cloudinit.settings import PER_INSTANCE12from cloudinit.settings import PER_INSTANCE
14from cloudinit.subp import prepend_base_command
15from cloudinit import util13from cloudinit import util
1614
1715
18distros = ['ubuntu']16UA_URL = 'https://ubuntu.com/advantage'
19frequency = PER_INSTANCE
2017
21LOG = logging.getLogger(__name__)18distros = ['ubuntu']
2219
23schema = {20schema = {
24 'id': 'cc_ubuntu_advantage',21 'id': 'cc_ubuntu_advantage',
25 'name': 'Ubuntu Advantage',22 'name': 'Ubuntu Advantage',
26 'title': 'Install, configure and manage ubuntu-advantage offerings',23 'title': 'Configure Ubuntu Advantage support services',
27 'description': dedent("""\24 'description': dedent("""\
28 This module provides configuration options to setup ubuntu-advantage25 Attach machine to an existing Ubuntu Advantage support contract and
29 subscriptions.26 enable or disable support services such as Livepatch, ESM,
3027 FIPS and FIPS Updates. When attaching a machine to Ubuntu Advantage,
31 .. note::28 one can also specify services to enable. When the 'enable'
32 Both ``commands`` value can be either a dictionary or a list. If29 list is present, any named service will be enabled and all absent
33 the configuration provided is a dictionary, the keys are only used30 services will remain disabled.
34 to order the execution of the commands and the dictionary is31
35 merged with any vendor-data ubuntu-advantage configuration32 Note that when enabling FIPS or FIPS updates you will need to schedule
36 provided. If a ``commands`` is provided as a list, any vendor-data33 a reboot to ensure the machine is running the FIPS-compliant kernel.
37 ubuntu-advantage ``commands`` are ignored.34 See :ref:`Power State Change` for information on how to configure
3835 cloud-init to perform this reboot.
39 Ubuntu-advantage ``commands`` is a dictionary or list of
40 ubuntu-advantage commands to run on the deployed machine.
41 These commands can be used to enable or disable subscriptions to
42 various ubuntu-advantage products. See 'man ubuntu-advantage' for more
43 information on supported subcommands.
44
45 .. note::
46 Each command item can be a string or list. If the item is a list,
47 'ubuntu-advantage' can be omitted and it will automatically be
48 inserted as part of the command.
49 """),36 """),
50 'distros': distros,37 'distros': distros,
51 'examples': [dedent("""\38 'examples': [dedent("""\
52 # Enable Extended Security Maintenance using your service auth token39 # Attach the machine to a Ubuntu Advantage support contract with a
40 # UA contract token obtained from %s.
41 ubuntu_advantage:
42 token: <ua_contract_token>
43 """ % UA_URL), dedent("""\
44 # Attach the machine to an Ubuntu Advantage support contract enabling
45 # only fips and esm services. Services will only be enabled if
46 # the environment supports said service. Otherwise warnings will
47 # be logged for incompatible services specified.
53 ubuntu-advantage:48 ubuntu-advantage:
54 commands:49 token: <ua_contract_token>
55 00: ubuntu-advantage enable-esm <token>50 enable:
51 - fips
52 - esm
56 """), dedent("""\53 """), dedent("""\
57 # Enable livepatch by providing your livepatch token54 # Attach the machine to an Ubuntu Advantage support contract and enable
55 # the FIPS service. Perform a reboot once cloud-init has
56 # completed.
57 power_state:
58 mode: reboot
58 ubuntu-advantage:59 ubuntu-advantage:
59 commands:60 token: <ua_contract_token>
60 00: ubuntu-advantage enable-livepatch <livepatch-token>61 enable:
6162 - fips
62 """), dedent("""\63 """)],
63 # Convenience: the ubuntu-advantage command can be omitted when
64 # specifying commands as a list and 'ubuntu-advantage' will
65 # automatically be prepended.
66 # The following commands are equivalent
67 ubuntu-advantage:
68 commands:
69 00: ['enable-livepatch', 'my-token']
70 01: ['ubuntu-advantage', 'enable-livepatch', 'my-token']
71 02: ubuntu-advantage enable-livepatch my-token
72 03: 'ubuntu-advantage enable-livepatch my-token'
73 """)],
74 'frequency': PER_INSTANCE,64 'frequency': PER_INSTANCE,
75 'type': 'object',65 'type': 'object',
76 'properties': {66 'properties': {
77 'ubuntu-advantage': {67 'ubuntu_advantage': {
78 'type': 'object',68 'type': 'object',
79 'properties': {69 'properties': {
80 'commands': {70 'enable': {
81 'type': ['object', 'array'], # Array of strings or dict71 'type': 'array',
82 'items': {72 'items': {'type': 'string'},
83 'oneOf': [73 },
84 {'type': 'array', 'items': {'type': 'string'}},74 'token': {
85 {'type': 'string'}]75 'type': 'string',
86 },76 'description': (
87 'additionalItems': False, # Reject non-string & non-list77 'A contract token obtained from %s.' % UA_URL)
88 'minItems': 1,
89 'minProperties': 1,
90 }78 }
91 },79 },
92 'additionalProperties': False, # Reject keys not in schema80 'required': ['token'],
93 'required': ['commands']81 'additionalProperties': False
94 }82 }
95 }83 }
96}84}
9785
98# TODO schema for 'assertions' and 'commands' are too permissive at the moment.
99# Once python-jsonschema supports schema draft 6 add support for arbitrary
100# object keys with 'patternProperties' constraint to validate string values.
101
102__doc__ = get_schema_doc(schema) # Supplement python help()86__doc__ = get_schema_doc(schema) # Supplement python help()
10387
104UA_CMD = "ubuntu-advantage"88LOG = logging.getLogger(__name__)
105
106
107def run_commands(commands):
108 """Run the commands provided in ubuntu-advantage:commands config.
10989
110 Commands are run individually. Any errors are collected and reported
111 after attempting all commands.
11290
113 @param commands: A list or dict containing commands to run. Keys of a91def configure_ua(token=None, enable=None):
114 dict will be used to order the commands provided as dict values.92 """Call ua commandline client to attach or enable services."""
115 """93 error = None
116 if not commands:94 if not token:
117 return95 error = ('ubuntu_advantage: token must be provided')
118 LOG.debug('Running user-provided ubuntu-advantage commands')96 LOG.error(error)
119 if isinstance(commands, dict):97 raise RuntimeError(error)
120 # Sort commands based on dictionary key98
121 commands = [v for _, v in sorted(commands.items())]99 if enable is None:
122 elif not isinstance(commands, list):100 enable = []
123 raise TypeError(101 elif isinstance(enable, six.string_types):
124 'commands parameter was not a list or dict: {commands}'.format(102 LOG.warning('ubuntu_advantage: enable should be a list, not'
125 commands=commands))103 ' a string; treating as a single enable')
126104 enable = [enable]
127 fixed_ua_commands = prepend_base_command('ubuntu-advantage', commands)105 elif not isinstance(enable, list):
128106 LOG.warning('ubuntu_advantage: enable should be a list, not'
129 cmd_failures = []107 ' a %s; skipping enabling services',
130 for command in fixed_ua_commands:108 type(enable).__name__)
131 shell = isinstance(command, str)109 enable = []
132 try:110
133 util.subp(command, shell=shell, status_cb=sys.stderr.write)111 attach_cmd = ['ua', 'attach', token]
134 except util.ProcessExecutionError as e:112 LOG.debug('Attaching to Ubuntu Advantage. %s', ' '.join(attach_cmd))
135 cmd_failures.append(str(e))113 try:
136 if cmd_failures:114 util.subp(attach_cmd)
137 msg = (115 except util.ProcessExecutionError as e:
138 'Failures running ubuntu-advantage commands:\n'116 msg = 'Failure attaching Ubuntu Advantage:\n{error}'.format(
139 '{cmd_failures}'.format(117 error=str(e))
140 cmd_failures=cmd_failures))
141 util.logexc(LOG, msg)118 util.logexc(LOG, msg)
142 raise RuntimeError(msg)119 raise RuntimeError(msg)
120 enable_errors = []
121 for service in enable:
122 try:
123 cmd = ['ua', 'enable', service]
124 util.subp(cmd, capture=True)
125 except util.ProcessExecutionError as e:
126 enable_errors.append((service, e))
127 if enable_errors:
128 for service, error in enable_errors:
129 msg = 'Failure enabling "{service}":\n{error}'.format(
130 service=service, error=str(error))
131 util.logexc(LOG, msg)
132 raise RuntimeError(
133 'Failure enabling Ubuntu Advantage service(s): {}'.format(
134 ', '.join('"{}"'.format(service)
135 for service, _ in enable_errors)))
143136
144137
145def maybe_install_ua_tools(cloud):138def maybe_install_ua_tools(cloud):
146 """Install ubuntu-advantage-tools if not present."""139 """Install ubuntu-advantage-tools if not present."""
147 if util.which('ubuntu-advantage'):140 if util.which('ua'):
148 return141 return
149 try:142 try:
150 cloud.distro.update_package_sources()143 cloud.distro.update_package_sources()
@@ -159,14 +152,28 @@ def maybe_install_ua_tools(cloud):
159152
160153
161def handle(name, cfg, cloud, log, args):154def handle(name, cfg, cloud, log, args):
162 cfgin = cfg.get('ubuntu-advantage')155 ua_section = None
163 if cfgin is None:156 if 'ubuntu-advantage' in cfg:
164 LOG.debug(("Skipping module named %s,"157 LOG.warning('Deprecated configuration key "ubuntu-advantage" provided.'
165 " no 'ubuntu-advantage' key in configuration"), name)158 ' Expected underscore delimited "ubuntu_advantage"; will'
159 ' attempt to continue.')
160 ua_section = cfg['ubuntu-advantage']
161 if 'ubuntu_advantage' in cfg:
162 ua_section = cfg['ubuntu_advantage']
163 if ua_section is None:
164 LOG.debug("Skipping module named %s,"
165 " no 'ubuntu_advantage' configuration found", name)
166 return166 return
167
168 validate_cloudconfig_schema(cfg, schema)167 validate_cloudconfig_schema(cfg, schema)
168 if 'commands' in ua_section:
169 msg = (
170 'Deprecated configuration "ubuntu-advantage: commands" provided.'
171 ' Expected "token"')
172 LOG.error(msg)
173 raise RuntimeError(msg)
174
169 maybe_install_ua_tools(cloud)175 maybe_install_ua_tools(cloud)
170 run_commands(cfgin.get('commands', []))176 configure_ua(token=ua_section.get('token'),
177 enable=ua_section.get('enable'))
171178
172# vi: ts=4 expandtab179# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py
index b7cf9be..8c4161e 100644
--- a/cloudinit/config/tests/test_ubuntu_advantage.py
+++ b/cloudinit/config/tests/test_ubuntu_advantage.py
@@ -1,10 +1,7 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import re
4from six import StringIO
5
6from cloudinit.config.cc_ubuntu_advantage import (3from cloudinit.config.cc_ubuntu_advantage import (
7 handle, maybe_install_ua_tools, run_commands, schema)4 configure_ua, handle, maybe_install_ua_tools, schema)
8from cloudinit.config.schema import validate_cloudconfig_schema5from cloudinit.config.schema import validate_cloudconfig_schema
9from cloudinit import util6from cloudinit import util
10from cloudinit.tests.helpers import (7from cloudinit.tests.helpers import (
@@ -20,90 +17,120 @@ class FakeCloud(object):
20 self.distro = distro17 self.distro = distro
2118
2219
23class TestRunCommands(CiTestCase):20class TestConfigureUA(CiTestCase):
2421
25 with_logs = True22 with_logs = True
26 allowed_subp = [CiTestCase.SUBP_SHELL_TRUE]23 allowed_subp = [CiTestCase.SUBP_SHELL_TRUE]
2724
28 def setUp(self):25 def setUp(self):
29 super(TestRunCommands, self).setUp()26 super(TestConfigureUA, self).setUp()
30 self.tmp = self.tmp_dir()27 self.tmp = self.tmp_dir()
3128
32 @mock.patch('%s.util.subp' % MPATH)29 @mock.patch('%s.util.subp' % MPATH)
33 def test_run_commands_on_empty_list(self, m_subp):30 def test_configure_ua_attach_error(self, m_subp):
34 """When provided with an empty list, run_commands does nothing."""31 """Errors from ua attach command are raised."""
35 run_commands([])32 m_subp.side_effect = util.ProcessExecutionError(
36 self.assertEqual('', self.logs.getvalue())33 'Invalid token SomeToken')
37 m_subp.assert_not_called()34 with self.assertRaises(RuntimeError) as context_manager:
3835 configure_ua(token='SomeToken')
39 def test_run_commands_on_non_list_or_dict(self):
40 """When provided an invalid type, run_commands raises an error."""
41 with self.assertRaises(TypeError) as context_manager:
42 run_commands(commands="I'm Not Valid")
43 self.assertEqual(36 self.assertEqual(
44 "commands parameter was not a list or dict: I'm Not Valid",37 'Failure attaching Ubuntu Advantage:\nUnexpected error while'
38 ' running command.\nCommand: -\nExit code: -\nReason: -\n'
39 'Stdout: Invalid token SomeToken\nStderr: -',
45 str(context_manager.exception))40 str(context_manager.exception))
4641
47 def test_run_command_logs_commands_and_exit_codes_to_stderr(self):42 @mock.patch('%s.util.subp' % MPATH)
48 """All exit codes are logged to stderr."""43 def test_configure_ua_attach_with_token(self, m_subp):
49 outfile = self.tmp_path('output.log', dir=self.tmp)44 """When token is provided, attach the machine to ua using the token."""
5045 configure_ua(token='SomeToken')
51 cmd1 = 'echo "HI" >> %s' % outfile46 m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken'])
52 cmd2 = 'bogus command'47 self.assertEqual(
53 cmd3 = 'echo "MOM" >> %s' % outfile48 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
54 commands = [cmd1, cmd2, cmd3]49 self.logs.getvalue())
5550
56 mock_path = '%s.sys.stderr' % MPATH51 @mock.patch('%s.util.subp' % MPATH)
57 with mock.patch(mock_path, new_callable=StringIO) as m_stderr:52 def test_configure_ua_attach_on_service_error(self, m_subp):
58 with self.assertRaises(RuntimeError) as context_manager:53 """all services should be enabled and then any failures raised"""
59 run_commands(commands=commands)
60
61 self.assertIsNotNone(
62 re.search(r'bogus: (command )?not found',
63 str(context_manager.exception)),
64 msg='Expected bogus command not found')
65 expected_stderr_log = '\n'.join([
66 'Begin run command: {cmd}'.format(cmd=cmd1),
67 'End run command: exit(0)',
68 'Begin run command: {cmd}'.format(cmd=cmd2),
69 'ERROR: End run command: exit(127)',
70 'Begin run command: {cmd}'.format(cmd=cmd3),
71 'End run command: exit(0)\n'])
72 self.assertEqual(expected_stderr_log, m_stderr.getvalue())
73
74 def test_run_command_as_lists(self):
75 """When commands are specified as a list, run them in order."""
76 outfile = self.tmp_path('output.log', dir=self.tmp)
77
78 cmd1 = 'echo "HI" >> %s' % outfile
79 cmd2 = 'echo "MOM" >> %s' % outfile
80 commands = [cmd1, cmd2]
81 with mock.patch('%s.sys.stderr' % MPATH, new_callable=StringIO):
82 run_commands(commands=commands)
8354
55 def fake_subp(cmd, capture=None):
56 fail_cmds = [['ua', 'enable', svc] for svc in ['esm', 'cc']]
57 if cmd in fail_cmds and capture:
58 svc = cmd[-1]
59 raise util.ProcessExecutionError(
60 'Invalid {} credentials'.format(svc.upper()))
61
62 m_subp.side_effect = fake_subp
63
64 with self.assertRaises(RuntimeError) as context_manager:
65 configure_ua(token='SomeToken', enable=['esm', 'cc', 'fips'])
66 self.assertEqual(
67 m_subp.call_args_list,
68 [mock.call(['ua', 'attach', 'SomeToken']),
69 mock.call(['ua', 'enable', 'esm'], capture=True),
70 mock.call(['ua', 'enable', 'cc'], capture=True),
71 mock.call(['ua', 'enable', 'fips'], capture=True)])
84 self.assertIn(72 self.assertIn(
85 'DEBUG: Running user-provided ubuntu-advantage commands',73 'WARNING: Failure enabling "esm":\nUnexpected error'
74 ' while running command.\nCommand: -\nExit code: -\nReason: -\n'
75 'Stdout: Invalid ESM credentials\nStderr: -\n',
86 self.logs.getvalue())76 self.logs.getvalue())
87 self.assertEqual('HI\nMOM\n', util.load_file(outfile))
88 self.assertIn(77 self.assertIn(
89 'WARNING: Non-ubuntu-advantage commands in ubuntu-advantage'78 'WARNING: Failure enabling "cc":\nUnexpected error'
90 ' config:',79 ' while running command.\nCommand: -\nExit code: -\nReason: -\n'
80 'Stdout: Invalid CC credentials\nStderr: -\n',
81 self.logs.getvalue())
82 self.assertEqual(
83 'Failure enabling Ubuntu Advantage service(s): "esm", "cc"',
84 str(context_manager.exception))
85
86 @mock.patch('%s.util.subp' % MPATH)
87 def test_configure_ua_attach_with_empty_services(self, m_subp):
88 """When services is an empty list, do not auto-enable attach."""
89 configure_ua(token='SomeToken', enable=[])
90 m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken'])
91 self.assertEqual(
92 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
91 self.logs.getvalue())93 self.logs.getvalue())
9294
93 def test_run_command_dict_sorted_as_command_script(self):95 @mock.patch('%s.util.subp' % MPATH)
94 """When commands are a dict, sort them and run."""96 def test_configure_ua_attach_with_specific_services(self, m_subp):
95 outfile = self.tmp_path('output.log', dir=self.tmp)97 """When services a list, only enable specific services."""
96 cmd1 = 'echo "HI" >> %s' % outfile98 configure_ua(token='SomeToken', enable=['fips'])
97 cmd2 = 'echo "MOM" >> %s' % outfile99 self.assertEqual(
98 commands = {'02': cmd1, '01': cmd2}100 m_subp.call_args_list,
99 with mock.patch('%s.sys.stderr' % MPATH, new_callable=StringIO):101 [mock.call(['ua', 'attach', 'SomeToken']),
100 run_commands(commands=commands)102 mock.call(['ua', 'enable', 'fips'], capture=True)])
103 self.assertEqual(
104 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
105 self.logs.getvalue())
106
107 @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock())
108 @mock.patch('%s.util.subp' % MPATH)
109 def test_configure_ua_attach_with_string_services(self, m_subp):
110 """When services a string, treat as singleton list and warn"""
111 configure_ua(token='SomeToken', enable='fips')
112 self.assertEqual(
113 m_subp.call_args_list,
114 [mock.call(['ua', 'attach', 'SomeToken']),
115 mock.call(['ua', 'enable', 'fips'], capture=True)])
116 self.assertEqual(
117 'WARNING: ubuntu_advantage: enable should be a list, not a'
118 ' string; treating as a single enable\n'
119 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
120 self.logs.getvalue())
101121
102 expected_messages = [122 @mock.patch('%s.util.subp' % MPATH)
103 'DEBUG: Running user-provided ubuntu-advantage commands']123 def test_configure_ua_attach_with_weird_services(self, m_subp):
104 for message in expected_messages:124 """When services not string or list, warn but still attach"""
105 self.assertIn(message, self.logs.getvalue())125 configure_ua(token='SomeToken', enable={'deffo': 'wont work'})
106 self.assertEqual('MOM\nHI\n', util.load_file(outfile))126 self.assertEqual(
127 m_subp.call_args_list,
128 [mock.call(['ua', 'attach', 'SomeToken'])])
129 self.assertEqual(
130 'WARNING: ubuntu_advantage: enable should be a list, not a'
131 ' dict; skipping enabling services\n'
132 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n',
133 self.logs.getvalue())
107134
108135
109@skipUnlessJsonSchema()136@skipUnlessJsonSchema()
@@ -112,90 +139,50 @@ class TestSchema(CiTestCase, SchemaTestCaseMixin):
112 with_logs = True139 with_logs = True
113 schema = schema140 schema = schema
114141
115 def test_schema_warns_on_ubuntu_advantage_not_as_dict(self):142 @mock.patch('%s.maybe_install_ua_tools' % MPATH)
116 """If ubuntu-advantage configuration is not a dict, emit a warning."""143 @mock.patch('%s.configure_ua' % MPATH)
117 validate_cloudconfig_schema({'ubuntu-advantage': 'wrong type'}, schema)144 def test_schema_warns_on_ubuntu_advantage_not_dict(self, _cfg, _):
145 """If ubuntu_advantage configuration is not a dict, emit a warning."""
146 validate_cloudconfig_schema({'ubuntu_advantage': 'wrong type'}, schema)
118 self.assertEqual(147 self.assertEqual(
119 "WARNING: Invalid config:\nubuntu-advantage: 'wrong type' is not"148 "WARNING: Invalid config:\nubuntu_advantage: 'wrong type' is not"
120 " of type 'object'\n",149 " of type 'object'\n",
121 self.logs.getvalue())150 self.logs.getvalue())
122151
123 @mock.patch('%s.run_commands' % MPATH)152 @mock.patch('%s.maybe_install_ua_tools' % MPATH)
124 def test_schema_disallows_unknown_keys(self, _):153 @mock.patch('%s.configure_ua' % MPATH)
125 """Unknown keys in ubuntu-advantage configuration emit warnings."""154 def test_schema_disallows_unknown_keys(self, _cfg, _):
155 """Unknown keys in ubuntu_advantage configuration emit warnings."""
126 validate_cloudconfig_schema(156 validate_cloudconfig_schema(
127 {'ubuntu-advantage': {'commands': ['ls'], 'invalid-key': ''}},157 {'ubuntu_advantage': {'token': 'winner', 'invalid-key': ''}},
128 schema)158 schema)
129 self.assertIn(159 self.assertIn(
130 'WARNING: Invalid config:\nubuntu-advantage: Additional properties'160 'WARNING: Invalid config:\nubuntu_advantage: Additional properties'
131 " are not allowed ('invalid-key' was unexpected)",161 " are not allowed ('invalid-key' was unexpected)",
132 self.logs.getvalue())162 self.logs.getvalue())
133163
134 def test_warn_schema_requires_commands(self):164 @mock.patch('%s.maybe_install_ua_tools' % MPATH)
135 """Warn when ubuntu-advantage configuration lacks commands."""165 @mock.patch('%s.configure_ua' % MPATH)
136 validate_cloudconfig_schema(166 def test_warn_schema_requires_token(self, _cfg, _):
137 {'ubuntu-advantage': {}}, schema)167 """Warn if ubuntu_advantage configuration lacks token."""
138 self.assertEqual(
139 "WARNING: Invalid config:\nubuntu-advantage: 'commands' is a"
140 " required property\n",
141 self.logs.getvalue())
142
143 @mock.patch('%s.run_commands' % MPATH)
144 def test_warn_schema_commands_is_not_list_or_dict(self, _):
145 """Warn when ubuntu-advantage:commands config is not a list or dict."""
146 validate_cloudconfig_schema(168 validate_cloudconfig_schema(
147 {'ubuntu-advantage': {'commands': 'broken'}}, schema)169 {'ubuntu_advantage': {'enable': ['esm']}}, schema)
148 self.assertEqual(170 self.assertEqual(
149 "WARNING: Invalid config:\nubuntu-advantage.commands: 'broken' is"171 "WARNING: Invalid config:\nubuntu_advantage:"
150 " not of type 'object', 'array'\n",172 " 'token' is a required property\n", self.logs.getvalue())
151 self.logs.getvalue())
152173
153 @mock.patch('%s.run_commands' % MPATH)174 @mock.patch('%s.maybe_install_ua_tools' % MPATH)
154 def test_warn_schema_when_commands_is_empty(self, _):175 @mock.patch('%s.configure_ua' % MPATH)
155 """Emit warnings when ubuntu-advantage:commands is empty."""176 def test_warn_schema_services_is_not_list_or_dict(self, _cfg, _):
156 validate_cloudconfig_schema(177 """Warn when ubuntu_advantage:enable config is not a list."""
157 {'ubuntu-advantage': {'commands': []}}, schema)
158 validate_cloudconfig_schema(178 validate_cloudconfig_schema(
159 {'ubuntu-advantage': {'commands': {}}}, schema)179 {'ubuntu_advantage': {'enable': 'needslist'}}, schema)
160 self.assertEqual(180 self.assertEqual(
161 "WARNING: Invalid config:\nubuntu-advantage.commands: [] is too"181 "WARNING: Invalid config:\nubuntu_advantage: 'token' is a"
162 " short\nWARNING: Invalid config:\nubuntu-advantage.commands: {}"182 " required property\nubuntu_advantage.enable: 'needslist'"
163 " does not have enough properties\n",183 " is not of type 'array'\n",
164 self.logs.getvalue())184 self.logs.getvalue())
165185
166 @mock.patch('%s.run_commands' % MPATH)
167 def test_schema_when_commands_are_list_or_dict(self, _):
168 """No warnings when ubuntu-advantage:commands are a list or dict."""
169 validate_cloudconfig_schema(
170 {'ubuntu-advantage': {'commands': ['valid']}}, schema)
171 validate_cloudconfig_schema(
172 {'ubuntu-advantage': {'commands': {'01': 'also valid'}}}, schema)
173 self.assertEqual('', self.logs.getvalue())
174
175 def test_duplicates_are_fine_array_array(self):
176 """Duplicated commands array/array entries are allowed."""
177 self.assertSchemaValid(
178 {'commands': [["echo", "bye"], ["echo" "bye"]]},
179 "command entries can be duplicate.")
180
181 def test_duplicates_are_fine_array_string(self):
182 """Duplicated commands array/string entries are allowed."""
183 self.assertSchemaValid(
184 {'commands': ["echo bye", "echo bye"]},
185 "command entries can be duplicate.")
186
187 def test_duplicates_are_fine_dict_array(self):
188 """Duplicated commands dict/array entries are allowed."""
189 self.assertSchemaValid(
190 {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}},
191 "command entries can be duplicate.")
192
193 def test_duplicates_are_fine_dict_string(self):
194 """Duplicated commands dict/string entries are allowed."""
195 self.assertSchemaValid(
196 {'commands': {'00': "echo bye", '01': "echo bye"}},
197 "command entries can be duplicate.")
198
199186
200class TestHandle(CiTestCase):187class TestHandle(CiTestCase):
201188
@@ -205,41 +192,89 @@ class TestHandle(CiTestCase):
205 super(TestHandle, self).setUp()192 super(TestHandle, self).setUp()
206 self.tmp = self.tmp_dir()193 self.tmp = self.tmp_dir()
207194
208 @mock.patch('%s.run_commands' % MPATH)
209 @mock.patch('%s.validate_cloudconfig_schema' % MPATH)195 @mock.patch('%s.validate_cloudconfig_schema' % MPATH)
210 def test_handle_no_config(self, m_schema, m_run):196 def test_handle_no_config(self, m_schema):
211 """When no ua-related configuration is provided, nothing happens."""197 """When no ua-related configuration is provided, nothing happens."""
212 cfg = {}198 cfg = {}
213 handle('ua-test', cfg=cfg, cloud=None, log=self.logger, args=None)199 handle('ua-test', cfg=cfg, cloud=None, log=self.logger, args=None)
214 self.assertIn(200 self.assertIn(
215 "DEBUG: Skipping module named ua-test, no 'ubuntu-advantage' key"201 "DEBUG: Skipping module named ua-test, no 'ubuntu_advantage'"
216 " in config",202 ' configuration found',
217 self.logs.getvalue())203 self.logs.getvalue())
218 m_schema.assert_not_called()204 m_schema.assert_not_called()
219 m_run.assert_not_called()
220205
206 @mock.patch('%s.configure_ua' % MPATH)
221 @mock.patch('%s.maybe_install_ua_tools' % MPATH)207 @mock.patch('%s.maybe_install_ua_tools' % MPATH)
222 def test_handle_tries_to_install_ubuntu_advantage_tools(self, m_install):208 def test_handle_tries_to_install_ubuntu_advantage_tools(
209 self, m_install, m_cfg):
223 """If ubuntu_advantage is provided, try installing ua-tools package."""210 """If ubuntu_advantage is provided, try installing ua-tools package."""
224 cfg = {'ubuntu-advantage': {}}211 cfg = {'ubuntu_advantage': {'token': 'valid'}}
225 mycloud = FakeCloud(None)212 mycloud = FakeCloud(None)
226 handle('nomatter', cfg=cfg, cloud=mycloud, log=self.logger, args=None)213 handle('nomatter', cfg=cfg, cloud=mycloud, log=self.logger, args=None)
227 m_install.assert_called_once_with(mycloud)214 m_install.assert_called_once_with(mycloud)
228215
216 @mock.patch('%s.configure_ua' % MPATH)
229 @mock.patch('%s.maybe_install_ua_tools' % MPATH)217 @mock.patch('%s.maybe_install_ua_tools' % MPATH)
230 def test_handle_runs_commands_provided(self, m_install):218 def test_handle_passes_credentials_and_services_to_configure_ua(
231 """When commands are specified as a list, run them."""219 self, m_install, m_configure_ua):
232 outfile = self.tmp_path('output.log', dir=self.tmp)220 """All ubuntu_advantage config keys are passed to configure_ua."""
221 cfg = {'ubuntu_advantage': {'token': 'token', 'enable': ['esm']}}
222 handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None)
223 m_configure_ua.assert_called_once_with(
224 token='token', enable=['esm'])
225
226 @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock())
227 @mock.patch('%s.configure_ua' % MPATH)
228 def test_handle_warns_on_deprecated_ubuntu_advantage_key_w_config(
229 self, m_configure_ua):
230 """Warning when ubuntu-advantage key is present with new config"""
231 cfg = {'ubuntu-advantage': {'token': 'token', 'enable': ['esm']}}
232 handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None)
233 self.assertEqual(
234 'WARNING: Deprecated configuration key "ubuntu-advantage"'
235 ' provided. Expected underscore delimited "ubuntu_advantage";'
236 ' will attempt to continue.',
237 self.logs.getvalue().splitlines()[0])
238 m_configure_ua.assert_called_once_with(
239 token='token', enable=['esm'])
240
241 def test_handle_error_on_deprecated_commands_key_dashed(self):
242 """Error when commands is present in ubuntu-advantage key."""
243 cfg = {'ubuntu-advantage': {'commands': 'nogo'}}
244 with self.assertRaises(RuntimeError) as context_manager:
245 handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None)
246 self.assertEqual(
247 'Deprecated configuration "ubuntu-advantage: commands" provided.'
248 ' Expected "token"',
249 str(context_manager.exception))
250
251 def test_handle_error_on_deprecated_commands_key_underscored(self):
252 """Error when commands is present in ubuntu_advantage key."""
253 cfg = {'ubuntu_advantage': {'commands': 'nogo'}}
254 with self.assertRaises(RuntimeError) as context_manager:
255 handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None)
256 self.assertEqual(
257 'Deprecated configuration "ubuntu-advantage: commands" provided.'
258 ' Expected "token"',
259 str(context_manager.exception))
233260
261 @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock())
262 @mock.patch('%s.configure_ua' % MPATH)
263 def test_handle_prefers_new_style_config(
264 self, m_configure_ua):
265 """ubuntu_advantage should be preferred over ubuntu-advantage"""
234 cfg = {266 cfg = {
235 'ubuntu-advantage': {'commands': ['echo "HI" >> %s' % outfile,267 'ubuntu-advantage': {'token': 'nope', 'enable': ['wrong']},
236 'echo "MOM" >> %s' % outfile]}}268 'ubuntu_advantage': {'token': 'token', 'enable': ['esm']},
237 mock_path = '%s.sys.stderr' % MPATH269 }
238 with self.allow_subp([CiTestCase.SUBP_SHELL_TRUE]):270 handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None)
239 with mock.patch(mock_path, new_callable=StringIO):271 self.assertEqual(
240 handle('nomatter', cfg=cfg, cloud=None, log=self.logger,272 'WARNING: Deprecated configuration key "ubuntu-advantage"'
241 args=None)273 ' provided. Expected underscore delimited "ubuntu_advantage";'
242 self.assertEqual('HI\nMOM\n', util.load_file(outfile))274 ' will attempt to continue.',
275 self.logs.getvalue().splitlines()[0])
276 m_configure_ua.assert_called_once_with(
277 token='token', enable=['esm'])
243278
244279
245class TestMaybeInstallUATools(CiTestCase):280class TestMaybeInstallUATools(CiTestCase):
@@ -253,7 +288,7 @@ class TestMaybeInstallUATools(CiTestCase):
253 @mock.patch('%s.util.which' % MPATH)288 @mock.patch('%s.util.which' % MPATH)
254 def test_maybe_install_ua_tools_noop_when_ua_tools_present(self, m_which):289 def test_maybe_install_ua_tools_noop_when_ua_tools_present(self, m_which):
255 """Do nothing if ubuntu-advantage-tools already exists."""290 """Do nothing if ubuntu-advantage-tools already exists."""
256 m_which.return_value = '/usr/bin/ubuntu-advantage' # already installed291 m_which.return_value = '/usr/bin/ua' # already installed
257 distro = mock.MagicMock()292 distro = mock.MagicMock()
258 distro.update_package_sources.side_effect = RuntimeError(293 distro.update_package_sources.side_effect = RuntimeError(
259 'Some apt error')294 'Some apt error')

Subscribers

People subscribed via source and target branches