diff options
author | Chad Smith <chad.smith@canonical.com> | 2019-05-12 20:15:01 -0600 |
---|---|---|
committer | Chad Smith <chad.smith@canonical.com> | 2019-05-12 20:16:42 -0600 |
commit | fab67d432edb7102da51580849989f434e383905 (patch) | |
tree | d09c48204bc713232c332b96f7623e7603f0cbd4 | |
parent | cddbcb466ad19b45ce0b78edee7a0fed5e2263c2 (diff) | |
download | cloud-init-git-fab67d432edb7102da51580849989f434e383905.tar.gz |
d/patch: retain ubuntu-advantage cloud-config module at old command line
-rw-r--r-- | debian/patches/series | 1 | ||||
-rw-r--r-- | debian/patches/ubuntu-advantage-revert-tip.patch | 733 |
2 files changed, 734 insertions, 0 deletions
diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 00000000..2284299e --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +ubuntu-advantage-revert-tip.patch diff --git a/debian/patches/ubuntu-advantage-revert-tip.patch b/debian/patches/ubuntu-advantage-revert-tip.patch new file mode 100644 index 00000000..d98423d1 --- /dev/null +++ b/debian/patches/ubuntu-advantage-revert-tip.patch @@ -0,0 +1,733 @@ +Description: Revert upstream changes for ubuntu-advantage-tools v 19.1 + ubuntu-advantage-tools v. 19.1 or later is required for the new + cloud-config module because the two command lines are incompatible. + Cosmic can drop this patch once ubuntu-advantage-tools has been SRU'd >= 19.1 +Author: Chad Smith <chad.smith@canonical.com> +Origin: backport +Bug: https://bugs.launchpad.net/cloud-init/+bug/1828641 +Forwarded: not-needed +Last-Update: 2019-05-10 +Index: cloud-init/cloudinit/config/cc_ubuntu_advantage.py +=================================================================== +--- cloud-init.orig/cloudinit/config/cc_ubuntu_advantage.py ++++ cloud-init/cloudinit/config/cc_ubuntu_advantage.py +@@ -1,143 +1,150 @@ ++# Copyright (C) 2018 Canonical Ltd. ++# + # This file is part of cloud-init. See LICENSE file for license information. + +-"""ubuntu_advantage: Configure Ubuntu Advantage support services""" ++"""Ubuntu advantage: manage ubuntu-advantage offerings from Canonical.""" + ++import sys + from textwrap import dedent + +-import six +- ++from cloudinit import log as logging + from cloudinit.config.schema import ( + get_schema_doc, validate_cloudconfig_schema) +-from cloudinit import log as logging + from cloudinit.settings import PER_INSTANCE ++from cloudinit.subp import prepend_base_command + from cloudinit import util + + +-UA_URL = 'https://ubuntu.com/advantage' +- + distros = ['ubuntu'] ++frequency = PER_INSTANCE ++ ++LOG = logging.getLogger(__name__) + + schema = { + 'id': 'cc_ubuntu_advantage', + 'name': 'Ubuntu Advantage', +- 'title': 'Configure Ubuntu Advantage support services', ++ 'title': 'Install, configure and manage ubuntu-advantage offerings', + 'description': dedent("""\ +- Attach machine to an existing Ubuntu Advantage support contract and +- enable or disable support services such as Livepatch, ESM, +- FIPS and FIPS Updates. When attaching a machine to Ubuntu Advantage, +- one can also specify services to enable. When the 'enable' +- list is present, any named service will be enabled and all absent +- services will remain disabled. +- +- Note that when enabling FIPS or FIPS updates you will need to schedule +- a reboot to ensure the machine is running the FIPS-compliant kernel. +- See :ref:`Power State Change` for information on how to configure +- cloud-init to perform this reboot. ++ This module provides configuration options to setup ubuntu-advantage ++ subscriptions. ++ ++ .. note:: ++ Both ``commands`` value can be either a dictionary or a list. If ++ the configuration provided is a dictionary, the keys are only used ++ to order the execution of the commands and the dictionary is ++ merged with any vendor-data ubuntu-advantage configuration ++ provided. If a ``commands`` is provided as a list, any vendor-data ++ ubuntu-advantage ``commands`` are ignored. ++ ++ Ubuntu-advantage ``commands`` is a dictionary or list of ++ ubuntu-advantage commands to run on the deployed machine. ++ These commands can be used to enable or disable subscriptions to ++ various ubuntu-advantage products. See 'man ubuntu-advantage' for more ++ information on supported subcommands. ++ ++ .. note:: ++ Each command item can be a string or list. If the item is a list, ++ 'ubuntu-advantage' can be omitted and it will automatically be ++ inserted as part of the command. + """), + 'distros': distros, + 'examples': [dedent("""\ +- # Attach the machine to a Ubuntu Advantage support contract with a +- # UA contract token obtained from %s. +- ubuntu_advantage: +- token: <ua_contract_token> +- """ % UA_URL), dedent("""\ +- # Attach the machine to an Ubuntu Advantage support contract enabling +- # only fips and esm services. Services will only be enabled if +- # the environment supports said service. Otherwise warnings will +- # be logged for incompatible services specified. ++ # Enable Extended Security Maintenance using your service auth token ++ ubuntu-advantage: ++ commands: ++ 00: ubuntu-advantage enable-esm <token> ++ """), dedent("""\ ++ # Enable livepatch by providing your livepatch token + ubuntu-advantage: +- token: <ua_contract_token> +- enable: +- - fips +- - esm ++ commands: ++ 00: ubuntu-advantage enable-livepatch <livepatch-token> ++ + """), dedent("""\ +- # Attach the machine to an Ubuntu Advantage support contract and enable +- # the FIPS service. Perform a reboot once cloud-init has +- # completed. +- power_state: +- mode: reboot ++ # Convenience: the ubuntu-advantage command can be omitted when ++ # specifying commands as a list and 'ubuntu-advantage' will ++ # automatically be prepended. ++ # The following commands are equivalent + ubuntu-advantage: +- token: <ua_contract_token> +- enable: +- - fips +- """)], ++ commands: ++ 00: ['enable-livepatch', 'my-token'] ++ 01: ['ubuntu-advantage', 'enable-livepatch', 'my-token'] ++ 02: ubuntu-advantage enable-livepatch my-token ++ 03: 'ubuntu-advantage enable-livepatch my-token' ++ """)], + 'frequency': PER_INSTANCE, + 'type': 'object', + 'properties': { +- 'ubuntu_advantage': { ++ 'ubuntu-advantage': { + 'type': 'object', + 'properties': { +- 'enable': { +- 'type': 'array', +- 'items': {'type': 'string'}, +- }, +- 'token': { +- 'type': 'string', +- 'description': ( +- 'A contract token obtained from %s.' % UA_URL) ++ 'commands': { ++ 'type': ['object', 'array'], # Array of strings or dict ++ 'items': { ++ 'oneOf': [ ++ {'type': 'array', 'items': {'type': 'string'}}, ++ {'type': 'string'}] ++ }, ++ 'additionalItems': False, # Reject non-string & non-list ++ 'minItems': 1, ++ 'minProperties': 1, + } + }, +- 'required': ['token'], +- 'additionalProperties': False ++ 'additionalProperties': False, # Reject keys not in schema ++ 'required': ['commands'] + } + } + } + ++# TODO schema for 'assertions' and 'commands' are too permissive at the moment. ++# Once python-jsonschema supports schema draft 6 add support for arbitrary ++# object keys with 'patternProperties' constraint to validate string values. ++ + __doc__ = get_schema_doc(schema) # Supplement python help() + +-LOG = logging.getLogger(__name__) ++UA_CMD = "ubuntu-advantage" + + +-def configure_ua(token=None, enable=None): +- """Call ua commandline client to attach or enable services.""" +- error = None +- if not token: +- error = ('ubuntu_advantage: token must be provided') +- LOG.error(error) +- raise RuntimeError(error) +- +- if enable is None: +- enable = [] +- elif isinstance(enable, six.string_types): +- LOG.warning('ubuntu_advantage: enable should be a list, not' +- ' a string; treating as a single enable') +- enable = [enable] +- elif not isinstance(enable, list): +- LOG.warning('ubuntu_advantage: enable should be a list, not' +- ' a %s; skipping enabling services', +- type(enable).__name__) +- enable = [] ++def run_commands(commands): ++ """Run the commands provided in ubuntu-advantage:commands config. + +- attach_cmd = ['ua', 'attach', token] +- LOG.debug('Attaching to Ubuntu Advantage. %s', ' '.join(attach_cmd)) +- try: +- util.subp(attach_cmd) +- except util.ProcessExecutionError as e: +- msg = 'Failure attaching Ubuntu Advantage:\n{error}'.format( +- error=str(e)) +- util.logexc(LOG, msg) +- raise RuntimeError(msg) +- enable_errors = [] +- for service in enable: ++ Commands are run individually. Any errors are collected and reported ++ after attempting all commands. ++ ++ @param commands: A list or dict containing commands to run. Keys of a ++ dict will be used to order the commands provided as dict values. ++ """ ++ if not commands: ++ return ++ LOG.debug('Running user-provided ubuntu-advantage commands') ++ if isinstance(commands, dict): ++ # Sort commands based on dictionary key ++ commands = [v for _, v in sorted(commands.items())] ++ elif not isinstance(commands, list): ++ raise TypeError( ++ 'commands parameter was not a list or dict: {commands}'.format( ++ commands=commands)) ++ ++ fixed_ua_commands = prepend_base_command('ubuntu-advantage', commands) ++ ++ cmd_failures = [] ++ for command in fixed_ua_commands: ++ shell = isinstance(command, str) + try: +- cmd = ['ua', 'enable', service] +- util.subp(cmd, capture=True) ++ util.subp(command, shell=shell, status_cb=sys.stderr.write) + except util.ProcessExecutionError as e: +- enable_errors.append((service, e)) +- if enable_errors: +- for service, error in enable_errors: +- msg = 'Failure enabling "{service}":\n{error}'.format( +- service=service, error=str(error)) +- util.logexc(LOG, msg) +- raise RuntimeError( +- 'Failure enabling Ubuntu Advantage service(s): {}'.format( +- ', '.join('"{}"'.format(service) +- for service, _ in enable_errors))) ++ cmd_failures.append(str(e)) ++ if cmd_failures: ++ msg = ( ++ 'Failures running ubuntu-advantage commands:\n' ++ '{cmd_failures}'.format( ++ cmd_failures=cmd_failures)) ++ util.logexc(LOG, msg) ++ raise RuntimeError(msg) + + + def maybe_install_ua_tools(cloud): + """Install ubuntu-advantage-tools if not present.""" +- if util.which('ua'): ++ if util.which('ubuntu-advantage'): + return + try: + cloud.distro.update_package_sources() +@@ -152,28 +159,14 @@ def maybe_install_ua_tools(cloud): + + + def handle(name, cfg, cloud, log, args): +- ua_section = None +- if 'ubuntu-advantage' in cfg: +- LOG.warning('Deprecated configuration key "ubuntu-advantage" provided.' +- ' Expected underscore delimited "ubuntu_advantage"; will' +- ' attempt to continue.') +- ua_section = cfg['ubuntu-advantage'] +- if 'ubuntu_advantage' in cfg: +- ua_section = cfg['ubuntu_advantage'] +- if ua_section is None: +- LOG.debug("Skipping module named %s," +- " no 'ubuntu_advantage' configuration found", name) ++ cfgin = cfg.get('ubuntu-advantage') ++ if cfgin is None: ++ LOG.debug(("Skipping module named %s," ++ " no 'ubuntu-advantage' key in configuration"), name) + return +- validate_cloudconfig_schema(cfg, schema) +- if 'commands' in ua_section: +- msg = ( +- 'Deprecated configuration "ubuntu-advantage: commands" provided.' +- ' Expected "token"') +- LOG.error(msg) +- raise RuntimeError(msg) + ++ validate_cloudconfig_schema(cfg, schema) + maybe_install_ua_tools(cloud) +- configure_ua(token=ua_section.get('token'), +- enable=ua_section.get('enable')) ++ run_commands(cfgin.get('commands', [])) + + # vi: ts=4 expandtab +Index: cloud-init/cloudinit/config/tests/test_ubuntu_advantage.py +=================================================================== +--- cloud-init.orig/cloudinit/config/tests/test_ubuntu_advantage.py ++++ cloud-init/cloudinit/config/tests/test_ubuntu_advantage.py +@@ -1,7 +1,10 @@ + # This file is part of cloud-init. See LICENSE file for license information. + ++import re ++from six import StringIO ++ + from cloudinit.config.cc_ubuntu_advantage import ( +- configure_ua, handle, maybe_install_ua_tools, schema) ++ handle, maybe_install_ua_tools, run_commands, schema) + from cloudinit.config.schema import validate_cloudconfig_schema + from cloudinit import util + from cloudinit.tests.helpers import ( +@@ -17,120 +20,90 @@ class FakeCloud(object): + self.distro = distro + + +-class TestConfigureUA(CiTestCase): ++class TestRunCommands(CiTestCase): + + with_logs = True + allowed_subp = [CiTestCase.SUBP_SHELL_TRUE] + + def setUp(self): +- super(TestConfigureUA, self).setUp() ++ super(TestRunCommands, self).setUp() + self.tmp = self.tmp_dir() + + @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_error(self, m_subp): +- """Errors from ua attach command are raised.""" +- m_subp.side_effect = util.ProcessExecutionError( +- 'Invalid token SomeToken') +- with self.assertRaises(RuntimeError) as context_manager: +- configure_ua(token='SomeToken') ++ def test_run_commands_on_empty_list(self, m_subp): ++ """When provided with an empty list, run_commands does nothing.""" ++ run_commands([]) ++ self.assertEqual('', self.logs.getvalue()) ++ m_subp.assert_not_called() ++ ++ def test_run_commands_on_non_list_or_dict(self): ++ """When provided an invalid type, run_commands raises an error.""" ++ with self.assertRaises(TypeError) as context_manager: ++ run_commands(commands="I'm Not Valid") + self.assertEqual( +- 'Failure attaching Ubuntu Advantage:\nUnexpected error while' +- ' running command.\nCommand: -\nExit code: -\nReason: -\n' +- 'Stdout: Invalid token SomeToken\nStderr: -', ++ "commands parameter was not a list or dict: I'm Not Valid", + str(context_manager.exception)) + +- @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_with_token(self, m_subp): +- """When token is provided, attach the machine to ua using the token.""" +- configure_ua(token='SomeToken') +- m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken']) +- self.assertEqual( +- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', +- self.logs.getvalue()) +- +- @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_on_service_error(self, m_subp): +- """all services should be enabled and then any failures raised""" +- +- def fake_subp(cmd, capture=None): +- fail_cmds = [['ua', 'enable', svc] for svc in ['esm', 'cc']] +- if cmd in fail_cmds and capture: +- svc = cmd[-1] +- raise util.ProcessExecutionError( +- 'Invalid {} credentials'.format(svc.upper())) ++ def test_run_command_logs_commands_and_exit_codes_to_stderr(self): ++ """All exit codes are logged to stderr.""" ++ outfile = self.tmp_path('output.log', dir=self.tmp) ++ ++ cmd1 = 'echo "HI" >> %s' % outfile ++ cmd2 = 'bogus command' ++ cmd3 = 'echo "MOM" >> %s' % outfile ++ commands = [cmd1, cmd2, cmd3] ++ ++ mock_path = '%s.sys.stderr' % MPATH ++ with mock.patch(mock_path, new_callable=StringIO) as m_stderr: ++ with self.assertRaises(RuntimeError) as context_manager: ++ run_commands(commands=commands) ++ ++ self.assertIsNotNone( ++ re.search(r'bogus: (command )?not found', ++ str(context_manager.exception)), ++ msg='Expected bogus command not found') ++ expected_stderr_log = '\n'.join([ ++ 'Begin run command: {cmd}'.format(cmd=cmd1), ++ 'End run command: exit(0)', ++ 'Begin run command: {cmd}'.format(cmd=cmd2), ++ 'ERROR: End run command: exit(127)', ++ 'Begin run command: {cmd}'.format(cmd=cmd3), ++ 'End run command: exit(0)\n']) ++ self.assertEqual(expected_stderr_log, m_stderr.getvalue()) ++ ++ def test_run_command_as_lists(self): ++ """When commands are specified as a list, run them in order.""" ++ outfile = self.tmp_path('output.log', dir=self.tmp) ++ ++ cmd1 = 'echo "HI" >> %s' % outfile ++ cmd2 = 'echo "MOM" >> %s' % outfile ++ commands = [cmd1, cmd2] ++ with mock.patch('%s.sys.stderr' % MPATH, new_callable=StringIO): ++ run_commands(commands=commands) + +- m_subp.side_effect = fake_subp +- +- with self.assertRaises(RuntimeError) as context_manager: +- configure_ua(token='SomeToken', enable=['esm', 'cc', 'fips']) +- self.assertEqual( +- m_subp.call_args_list, +- [mock.call(['ua', 'attach', 'SomeToken']), +- mock.call(['ua', 'enable', 'esm'], capture=True), +- mock.call(['ua', 'enable', 'cc'], capture=True), +- mock.call(['ua', 'enable', 'fips'], capture=True)]) + self.assertIn( +- 'WARNING: Failure enabling "esm":\nUnexpected error' +- ' while running command.\nCommand: -\nExit code: -\nReason: -\n' +- 'Stdout: Invalid ESM credentials\nStderr: -\n', ++ 'DEBUG: Running user-provided ubuntu-advantage commands', + self.logs.getvalue()) ++ self.assertEqual('HI\nMOM\n', util.load_file(outfile)) + self.assertIn( +- 'WARNING: Failure enabling "cc":\nUnexpected error' +- ' while running command.\nCommand: -\nExit code: -\nReason: -\n' +- 'Stdout: Invalid CC credentials\nStderr: -\n', +- self.logs.getvalue()) +- self.assertEqual( +- 'Failure enabling Ubuntu Advantage service(s): "esm", "cc"', +- str(context_manager.exception)) +- +- @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_with_empty_services(self, m_subp): +- """When services is an empty list, do not auto-enable attach.""" +- configure_ua(token='SomeToken', enable=[]) +- m_subp.assert_called_once_with(['ua', 'attach', 'SomeToken']) +- self.assertEqual( +- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', +- self.logs.getvalue()) +- +- @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_with_specific_services(self, m_subp): +- """When services a list, only enable specific services.""" +- configure_ua(token='SomeToken', enable=['fips']) +- self.assertEqual( +- m_subp.call_args_list, +- [mock.call(['ua', 'attach', 'SomeToken']), +- mock.call(['ua', 'enable', 'fips'], capture=True)]) +- self.assertEqual( +- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', +- self.logs.getvalue()) +- +- @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock()) +- @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_with_string_services(self, m_subp): +- """When services a string, treat as singleton list and warn""" +- configure_ua(token='SomeToken', enable='fips') +- self.assertEqual( +- m_subp.call_args_list, +- [mock.call(['ua', 'attach', 'SomeToken']), +- mock.call(['ua', 'enable', 'fips'], capture=True)]) +- self.assertEqual( +- 'WARNING: ubuntu_advantage: enable should be a list, not a' +- ' string; treating as a single enable\n' +- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', ++ 'WARNING: Non-ubuntu-advantage commands in ubuntu-advantage' ++ ' config:', + self.logs.getvalue()) + +- @mock.patch('%s.util.subp' % MPATH) +- def test_configure_ua_attach_with_weird_services(self, m_subp): +- """When services not string or list, warn but still attach""" +- configure_ua(token='SomeToken', enable={'deffo': 'wont work'}) +- self.assertEqual( +- m_subp.call_args_list, +- [mock.call(['ua', 'attach', 'SomeToken'])]) +- self.assertEqual( +- 'WARNING: ubuntu_advantage: enable should be a list, not a' +- ' dict; skipping enabling services\n' +- 'DEBUG: Attaching to Ubuntu Advantage. ua attach SomeToken\n', +- self.logs.getvalue()) ++ def test_run_command_dict_sorted_as_command_script(self): ++ """When commands are a dict, sort them and run.""" ++ outfile = self.tmp_path('output.log', dir=self.tmp) ++ cmd1 = 'echo "HI" >> %s' % outfile ++ cmd2 = 'echo "MOM" >> %s' % outfile ++ commands = {'02': cmd1, '01': cmd2} ++ with mock.patch('%s.sys.stderr' % MPATH, new_callable=StringIO): ++ run_commands(commands=commands) ++ ++ expected_messages = [ ++ 'DEBUG: Running user-provided ubuntu-advantage commands'] ++ for message in expected_messages: ++ self.assertIn(message, self.logs.getvalue()) ++ self.assertEqual('MOM\nHI\n', util.load_file(outfile)) + + + @skipUnlessJsonSchema() +@@ -139,50 +112,90 @@ class TestSchema(CiTestCase, SchemaTestC + with_logs = True + schema = schema + +- @mock.patch('%s.maybe_install_ua_tools' % MPATH) +- @mock.patch('%s.configure_ua' % MPATH) +- def test_schema_warns_on_ubuntu_advantage_not_dict(self, _cfg, _): +- """If ubuntu_advantage configuration is not a dict, emit a warning.""" +- validate_cloudconfig_schema({'ubuntu_advantage': 'wrong type'}, schema) ++ def test_schema_warns_on_ubuntu_advantage_not_as_dict(self): ++ """If ubuntu-advantage configuration is not a dict, emit a warning.""" ++ validate_cloudconfig_schema({'ubuntu-advantage': 'wrong type'}, schema) + self.assertEqual( +- "WARNING: Invalid config:\nubuntu_advantage: 'wrong type' is not" ++ "WARNING: Invalid config:\nubuntu-advantage: 'wrong type' is not" + " of type 'object'\n", + self.logs.getvalue()) + +- @mock.patch('%s.maybe_install_ua_tools' % MPATH) +- @mock.patch('%s.configure_ua' % MPATH) +- def test_schema_disallows_unknown_keys(self, _cfg, _): +- """Unknown keys in ubuntu_advantage configuration emit warnings.""" ++ @mock.patch('%s.run_commands' % MPATH) ++ def test_schema_disallows_unknown_keys(self, _): ++ """Unknown keys in ubuntu-advantage configuration emit warnings.""" + validate_cloudconfig_schema( +- {'ubuntu_advantage': {'token': 'winner', 'invalid-key': ''}}, ++ {'ubuntu-advantage': {'commands': ['ls'], 'invalid-key': ''}}, + schema) + self.assertIn( +- 'WARNING: Invalid config:\nubuntu_advantage: Additional properties' ++ 'WARNING: Invalid config:\nubuntu-advantage: Additional properties' + " are not allowed ('invalid-key' was unexpected)", + self.logs.getvalue()) + +- @mock.patch('%s.maybe_install_ua_tools' % MPATH) +- @mock.patch('%s.configure_ua' % MPATH) +- def test_warn_schema_requires_token(self, _cfg, _): +- """Warn if ubuntu_advantage configuration lacks token.""" ++ def test_warn_schema_requires_commands(self): ++ """Warn when ubuntu-advantage configuration lacks commands.""" + validate_cloudconfig_schema( +- {'ubuntu_advantage': {'enable': ['esm']}}, schema) ++ {'ubuntu-advantage': {}}, schema) + self.assertEqual( +- "WARNING: Invalid config:\nubuntu_advantage:" +- " 'token' is a required property\n", self.logs.getvalue()) ++ "WARNING: Invalid config:\nubuntu-advantage: 'commands' is a" ++ " required property\n", ++ self.logs.getvalue()) + +- @mock.patch('%s.maybe_install_ua_tools' % MPATH) +- @mock.patch('%s.configure_ua' % MPATH) +- def test_warn_schema_services_is_not_list_or_dict(self, _cfg, _): +- """Warn when ubuntu_advantage:enable config is not a list.""" ++ @mock.patch('%s.run_commands' % MPATH) ++ def test_warn_schema_commands_is_not_list_or_dict(self, _): ++ """Warn when ubuntu-advantage:commands config is not a list or dict.""" + validate_cloudconfig_schema( +- {'ubuntu_advantage': {'enable': 'needslist'}}, schema) ++ {'ubuntu-advantage': {'commands': 'broken'}}, schema) + self.assertEqual( +- "WARNING: Invalid config:\nubuntu_advantage: 'token' is a" +- " required property\nubuntu_advantage.enable: 'needslist'" +- " is not of type 'array'\n", ++ "WARNING: Invalid config:\nubuntu-advantage.commands: 'broken' is" ++ " not of type 'object', 'array'\n", + self.logs.getvalue()) + ++ @mock.patch('%s.run_commands' % MPATH) ++ def test_warn_schema_when_commands_is_empty(self, _): ++ """Emit warnings when ubuntu-advantage:commands is empty.""" ++ validate_cloudconfig_schema( ++ {'ubuntu-advantage': {'commands': []}}, schema) ++ validate_cloudconfig_schema( ++ {'ubuntu-advantage': {'commands': {}}}, schema) ++ self.assertEqual( ++ "WARNING: Invalid config:\nubuntu-advantage.commands: [] is too" ++ " short\nWARNING: Invalid config:\nubuntu-advantage.commands: {}" ++ " does not have enough properties\n", ++ self.logs.getvalue()) ++ ++ @mock.patch('%s.run_commands' % MPATH) ++ def test_schema_when_commands_are_list_or_dict(self, _): ++ """No warnings when ubuntu-advantage:commands are a list or dict.""" ++ validate_cloudconfig_schema( ++ {'ubuntu-advantage': {'commands': ['valid']}}, schema) ++ validate_cloudconfig_schema( ++ {'ubuntu-advantage': {'commands': {'01': 'also valid'}}}, schema) ++ self.assertEqual('', self.logs.getvalue()) ++ ++ def test_duplicates_are_fine_array_array(self): ++ """Duplicated commands array/array entries are allowed.""" ++ self.assertSchemaValid( ++ {'commands': [["echo", "bye"], ["echo" "bye"]]}, ++ "command entries can be duplicate.") ++ ++ def test_duplicates_are_fine_array_string(self): ++ """Duplicated commands array/string entries are allowed.""" ++ self.assertSchemaValid( ++ {'commands': ["echo bye", "echo bye"]}, ++ "command entries can be duplicate.") ++ ++ def test_duplicates_are_fine_dict_array(self): ++ """Duplicated commands dict/array entries are allowed.""" ++ self.assertSchemaValid( ++ {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}}, ++ "command entries can be duplicate.") ++ ++ def test_duplicates_are_fine_dict_string(self): ++ """Duplicated commands dict/string entries are allowed.""" ++ self.assertSchemaValid( ++ {'commands': {'00': "echo bye", '01': "echo bye"}}, ++ "command entries can be duplicate.") ++ + + class TestHandle(CiTestCase): + +@@ -192,89 +205,41 @@ class TestHandle(CiTestCase): + super(TestHandle, self).setUp() + self.tmp = self.tmp_dir() + ++ @mock.patch('%s.run_commands' % MPATH) + @mock.patch('%s.validate_cloudconfig_schema' % MPATH) +- def test_handle_no_config(self, m_schema): ++ def test_handle_no_config(self, m_schema, m_run): + """When no ua-related configuration is provided, nothing happens.""" + cfg = {} + handle('ua-test', cfg=cfg, cloud=None, log=self.logger, args=None) + self.assertIn( +- "DEBUG: Skipping module named ua-test, no 'ubuntu_advantage'" +- ' configuration found', ++ "DEBUG: Skipping module named ua-test, no 'ubuntu-advantage' key" ++ " in config", + self.logs.getvalue()) + m_schema.assert_not_called() ++ m_run.assert_not_called() + +- @mock.patch('%s.configure_ua' % MPATH) + @mock.patch('%s.maybe_install_ua_tools' % MPATH) +- def test_handle_tries_to_install_ubuntu_advantage_tools( +- self, m_install, m_cfg): ++ def test_handle_tries_to_install_ubuntu_advantage_tools(self, m_install): + """If ubuntu_advantage is provided, try installing ua-tools package.""" +- cfg = {'ubuntu_advantage': {'token': 'valid'}} ++ cfg = {'ubuntu-advantage': {}} + mycloud = FakeCloud(None) + handle('nomatter', cfg=cfg, cloud=mycloud, log=self.logger, args=None) + m_install.assert_called_once_with(mycloud) + +- @mock.patch('%s.configure_ua' % MPATH) + @mock.patch('%s.maybe_install_ua_tools' % MPATH) +- def test_handle_passes_credentials_and_services_to_configure_ua( +- self, m_install, m_configure_ua): +- """All ubuntu_advantage config keys are passed to configure_ua.""" +- cfg = {'ubuntu_advantage': {'token': 'token', 'enable': ['esm']}} +- handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) +- m_configure_ua.assert_called_once_with( +- token='token', enable=['esm']) +- +- @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock()) +- @mock.patch('%s.configure_ua' % MPATH) +- def test_handle_warns_on_deprecated_ubuntu_advantage_key_w_config( +- self, m_configure_ua): +- """Warning when ubuntu-advantage key is present with new config""" +- cfg = {'ubuntu-advantage': {'token': 'token', 'enable': ['esm']}} +- handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) +- self.assertEqual( +- 'WARNING: Deprecated configuration key "ubuntu-advantage"' +- ' provided. Expected underscore delimited "ubuntu_advantage";' +- ' will attempt to continue.', +- self.logs.getvalue().splitlines()[0]) +- m_configure_ua.assert_called_once_with( +- token='token', enable=['esm']) +- +- def test_handle_error_on_deprecated_commands_key_dashed(self): +- """Error when commands is present in ubuntu-advantage key.""" +- cfg = {'ubuntu-advantage': {'commands': 'nogo'}} +- with self.assertRaises(RuntimeError) as context_manager: +- handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) +- self.assertEqual( +- 'Deprecated configuration "ubuntu-advantage: commands" provided.' +- ' Expected "token"', +- str(context_manager.exception)) +- +- def test_handle_error_on_deprecated_commands_key_underscored(self): +- """Error when commands is present in ubuntu_advantage key.""" +- cfg = {'ubuntu_advantage': {'commands': 'nogo'}} +- with self.assertRaises(RuntimeError) as context_manager: +- handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) +- self.assertEqual( +- 'Deprecated configuration "ubuntu-advantage: commands" provided.' +- ' Expected "token"', +- str(context_manager.exception)) ++ def test_handle_runs_commands_provided(self, m_install): ++ """When commands are specified as a list, run them.""" ++ outfile = self.tmp_path('output.log', dir=self.tmp) + +- @mock.patch('%s.maybe_install_ua_tools' % MPATH, mock.MagicMock()) +- @mock.patch('%s.configure_ua' % MPATH) +- def test_handle_prefers_new_style_config( +- self, m_configure_ua): +- """ubuntu_advantage should be preferred over ubuntu-advantage""" + cfg = { +- 'ubuntu-advantage': {'token': 'nope', 'enable': ['wrong']}, +- 'ubuntu_advantage': {'token': 'token', 'enable': ['esm']}, +- } +- handle('nomatter', cfg=cfg, cloud=None, log=self.logger, args=None) +- self.assertEqual( +- 'WARNING: Deprecated configuration key "ubuntu-advantage"' +- ' provided. Expected underscore delimited "ubuntu_advantage";' +- ' will attempt to continue.', +- self.logs.getvalue().splitlines()[0]) +- m_configure_ua.assert_called_once_with( +- token='token', enable=['esm']) ++ 'ubuntu-advantage': {'commands': ['echo "HI" >> %s' % outfile, ++ 'echo "MOM" >> %s' % outfile]}} ++ mock_path = '%s.sys.stderr' % MPATH ++ with self.allow_subp([CiTestCase.SUBP_SHELL_TRUE]): ++ with mock.patch(mock_path, new_callable=StringIO): ++ handle('nomatter', cfg=cfg, cloud=None, log=self.logger, ++ args=None) ++ self.assertEqual('HI\nMOM\n', util.load_file(outfile)) + + + class TestMaybeInstallUATools(CiTestCase): +@@ -288,7 +253,7 @@ class TestMaybeInstallUATools(CiTestCase + @mock.patch('%s.util.which' % MPATH) + def test_maybe_install_ua_tools_noop_when_ua_tools_present(self, m_which): + """Do nothing if ubuntu-advantage-tools already exists.""" +- m_which.return_value = '/usr/bin/ua' # already installed ++ m_which.return_value = '/usr/bin/ubuntu-advantage' # already installed + distro = mock.MagicMock() + distro.update_package_sources.side_effect = RuntimeError( + 'Some apt error') |