summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/analyze/tests/test_boot.py161
-rw-r--r--cloudinit/analyze/tests/test_dump.py208
-rw-r--r--cloudinit/cmd/devel/tests/__init__.py0
-rw-r--r--cloudinit/cmd/devel/tests/test_logs.py167
-rw-r--r--cloudinit/cmd/devel/tests/test_render.py144
-rw-r--r--cloudinit/cmd/tests/__init__.py0
-rw-r--r--cloudinit/cmd/tests/test_clean.py178
-rw-r--r--cloudinit/cmd/tests/test_cloud_id.py127
-rw-r--r--cloudinit/cmd/tests/test_main.py188
-rw-r--r--cloudinit/cmd/tests/test_query.py392
-rw-r--r--cloudinit/cmd/tests/test_status.py391
-rw-r--r--cloudinit/config/tests/test_apt_pipelining.py28
-rw-r--r--cloudinit/config/tests/test_disable_ec2_metadata.py48
-rw-r--r--cloudinit/config/tests/test_final_message.py46
-rw-r--r--cloudinit/config/tests/test_grub_dpkg.py176
-rw-r--r--cloudinit/config/tests/test_keys_to_console.py34
-rw-r--r--cloudinit/config/tests/test_mounts.py61
-rw-r--r--cloudinit/config/tests/test_resolv_conf.py92
-rw-r--r--cloudinit/config/tests/test_set_passwords.py168
-rw-r--r--cloudinit/config/tests/test_snap.py564
-rw-r--r--cloudinit/config/tests/test_ssh.py405
-rw-r--r--cloudinit/config/tests/test_ubuntu_advantage.py333
-rw-r--r--cloudinit/config/tests/test_ubuntu_drivers.py244
-rw-r--r--cloudinit/config/tests/test_users_groups.py172
-rw-r--r--cloudinit/distros/tests/__init__.py0
-rw-r--r--cloudinit/distros/tests/test_init.py161
-rw-r--r--cloudinit/distros/tests/test_networking.py223
-rw-r--r--cloudinit/net/tests/__init__.py0
-rw-r--r--cloudinit/net/tests/test_dhcp.py647
-rw-r--r--cloudinit/net/tests/test_init.py1402
-rw-r--r--cloudinit/net/tests/test_network_state.py164
-rw-r--r--cloudinit/net/tests/test_networkd.py64
-rw-r--r--cloudinit/sources/helpers/tests/test_netlink.py480
-rw-r--r--cloudinit/sources/helpers/tests/test_openstack.py49
-rw-r--r--cloudinit/sources/tests/__init__.py0
-rw-r--r--cloudinit/sources/tests/test_init.py771
-rw-r--r--cloudinit/sources/tests/test_lxd.py376
-rw-r--r--cloudinit/sources/tests/test_oracle.py797
-rw-r--r--cloudinit/tests/__init__.py0
-rw-r--r--cloudinit/tests/helpers.py507
-rw-r--r--cloudinit/tests/test_conftest.py65
-rw-r--r--cloudinit/tests/test_dhclient_hook.py105
-rw-r--r--cloudinit/tests/test_dmi.py154
-rw-r--r--cloudinit/tests/test_event.py26
-rw-r--r--cloudinit/tests/test_features.py60
-rw-r--r--cloudinit/tests/test_gpg.py55
-rw-r--r--cloudinit/tests/test_netinfo.py181
-rw-r--r--cloudinit/tests/test_persistence.py127
-rw-r--r--cloudinit/tests/test_simpletable.py106
-rw-r--r--cloudinit/tests/test_stages.py478
-rw-r--r--cloudinit/tests/test_subp.py286
-rw-r--r--cloudinit/tests/test_temp_utils.py117
-rw-r--r--cloudinit/tests/test_upgrade.py52
-rw-r--r--cloudinit/tests/test_url_helper.py178
-rw-r--r--cloudinit/tests/test_util.py1187
-rw-r--r--cloudinit/tests/test_version.py31
56 files changed, 0 insertions, 13176 deletions
diff --git a/cloudinit/analyze/tests/test_boot.py b/cloudinit/analyze/tests/test_boot.py
deleted file mode 100644
index 6b3afb5e..00000000
--- a/cloudinit/analyze/tests/test_boot.py
+++ /dev/null
@@ -1,161 +0,0 @@
-import os
-from cloudinit.analyze.__main__ import (analyze_boot, get_parser)
-from cloudinit.tests.helpers import CiTestCase, mock
-from cloudinit.analyze.show import dist_check_timestamp, SystemctlReader, \
- FAIL_CODE, CONTAINER_CODE
-
-err_code = (FAIL_CODE, -1, -1, -1)
-
-
-class TestDistroChecker(CiTestCase):
-
- def test_blank_distro(self):
- self.assertEqual(err_code, dist_check_timestamp())
-
- @mock.patch('cloudinit.util.is_FreeBSD', return_value=True)
- def test_freebsd_gentoo_cant_find(self, m_is_FreeBSD):
- self.assertEqual(err_code, dist_check_timestamp())
-
- @mock.patch('cloudinit.subp.subp', return_value=(0, 1))
- def test_subp_fails(self, m_subp):
- self.assertEqual(err_code, dist_check_timestamp())
-
-
-class TestSystemCtlReader(CiTestCase):
-
- def test_systemctl_invalid_property(self):
- reader = SystemctlReader('dummyProperty')
- with self.assertRaises(RuntimeError):
- reader.parse_epoch_as_float()
-
- def test_systemctl_invalid_parameter(self):
- reader = SystemctlReader('dummyProperty', 'dummyParameter')
- with self.assertRaises(RuntimeError):
- reader.parse_epoch_as_float()
-
- @mock.patch('cloudinit.subp.subp', return_value=('U=1000000', None))
- def test_systemctl_works_correctly_threshold(self, m_subp):
- reader = SystemctlReader('dummyProperty', 'dummyParameter')
- self.assertEqual(1.0, reader.parse_epoch_as_float())
- thresh = 1.0 - reader.parse_epoch_as_float()
- self.assertTrue(thresh < 1e-6)
- self.assertTrue(thresh > (-1 * 1e-6))
-
- @mock.patch('cloudinit.subp.subp', return_value=('U=0', None))
- def test_systemctl_succeed_zero(self, m_subp):
- reader = SystemctlReader('dummyProperty', 'dummyParameter')
- self.assertEqual(0.0, reader.parse_epoch_as_float())
-
- @mock.patch('cloudinit.subp.subp', return_value=('U=1', None))
- def test_systemctl_succeed_distinct(self, m_subp):
- reader = SystemctlReader('dummyProperty', 'dummyParameter')
- val1 = reader.parse_epoch_as_float()
- m_subp.return_value = ('U=2', None)
- reader2 = SystemctlReader('dummyProperty', 'dummyParameter')
- val2 = reader2.parse_epoch_as_float()
- self.assertNotEqual(val1, val2)
-
- @mock.patch('cloudinit.subp.subp', return_value=('100', None))
- def test_systemctl_epoch_not_splittable(self, m_subp):
- reader = SystemctlReader('dummyProperty', 'dummyParameter')
- with self.assertRaises(IndexError):
- reader.parse_epoch_as_float()
-
- @mock.patch('cloudinit.subp.subp', return_value=('U=foobar', None))
- def test_systemctl_cannot_convert_epoch_to_float(self, m_subp):
- reader = SystemctlReader('dummyProperty', 'dummyParameter')
- with self.assertRaises(ValueError):
- reader.parse_epoch_as_float()
-
-
-class TestAnalyzeBoot(CiTestCase):
-
- def set_up_dummy_file_ci(self, path, log_path):
- infh = open(path, 'w+')
- infh.write('2019-07-08 17:40:49,601 - util.py[DEBUG]: Cloud-init v. '
- '19.1-1-gbaa47854-0ubuntu1~18.04.1 running \'init-local\' '
- 'at Mon, 08 Jul 2019 17:40:49 +0000. Up 18.84 seconds.')
- infh.close()
- outfh = open(log_path, 'w+')
- outfh.close()
-
- def set_up_dummy_file(self, path, log_path):
- infh = open(path, 'w+')
- infh.write('dummy data')
- infh.close()
- outfh = open(log_path, 'w+')
- outfh.close()
-
- def remove_dummy_file(self, path, log_path):
- if os.path.isfile(path):
- os.remove(path)
- if os.path.isfile(log_path):
- os.remove(log_path)
-
- @mock.patch('cloudinit.analyze.show.dist_check_timestamp',
- return_value=err_code)
- def test_boot_invalid_distro(self, m_dist_check_timestamp):
-
- path = os.path.dirname(os.path.abspath(__file__))
- log_path = path + '/boot-test.log'
- path += '/dummy.log'
- self.set_up_dummy_file(path, log_path)
-
- parser = get_parser()
- args = parser.parse_args(args=['boot', '-i', path, '-o',
- log_path])
- name_default = ''
- analyze_boot(name_default, args)
- # now args have been tested, go into outfile and make sure error
- # message is in the outfile
- outfh = open(args.outfile, 'r')
- data = outfh.read()
- err_string = 'Your Linux distro or container does not support this ' \
- 'functionality.\nYou must be running a Kernel ' \
- 'Telemetry supported distro.\nPlease check ' \
- 'https://cloudinit.readthedocs.io/en/latest/topics' \
- '/analyze.html for more information on supported ' \
- 'distros.\n'
-
- self.remove_dummy_file(path, log_path)
- self.assertEqual(err_string, data)
-
- @mock.patch("cloudinit.util.is_container", return_value=True)
- @mock.patch('cloudinit.subp.subp', return_value=('U=1000000', None))
- def test_container_no_ci_log_line(self, m_is_container, m_subp):
- path = os.path.dirname(os.path.abspath(__file__))
- log_path = path + '/boot-test.log'
- path += '/dummy.log'
- self.set_up_dummy_file(path, log_path)
-
- parser = get_parser()
- args = parser.parse_args(args=['boot', '-i', path, '-o',
- log_path])
- name_default = ''
-
- finish_code = analyze_boot(name_default, args)
-
- self.remove_dummy_file(path, log_path)
- self.assertEqual(FAIL_CODE, finish_code)
-
- @mock.patch("cloudinit.util.is_container", return_value=True)
- @mock.patch('cloudinit.subp.subp', return_value=('U=1000000', None))
- @mock.patch('cloudinit.analyze.__main__._get_events', return_value=[{
- 'name': 'init-local', 'description': 'starting search', 'timestamp':
- 100000}])
- @mock.patch('cloudinit.analyze.show.dist_check_timestamp',
- return_value=(CONTAINER_CODE, 1, 1, 1))
- def test_container_ci_log_line(self, m_is_container, m_subp, m_get, m_g):
- path = os.path.dirname(os.path.abspath(__file__))
- log_path = path + '/boot-test.log'
- path += '/dummy.log'
- self.set_up_dummy_file_ci(path, log_path)
-
- parser = get_parser()
- args = parser.parse_args(args=['boot', '-i', path, '-o',
- log_path])
- name_default = ''
- finish_code = analyze_boot(name_default, args)
-
- self.remove_dummy_file(path, log_path)
- self.assertEqual(CONTAINER_CODE, finish_code)
diff --git a/cloudinit/analyze/tests/test_dump.py b/cloudinit/analyze/tests/test_dump.py
deleted file mode 100644
index dac1efb6..00000000
--- a/cloudinit/analyze/tests/test_dump.py
+++ /dev/null
@@ -1,208 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from datetime import datetime
-from textwrap import dedent
-
-from cloudinit.analyze.dump import (
- dump_events, parse_ci_logline, parse_timestamp)
-from cloudinit.util import write_file
-from cloudinit.subp import which
-from cloudinit.tests.helpers import CiTestCase, mock, skipIf
-
-
-class TestParseTimestamp(CiTestCase):
-
- def test_parse_timestamp_handles_cloud_init_default_format(self):
- """Logs with cloud-init detailed formats will be properly parsed."""
- trusty_fmt = '%Y-%m-%d %H:%M:%S,%f'
- trusty_stamp = '2016-09-12 14:39:20,839'
- dt = datetime.strptime(trusty_stamp, trusty_fmt)
- self.assertEqual(
- float(dt.strftime('%s.%f')), parse_timestamp(trusty_stamp))
-
- def test_parse_timestamp_handles_syslog_adding_year(self):
- """Syslog timestamps lack a year. Add year and properly parse."""
- syslog_fmt = '%b %d %H:%M:%S %Y'
- syslog_stamp = 'Aug 08 15:12:51'
-
- # convert stamp ourselves by adding the missing year value
- year = datetime.now().year
- dt = datetime.strptime(syslog_stamp + " " + str(year), syslog_fmt)
- self.assertEqual(
- float(dt.strftime('%s.%f')),
- parse_timestamp(syslog_stamp))
-
- def test_parse_timestamp_handles_journalctl_format_adding_year(self):
- """Journalctl precise timestamps lack a year. Add year and parse."""
- journal_fmt = '%b %d %H:%M:%S.%f %Y'
- journal_stamp = 'Aug 08 17:15:50.606811'
-
- # convert stamp ourselves by adding the missing year value
- year = datetime.now().year
- dt = datetime.strptime(journal_stamp + " " + str(year), journal_fmt)
- self.assertEqual(
- float(dt.strftime('%s.%f')), parse_timestamp(journal_stamp))
-
- @skipIf(not which("date"), "'date' command not available.")
- def test_parse_unexpected_timestamp_format_with_date_command(self):
- """Dump sends unexpected timestamp formats to date for processing."""
- new_fmt = '%H:%M %m/%d %Y'
- new_stamp = '17:15 08/08'
- # convert stamp ourselves by adding the missing year value
- year = datetime.now().year
- dt = datetime.strptime(new_stamp + " " + str(year), new_fmt)
-
- # use date(1)
- with self.allow_subp(["date"]):
- self.assertEqual(
- float(dt.strftime('%s.%f')), parse_timestamp(new_stamp))
-
-
-class TestParseCILogLine(CiTestCase):
-
- def test_parse_logline_returns_none_without_separators(self):
- """When no separators are found, parse_ci_logline returns None."""
- expected_parse_ignores = [
- '', '-', 'adsf-asdf', '2017-05-22 18:02:01,088', 'CLOUDINIT']
- for parse_ignores in expected_parse_ignores:
- self.assertIsNone(parse_ci_logline(parse_ignores))
-
- def test_parse_logline_returns_event_for_cloud_init_logs(self):
- """parse_ci_logline returns an event parse from cloud-init format."""
- line = (
- "2017-08-08 20:05:07,147 - util.py[DEBUG]: Cloud-init v. 0.7.9"
- " running 'init-local' at Tue, 08 Aug 2017 20:05:07 +0000. Up"
- " 6.26 seconds.")
- dt = datetime.strptime(
- '2017-08-08 20:05:07,147', '%Y-%m-%d %H:%M:%S,%f')
- timestamp = float(dt.strftime('%s.%f'))
- expected = {
- 'description': 'starting search for local datasources',
- 'event_type': 'start',
- 'name': 'init-local',
- 'origin': 'cloudinit',
- 'timestamp': timestamp}
- self.assertEqual(expected, parse_ci_logline(line))
-
- def test_parse_logline_returns_event_for_journalctl_logs(self):
- """parse_ci_logline returns an event parse from journalctl format."""
- line = ("Nov 03 06:51:06.074410 x2 cloud-init[106]: [CLOUDINIT]"
- " util.py[DEBUG]: Cloud-init v. 0.7.8 running 'init-local' at"
- " Thu, 03 Nov 2016 06:51:06 +0000. Up 1.0 seconds.")
- year = datetime.now().year
- dt = datetime.strptime(
- 'Nov 03 06:51:06.074410 %d' % year, '%b %d %H:%M:%S.%f %Y')
- timestamp = float(dt.strftime('%s.%f'))
- expected = {
- 'description': 'starting search for local datasources',
- 'event_type': 'start',
- 'name': 'init-local',
- 'origin': 'cloudinit',
- 'timestamp': timestamp}
- self.assertEqual(expected, parse_ci_logline(line))
-
- @mock.patch("cloudinit.analyze.dump.parse_timestamp_from_date")
- def test_parse_logline_returns_event_for_finish_events(self,
- m_parse_from_date):
- """parse_ci_logline returns a finish event for a parsed log line."""
- line = ('2016-08-30 21:53:25.972325+00:00 y1 [CLOUDINIT]'
- ' handlers.py[DEBUG]: finish: modules-final: SUCCESS: running'
- ' modules for final')
- expected = {
- 'description': 'running modules for final',
- 'event_type': 'finish',
- 'name': 'modules-final',
- 'origin': 'cloudinit',
- 'result': 'SUCCESS',
- 'timestamp': 1472594005.972}
- m_parse_from_date.return_value = "1472594005.972"
- self.assertEqual(expected, parse_ci_logline(line))
- m_parse_from_date.assert_has_calls(
- [mock.call("2016-08-30 21:53:25.972325+00:00")])
-
- def test_parse_logline_returns_event_for_amazon_linux_2_line(self):
- line = (
- "Apr 30 19:39:11 cloud-init[2673]: handlers.py[DEBUG]: start:"
- " init-local/check-cache: attempting to read from cache [check]")
- # Generate the expected value using `datetime`, so that TZ
- # determination is consistent with the code under test.
- timestamp_dt = datetime.strptime(
- "Apr 30 19:39:11", "%b %d %H:%M:%S"
- ).replace(year=datetime.now().year)
- expected = {
- 'description': 'attempting to read from cache [check]',
- 'event_type': 'start',
- 'name': 'init-local/check-cache',
- 'origin': 'cloudinit',
- 'timestamp': timestamp_dt.timestamp()}
- self.assertEqual(expected, parse_ci_logline(line))
-
-
-SAMPLE_LOGS = dedent("""\
-Nov 03 06:51:06.074410 x2 cloud-init[106]: [CLOUDINIT] util.py[DEBUG]:\
- Cloud-init v. 0.7.8 running 'init-local' at Thu, 03 Nov 2016\
- 06:51:06 +0000. Up 1.0 seconds.
-2016-08-30 21:53:25.972325+00:00 y1 [CLOUDINIT] handlers.py[DEBUG]: finish:\
- modules-final: SUCCESS: running modules for final
-""")
-
-
-class TestDumpEvents(CiTestCase):
- maxDiff = None
-
- @mock.patch("cloudinit.analyze.dump.parse_timestamp_from_date")
- def test_dump_events_with_rawdata(self, m_parse_from_date):
- """Rawdata is split and parsed into a tuple of events and data"""
- m_parse_from_date.return_value = "1472594005.972"
- events, data = dump_events(rawdata=SAMPLE_LOGS)
- expected_data = SAMPLE_LOGS.splitlines()
- self.assertEqual(
- [mock.call("2016-08-30 21:53:25.972325+00:00")],
- m_parse_from_date.call_args_list)
- self.assertEqual(expected_data, data)
- year = datetime.now().year
- dt1 = datetime.strptime(
- 'Nov 03 06:51:06.074410 %d' % year, '%b %d %H:%M:%S.%f %Y')
- timestamp1 = float(dt1.strftime('%s.%f'))
- expected_events = [{
- 'description': 'starting search for local datasources',
- 'event_type': 'start',
- 'name': 'init-local',
- 'origin': 'cloudinit',
- 'timestamp': timestamp1}, {
- 'description': 'running modules for final',
- 'event_type': 'finish',
- 'name': 'modules-final',
- 'origin': 'cloudinit',
- 'result': 'SUCCESS',
- 'timestamp': 1472594005.972}]
- self.assertEqual(expected_events, events)
-
- @mock.patch("cloudinit.analyze.dump.parse_timestamp_from_date")
- def test_dump_events_with_cisource(self, m_parse_from_date):
- """Cisource file is read and parsed into a tuple of events and data."""
- tmpfile = self.tmp_path('logfile')
- write_file(tmpfile, SAMPLE_LOGS)
- m_parse_from_date.return_value = 1472594005.972
-
- events, data = dump_events(cisource=open(tmpfile))
- year = datetime.now().year
- dt1 = datetime.strptime(
- 'Nov 03 06:51:06.074410 %d' % year, '%b %d %H:%M:%S.%f %Y')
- timestamp1 = float(dt1.strftime('%s.%f'))
- expected_events = [{
- 'description': 'starting search for local datasources',
- 'event_type': 'start',
- 'name': 'init-local',
- 'origin': 'cloudinit',
- 'timestamp': timestamp1}, {
- 'description': 'running modules for final',
- 'event_type': 'finish',
- 'name': 'modules-final',
- 'origin': 'cloudinit',
- 'result': 'SUCCESS',
- 'timestamp': 1472594005.972}]
- self.assertEqual(expected_events, events)
- self.assertEqual(SAMPLE_LOGS.splitlines(), [d.strip() for d in data])
- m_parse_from_date.assert_has_calls(
- [mock.call("2016-08-30 21:53:25.972325+00:00")])
diff --git a/cloudinit/cmd/devel/tests/__init__.py b/cloudinit/cmd/devel/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/cloudinit/cmd/devel/tests/__init__.py
+++ /dev/null
diff --git a/cloudinit/cmd/devel/tests/test_logs.py b/cloudinit/cmd/devel/tests/test_logs.py
deleted file mode 100644
index ddfd58e1..00000000
--- a/cloudinit/cmd/devel/tests/test_logs.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from datetime import datetime
-import os
-from io import StringIO
-
-from cloudinit.cmd.devel import logs
-from cloudinit.sources import INSTANCE_JSON_SENSITIVE_FILE
-from cloudinit.tests.helpers import (
- FilesystemMockingTestCase, mock, wrap_and_call)
-from cloudinit.subp import subp
-from cloudinit.util import ensure_dir, load_file, write_file
-
-
-@mock.patch('cloudinit.cmd.devel.logs.os.getuid')
-class TestCollectLogs(FilesystemMockingTestCase):
-
- def setUp(self):
- super(TestCollectLogs, self).setUp()
- self.new_root = self.tmp_dir()
- self.run_dir = self.tmp_path('run', self.new_root)
-
- def test_collect_logs_with_userdata_requires_root_user(self, m_getuid):
- """collect-logs errors when non-root user collects userdata ."""
- m_getuid.return_value = 100 # non-root
- output_tarfile = self.tmp_path('logs.tgz')
- with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
- self.assertEqual(
- 1, logs.collect_logs(output_tarfile, include_userdata=True))
- self.assertEqual(
- 'To include userdata, root user is required.'
- ' Try sudo cloud-init collect-logs\n',
- m_stderr.getvalue())
-
- def test_collect_logs_creates_tarfile(self, m_getuid):
- """collect-logs creates a tarfile with all related cloud-init info."""
- m_getuid.return_value = 100
- log1 = self.tmp_path('cloud-init.log', self.new_root)
- write_file(log1, 'cloud-init-log')
- log2 = self.tmp_path('cloud-init-output.log', self.new_root)
- write_file(log2, 'cloud-init-output-log')
- ensure_dir(self.run_dir)
- write_file(self.tmp_path('results.json', self.run_dir), 'results')
- write_file(self.tmp_path(INSTANCE_JSON_SENSITIVE_FILE, self.run_dir),
- 'sensitive')
- output_tarfile = self.tmp_path('logs.tgz')
-
- date = datetime.utcnow().date().strftime('%Y-%m-%d')
- date_logdir = 'cloud-init-logs-{0}'.format(date)
-
- version_out = '/usr/bin/cloud-init 18.2fake\n'
- expected_subp = {
- ('dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'):
- '0.7fake\n',
- ('cloud-init', '--version'): version_out,
- ('dmesg',): 'dmesg-out\n',
- ('journalctl', '--boot=0', '-o', 'short-precise'): 'journal-out\n',
- ('tar', 'czvf', output_tarfile, date_logdir): ''
- }
-
- def fake_subp(cmd):
- cmd_tuple = tuple(cmd)
- if cmd_tuple not in expected_subp:
- raise AssertionError(
- 'Unexpected command provided to subp: {0}'.format(cmd))
- if cmd == ['tar', 'czvf', output_tarfile, date_logdir]:
- subp(cmd) # Pass through tar cmd so we can check output
- return expected_subp[cmd_tuple], ''
-
- fake_stderr = mock.MagicMock()
-
- wrap_and_call(
- 'cloudinit.cmd.devel.logs',
- {'subp': {'side_effect': fake_subp},
- 'sys.stderr': {'new': fake_stderr},
- 'CLOUDINIT_LOGS': {'new': [log1, log2]},
- 'CLOUDINIT_RUN_DIR': {'new': self.run_dir}},
- logs.collect_logs, output_tarfile, include_userdata=False)
- # unpack the tarfile and check file contents
- subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root])
- out_logdir = self.tmp_path(date_logdir, self.new_root)
- self.assertFalse(
- os.path.exists(
- os.path.join(out_logdir, 'run', 'cloud-init',
- INSTANCE_JSON_SENSITIVE_FILE)),
- 'Unexpected file found: %s' % INSTANCE_JSON_SENSITIVE_FILE)
- self.assertEqual(
- '0.7fake\n',
- load_file(os.path.join(out_logdir, 'dpkg-version')))
- self.assertEqual(version_out,
- load_file(os.path.join(out_logdir, 'version')))
- self.assertEqual(
- 'cloud-init-log',
- load_file(os.path.join(out_logdir, 'cloud-init.log')))
- self.assertEqual(
- 'cloud-init-output-log',
- load_file(os.path.join(out_logdir, 'cloud-init-output.log')))
- self.assertEqual(
- 'dmesg-out\n',
- load_file(os.path.join(out_logdir, 'dmesg.txt')))
- self.assertEqual(
- 'journal-out\n',
- load_file(os.path.join(out_logdir, 'journal.txt')))
- self.assertEqual(
- 'results',
- load_file(
- os.path.join(out_logdir, 'run', 'cloud-init', 'results.json')))
- fake_stderr.write.assert_any_call('Wrote %s\n' % output_tarfile)
-
- def test_collect_logs_includes_optional_userdata(self, m_getuid):
- """collect-logs include userdata when --include-userdata is set."""
- m_getuid.return_value = 0
- log1 = self.tmp_path('cloud-init.log', self.new_root)
- write_file(log1, 'cloud-init-log')
- log2 = self.tmp_path('cloud-init-output.log', self.new_root)
- write_file(log2, 'cloud-init-output-log')
- userdata = self.tmp_path('user-data.txt', self.new_root)
- write_file(userdata, 'user-data')
- ensure_dir(self.run_dir)
- write_file(self.tmp_path('results.json', self.run_dir), 'results')
- write_file(self.tmp_path(INSTANCE_JSON_SENSITIVE_FILE, self.run_dir),
- 'sensitive')
- output_tarfile = self.tmp_path('logs.tgz')
-
- date = datetime.utcnow().date().strftime('%Y-%m-%d')
- date_logdir = 'cloud-init-logs-{0}'.format(date)
-
- version_out = '/usr/bin/cloud-init 18.2fake\n'
- expected_subp = {
- ('dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'):
- '0.7fake',
- ('cloud-init', '--version'): version_out,
- ('dmesg',): 'dmesg-out\n',
- ('journalctl', '--boot=0', '-o', 'short-precise'): 'journal-out\n',
- ('tar', 'czvf', output_tarfile, date_logdir): ''
- }
-
- def fake_subp(cmd):
- cmd_tuple = tuple(cmd)
- if cmd_tuple not in expected_subp:
- raise AssertionError(
- 'Unexpected command provided to subp: {0}'.format(cmd))
- if cmd == ['tar', 'czvf', output_tarfile, date_logdir]:
- subp(cmd) # Pass through tar cmd so we can check output
- return expected_subp[cmd_tuple], ''
-
- fake_stderr = mock.MagicMock()
-
- wrap_and_call(
- 'cloudinit.cmd.devel.logs',
- {'subp': {'side_effect': fake_subp},
- 'sys.stderr': {'new': fake_stderr},
- 'CLOUDINIT_LOGS': {'new': [log1, log2]},
- 'CLOUDINIT_RUN_DIR': {'new': self.run_dir},
- 'USER_DATA_FILE': {'new': userdata}},
- logs.collect_logs, output_tarfile, include_userdata=True)
- # unpack the tarfile and check file contents
- subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root])
- out_logdir = self.tmp_path(date_logdir, self.new_root)
- self.assertEqual(
- 'user-data',
- load_file(os.path.join(out_logdir, 'user-data.txt')))
- self.assertEqual(
- 'sensitive',
- load_file(os.path.join(out_logdir, 'run', 'cloud-init',
- INSTANCE_JSON_SENSITIVE_FILE)))
- fake_stderr.write.assert_any_call('Wrote %s\n' % output_tarfile)
diff --git a/cloudinit/cmd/devel/tests/test_render.py b/cloudinit/cmd/devel/tests/test_render.py
deleted file mode 100644
index a7fcf2ce..00000000
--- a/cloudinit/cmd/devel/tests/test_render.py
+++ /dev/null
@@ -1,144 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import os
-from io import StringIO
-
-from collections import namedtuple
-from cloudinit.cmd.devel import render
-from cloudinit.helpers import Paths
-from cloudinit.sources import INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE
-from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJinja
-from cloudinit.util import ensure_dir, write_file
-
-
-class TestRender(CiTestCase):
-
- with_logs = True
-
- args = namedtuple('renderargs', 'user_data instance_data debug')
-
- def setUp(self):
- super(TestRender, self).setUp()
- self.tmp = self.tmp_dir()
-
- def test_handle_args_error_on_missing_user_data(self):
- """When user_data file path does not exist, log an error."""
- absent_file = self.tmp_path('user-data', dir=self.tmp)
- instance_data = self.tmp_path('instance-data', dir=self.tmp)
- write_file(instance_data, '{}')
- args = self.args(
- user_data=absent_file, instance_data=instance_data, debug=False)
- with mock.patch('sys.stderr', new_callable=StringIO):
- self.assertEqual(1, render.handle_args('anyname', args))
- self.assertIn(
- 'Missing user-data file: %s' % absent_file,
- self.logs.getvalue())
-
- def test_handle_args_error_on_missing_instance_data(self):
- """When instance_data file path does not exist, log an error."""
- user_data = self.tmp_path('user-data', dir=self.tmp)
- absent_file = self.tmp_path('instance-data', dir=self.tmp)
- args = self.args(
- user_data=user_data, instance_data=absent_file, debug=False)
- with mock.patch('sys.stderr', new_callable=StringIO):
- self.assertEqual(1, render.handle_args('anyname', args))
- self.assertIn(
- 'Missing instance-data.json file: %s' % absent_file,
- self.logs.getvalue())
-
- def test_handle_args_defaults_instance_data(self):
- """When no instance_data argument, default to configured run_dir."""
- user_data = self.tmp_path('user-data', dir=self.tmp)
- run_dir = self.tmp_path('run_dir', dir=self.tmp)
- ensure_dir(run_dir)
- paths = Paths({'run_dir': run_dir})
- self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
- self.m_paths.return_value = paths
- args = self.args(
- user_data=user_data, instance_data=None, debug=False)
- with mock.patch('sys.stderr', new_callable=StringIO):
- self.assertEqual(1, render.handle_args('anyname', args))
- json_file = os.path.join(run_dir, INSTANCE_JSON_FILE)
- self.assertIn(
- 'Missing instance-data.json file: %s' % json_file,
- self.logs.getvalue())
-
- def test_handle_args_root_fallback_from_sensitive_instance_data(self):
- """When root user defaults to sensitive.json."""
- user_data = self.tmp_path('user-data', dir=self.tmp)
- run_dir = self.tmp_path('run_dir', dir=self.tmp)
- ensure_dir(run_dir)
- paths = Paths({'run_dir': run_dir})
- self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
- self.m_paths.return_value = paths
- args = self.args(
- user_data=user_data, instance_data=None, debug=False)
- with mock.patch('sys.stderr', new_callable=StringIO):
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 0
- self.assertEqual(1, render.handle_args('anyname', args))
- json_file = os.path.join(run_dir, INSTANCE_JSON_FILE)
- json_sensitive = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
- self.assertIn(
- 'WARNING: Missing root-readable %s. Using redacted %s' % (
- json_sensitive, json_file), self.logs.getvalue())
- self.assertIn(
- 'ERROR: Missing instance-data.json file: %s' % json_file,
- self.logs.getvalue())
-
- def test_handle_args_root_uses_sensitive_instance_data(self):
- """When root user, and no instance-data arg, use sensitive.json."""
- user_data = self.tmp_path('user-data', dir=self.tmp)
- write_file(user_data, '##template: jinja\nrendering: {{ my_var }}')
- run_dir = self.tmp_path('run_dir', dir=self.tmp)
- ensure_dir(run_dir)
- json_sensitive = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
- write_file(json_sensitive, '{"my-var": "jinja worked"}')
- paths = Paths({'run_dir': run_dir})
- self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
- self.m_paths.return_value = paths
- args = self.args(
- user_data=user_data, instance_data=None, debug=False)
- with mock.patch('sys.stderr', new_callable=StringIO):
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 0
- self.assertEqual(0, render.handle_args('anyname', args))
- self.assertIn('rendering: jinja worked', m_stdout.getvalue())
-
- @skipUnlessJinja()
- def test_handle_args_renders_instance_data_vars_in_template(self):
- """If user_data file is a jinja template render instance-data vars."""
- user_data = self.tmp_path('user-data', dir=self.tmp)
- write_file(user_data, '##template: jinja\nrendering: {{ my_var }}')
- instance_data = self.tmp_path('instance-data', dir=self.tmp)
- write_file(instance_data, '{"my-var": "jinja worked"}')
- args = self.args(
- user_data=user_data, instance_data=instance_data, debug=True)
- with mock.patch('sys.stderr', new_callable=StringIO) as m_console_err:
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- self.assertEqual(0, render.handle_args('anyname', args))
- self.assertIn(
- 'DEBUG: Converted jinja variables\n{', self.logs.getvalue())
- self.assertIn(
- 'DEBUG: Converted jinja variables\n{', m_console_err.getvalue())
- self.assertEqual('rendering: jinja worked', m_stdout.getvalue())
-
- @skipUnlessJinja()
- def test_handle_args_warns_and_gives_up_on_invalid_jinja_operation(self):
- """If user_data file has invalid jinja operations log warnings."""
- user_data = self.tmp_path('user-data', dir=self.tmp)
- write_file(user_data, '##template: jinja\nrendering: {{ my-var }}')
- instance_data = self.tmp_path('instance-data', dir=self.tmp)
- write_file(instance_data, '{"my-var": "jinja worked"}')
- args = self.args(
- user_data=user_data, instance_data=instance_data, debug=True)
- with mock.patch('sys.stderr', new_callable=StringIO):
- self.assertEqual(1, render.handle_args('anyname', args))
- self.assertIn(
- 'WARNING: Ignoring jinja template for %s: Undefined jinja'
- ' variable: "my-var". Jinja tried subtraction. Perhaps you meant'
- ' "my_var"?' % user_data,
- self.logs.getvalue())
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/tests/__init__.py b/cloudinit/cmd/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/cloudinit/cmd/tests/__init__.py
+++ /dev/null
diff --git a/cloudinit/cmd/tests/test_clean.py b/cloudinit/cmd/tests/test_clean.py
deleted file mode 100644
index a848a810..00000000
--- a/cloudinit/cmd/tests/test_clean.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from cloudinit.cmd import clean
-from cloudinit.util import ensure_dir, sym_link, write_file
-from cloudinit.tests.helpers import CiTestCase, wrap_and_call, mock
-from collections import namedtuple
-import os
-from io import StringIO
-
-mypaths = namedtuple('MyPaths', 'cloud_dir')
-
-
-class TestClean(CiTestCase):
-
- def setUp(self):
- super(TestClean, self).setUp()
- self.new_root = self.tmp_dir()
- self.artifact_dir = self.tmp_path('artifacts', self.new_root)
- self.log1 = self.tmp_path('cloud-init.log', self.new_root)
- self.log2 = self.tmp_path('cloud-init-output.log', self.new_root)
-
- class FakeInit(object):
- cfg = {'def_log_file': self.log1,
- 'output': {'all': '|tee -a {0}'.format(self.log2)}}
- # Ensure cloud_dir has a trailing slash, to match real behaviour
- paths = mypaths(cloud_dir='{}/'.format(self.artifact_dir))
-
- def __init__(self, ds_deps):
- pass
-
- def read_cfg(self):
- pass
-
- self.init_class = FakeInit
-
- def test_remove_artifacts_removes_logs(self):
- """remove_artifacts removes logs when remove_logs is True."""
- write_file(self.log1, 'cloud-init-log')
- write_file(self.log2, 'cloud-init-output-log')
-
- self.assertFalse(
- os.path.exists(self.artifact_dir), 'Unexpected artifacts dir')
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'Init': {'side_effect': self.init_class}},
- clean.remove_artifacts, remove_logs=True)
- self.assertFalse(os.path.exists(self.log1), 'Unexpected file')
- self.assertFalse(os.path.exists(self.log2), 'Unexpected file')
- self.assertEqual(0, retcode)
-
- def test_remove_artifacts_preserves_logs(self):
- """remove_artifacts leaves logs when remove_logs is False."""
- write_file(self.log1, 'cloud-init-log')
- write_file(self.log2, 'cloud-init-output-log')
-
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'Init': {'side_effect': self.init_class}},
- clean.remove_artifacts, remove_logs=False)
- self.assertTrue(os.path.exists(self.log1), 'Missing expected file')
- self.assertTrue(os.path.exists(self.log2), 'Missing expected file')
- self.assertEqual(0, retcode)
-
- def test_remove_artifacts_removes_unlinks_symlinks(self):
- """remove_artifacts cleans artifacts dir unlinking any symlinks."""
- dir1 = os.path.join(self.artifact_dir, 'dir1')
- ensure_dir(dir1)
- symlink = os.path.join(self.artifact_dir, 'mylink')
- sym_link(dir1, symlink)
-
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'Init': {'side_effect': self.init_class}},
- clean.remove_artifacts, remove_logs=False)
- self.assertEqual(0, retcode)
- for path in (dir1, symlink):
- self.assertFalse(
- os.path.exists(path),
- 'Unexpected {0} dir'.format(path))
-
- def test_remove_artifacts_removes_artifacts_skipping_seed(self):
- """remove_artifacts cleans artifacts dir with exception of seed dir."""
- dirs = [
- self.artifact_dir,
- os.path.join(self.artifact_dir, 'seed'),
- os.path.join(self.artifact_dir, 'dir1'),
- os.path.join(self.artifact_dir, 'dir2')]
- for _dir in dirs:
- ensure_dir(_dir)
-
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'Init': {'side_effect': self.init_class}},
- clean.remove_artifacts, remove_logs=False)
- self.assertEqual(0, retcode)
- for expected_dir in dirs[:2]:
- self.assertTrue(
- os.path.exists(expected_dir),
- 'Missing {0} dir'.format(expected_dir))
- for deleted_dir in dirs[2:]:
- self.assertFalse(
- os.path.exists(deleted_dir),
- 'Unexpected {0} dir'.format(deleted_dir))
-
- def test_remove_artifacts_removes_artifacts_removes_seed(self):
- """remove_artifacts removes seed dir when remove_seed is True."""
- dirs = [
- self.artifact_dir,
- os.path.join(self.artifact_dir, 'seed'),
- os.path.join(self.artifact_dir, 'dir1'),
- os.path.join(self.artifact_dir, 'dir2')]
- for _dir in dirs:
- ensure_dir(_dir)
-
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'Init': {'side_effect': self.init_class}},
- clean.remove_artifacts, remove_logs=False, remove_seed=True)
- self.assertEqual(0, retcode)
- self.assertTrue(
- os.path.exists(self.artifact_dir), 'Missing artifact dir')
- for deleted_dir in dirs[1:]:
- self.assertFalse(
- os.path.exists(deleted_dir),
- 'Unexpected {0} dir'.format(deleted_dir))
-
- def test_remove_artifacts_returns_one_on_errors(self):
- """remove_artifacts returns non-zero on failure and prints an error."""
- ensure_dir(self.artifact_dir)
- ensure_dir(os.path.join(self.artifact_dir, 'dir1'))
-
- with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'del_dir': {'side_effect': OSError('oops')},
- 'Init': {'side_effect': self.init_class}},
- clean.remove_artifacts, remove_logs=False)
- self.assertEqual(1, retcode)
- self.assertEqual(
- 'ERROR: Could not remove %s/dir1: oops\n' % self.artifact_dir,
- m_stderr.getvalue())
-
- def test_handle_clean_args_reboots(self):
- """handle_clean_args_reboots when reboot arg is provided."""
-
- called_cmds = []
-
- def fake_subp(cmd, capture):
- called_cmds.append((cmd, capture))
- return '', ''
-
- myargs = namedtuple('MyArgs', 'remove_logs remove_seed reboot')
- cmdargs = myargs(remove_logs=False, remove_seed=False, reboot=True)
- retcode = wrap_and_call(
- 'cloudinit.cmd.clean',
- {'subp': {'side_effect': fake_subp},
- 'Init': {'side_effect': self.init_class}},
- clean.handle_clean_args, name='does not matter', args=cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual(
- [(['shutdown', '-r', 'now'], False)], called_cmds)
-
- def test_status_main(self):
- '''clean.main can be run as a standalone script.'''
- write_file(self.log1, 'cloud-init-log')
- with self.assertRaises(SystemExit) as context_manager:
- wrap_and_call(
- 'cloudinit.cmd.clean',
- {'Init': {'side_effect': self.init_class},
- 'sys.argv': {'new': ['clean', '--logs']}},
- clean.main)
-
- self.assertEqual(0, context_manager.exception.code)
- self.assertFalse(
- os.path.exists(self.log1), 'Unexpected log {0}'.format(self.log1))
-
-
-# vi: ts=4 expandtab syntax=python
diff --git a/cloudinit/cmd/tests/test_cloud_id.py b/cloudinit/cmd/tests/test_cloud_id.py
deleted file mode 100644
index 3f3727fd..00000000
--- a/cloudinit/cmd/tests/test_cloud_id.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests for cloud-id command line utility."""
-
-from cloudinit import util
-from collections import namedtuple
-from io import StringIO
-
-from cloudinit.cmd import cloud_id
-
-from cloudinit.tests.helpers import CiTestCase, mock
-
-
-class TestCloudId(CiTestCase):
-
- args = namedtuple('cloudidargs', ('instance_data json long'))
-
- def setUp(self):
- super(TestCloudId, self).setUp()
- self.tmp = self.tmp_dir()
- self.instance_data = self.tmp_path('instance-data.json', dir=self.tmp)
-
- def test_cloud_id_arg_parser_defaults(self):
- """Validate the argument defaults when not provided by the end-user."""
- cmd = ['cloud-id']
- with mock.patch('sys.argv', cmd):
- args = cloud_id.get_parser().parse_args()
- self.assertEqual(
- '/run/cloud-init/instance-data.json',
- args.instance_data)
- self.assertEqual(False, args.long)
- self.assertEqual(False, args.json)
-
- def test_cloud_id_arg_parse_overrides(self):
- """Override argument defaults by specifying values for each param."""
- util.write_file(self.instance_data, '{}')
- cmd = ['cloud-id', '--instance-data', self.instance_data, '--long',
- '--json']
- with mock.patch('sys.argv', cmd):
- args = cloud_id.get_parser().parse_args()
- self.assertEqual(self.instance_data, args.instance_data)
- self.assertEqual(True, args.long)
- self.assertEqual(True, args.json)
-
- def test_cloud_id_missing_instance_data_json(self):
- """Exit error when the provided instance-data.json does not exist."""
- cmd = ['cloud-id', '--instance-data', self.instance_data]
- with mock.patch('sys.argv', cmd):
- with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
- with self.assertRaises(SystemExit) as context_manager:
- cloud_id.main()
- self.assertEqual(1, context_manager.exception.code)
- self.assertIn(
- "ERROR: File not found '%s'" % self.instance_data,
- m_stderr.getvalue())
-
- def test_cloud_id_non_json_instance_data(self):
- """Exit error when the provided instance-data.json is not json."""
- cmd = ['cloud-id', '--instance-data', self.instance_data]
- util.write_file(self.instance_data, '{')
- with mock.patch('sys.argv', cmd):
- with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
- with self.assertRaises(SystemExit) as context_manager:
- cloud_id.main()
- self.assertEqual(1, context_manager.exception.code)
- self.assertIn(
- "ERROR: File '%s' is not valid json." % self.instance_data,
- m_stderr.getvalue())
-
- def test_cloud_id_from_cloud_name_in_instance_data(self):
- """Report canonical cloud-id from cloud_name in instance-data."""
- util.write_file(
- self.instance_data,
- '{"v1": {"cloud_name": "mycloud", "region": "somereg"}}')
- cmd = ['cloud-id', '--instance-data', self.instance_data]
- with mock.patch('sys.argv', cmd):
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- with self.assertRaises(SystemExit) as context_manager:
- cloud_id.main()
- self.assertEqual(0, context_manager.exception.code)
- self.assertEqual("mycloud\n", m_stdout.getvalue())
-
- def test_cloud_id_long_name_from_instance_data(self):
- """Report long cloud-id format from cloud_name and region."""
- util.write_file(
- self.instance_data,
- '{"v1": {"cloud_name": "mycloud", "region": "somereg"}}')
- cmd = ['cloud-id', '--instance-data', self.instance_data, '--long']
- with mock.patch('sys.argv', cmd):
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- with self.assertRaises(SystemExit) as context_manager:
- cloud_id.main()
- self.assertEqual(0, context_manager.exception.code)
- self.assertEqual("mycloud\tsomereg\n", m_stdout.getvalue())
-
- def test_cloud_id_lookup_from_instance_data_region(self):
- """Report discovered canonical cloud_id when region lookup matches."""
- util.write_file(
- self.instance_data,
- '{"v1": {"cloud_name": "aws", "region": "cn-north-1",'
- ' "platform": "ec2"}}')
- cmd = ['cloud-id', '--instance-data', self.instance_data, '--long']
- with mock.patch('sys.argv', cmd):
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- with self.assertRaises(SystemExit) as context_manager:
- cloud_id.main()
- self.assertEqual(0, context_manager.exception.code)
- self.assertEqual("aws-china\tcn-north-1\n", m_stdout.getvalue())
-
- def test_cloud_id_lookup_json_instance_data_adds_cloud_id_to_json(self):
- """Report v1 instance-data content with cloud_id when --json set."""
- util.write_file(
- self.instance_data,
- '{"v1": {"cloud_name": "unknown", "region": "dfw",'
- ' "platform": "openstack", "public_ssh_keys": []}}')
- expected = util.json_dumps({
- 'cloud_id': 'openstack', 'cloud_name': 'unknown',
- 'platform': 'openstack', 'public_ssh_keys': [], 'region': 'dfw'})
- cmd = ['cloud-id', '--instance-data', self.instance_data, '--json']
- with mock.patch('sys.argv', cmd):
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- with self.assertRaises(SystemExit) as context_manager:
- cloud_id.main()
- self.assertEqual(0, context_manager.exception.code)
- self.assertEqual(expected + '\n', m_stdout.getvalue())
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py
deleted file mode 100644
index 2e380848..00000000
--- a/cloudinit/cmd/tests/test_main.py
+++ /dev/null
@@ -1,188 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from collections import namedtuple
-import copy
-import os
-from io import StringIO
-from unittest import mock
-
-import pytest
-
-from cloudinit.cmd import main
-from cloudinit import safeyaml
-from cloudinit.util import (
- ensure_dir, load_file, write_file)
-from cloudinit.tests.helpers import (
- FilesystemMockingTestCase, wrap_and_call)
-
-mypaths = namedtuple('MyPaths', 'run_dir')
-myargs = namedtuple('MyArgs', 'debug files force local reporter subcommand')
-
-
-class TestMain(FilesystemMockingTestCase):
- with_logs = True
- allowed_subp = False
-
- def setUp(self):
- super(TestMain, self).setUp()
- self.new_root = self.tmp_dir()
- self.cloud_dir = self.tmp_path('var/lib/cloud/', dir=self.new_root)
- os.makedirs(self.cloud_dir)
- self.replicateTestRoot('simple_ubuntu', self.new_root)
- self.cfg = {
- 'datasource_list': ['None'],
- 'runcmd': ['ls /etc'], # test ALL_DISTROS
- 'system_info': {'paths': {'cloud_dir': self.cloud_dir,
- 'run_dir': self.new_root}},
- 'write_files': [
- {
- 'path': '/etc/blah.ini',
- 'content': 'blah',
- 'permissions': 0o755,
- },
- ],
- 'cloud_init_modules': ['write-files', 'runcmd'],
- }
- cloud_cfg = safeyaml.dumps(self.cfg)
- ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
- self.cloud_cfg_file = os.path.join(
- self.new_root, 'etc', 'cloud', 'cloud.cfg')
- write_file(self.cloud_cfg_file, cloud_cfg)
- self.patchOS(self.new_root)
- self.patchUtils(self.new_root)
- self.stderr = StringIO()
- self.patchStdoutAndStderr(stderr=self.stderr)
-
- def test_main_init_run_net_stops_on_file_no_net(self):
- """When no-net file is present, main_init does not process modules."""
- stop_file = os.path.join(self.cloud_dir, 'data', 'no-net') # stop file
- write_file(stop_file, '')
- cmdargs = myargs(
- debug=False, files=None, force=False, local=False, reporter=None,
- subcommand='init')
- (_item1, item2) = wrap_and_call(
- 'cloudinit.cmd.main',
- {'util.close_stdin': True,
- 'netinfo.debug_info': 'my net debug info',
- 'util.fixup_output': ('outfmt', 'errfmt')},
- main.main_init, 'init', cmdargs)
- # We should not run write_files module
- self.assertFalse(
- os.path.exists(os.path.join(self.new_root, 'etc/blah.ini')),
- 'Unexpected run of write_files module produced blah.ini')
- self.assertEqual([], item2)
- # Instancify is called
- instance_id_path = 'var/lib/cloud/data/instance-id'
- self.assertFalse(
- os.path.exists(os.path.join(self.new_root, instance_id_path)),
- 'Unexpected call to datasource.instancify produced instance-id')
- expected_logs = [
- "Exiting. stop file ['{stop_file}'] existed\n".format(
- stop_file=stop_file),
- 'my net debug info' # netinfo.debug_info
- ]
- for log in expected_logs:
- self.assertIn(log, self.stderr.getvalue())
-
- def test_main_init_run_net_runs_modules(self):
- """Modules like write_files are run in 'net' mode."""
- cmdargs = myargs(
- debug=False, files=None, force=False, local=False, reporter=None,
- subcommand='init')
- (_item1, item2) = wrap_and_call(
- 'cloudinit.cmd.main',
- {'util.close_stdin': True,
- 'netinfo.debug_info': 'my net debug info',
- 'util.fixup_output': ('outfmt', 'errfmt')},
- main.main_init, 'init', cmdargs)
- self.assertEqual([], item2)
- # Instancify is called
- instance_id_path = 'var/lib/cloud/data/instance-id'
- self.assertEqual(
- 'iid-datasource-none\n',
- os.path.join(load_file(
- os.path.join(self.new_root, instance_id_path))))
- # modules are run (including write_files)
- self.assertEqual(
- 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini')))
- expected_logs = [
- 'network config is disabled by fallback', # apply_network_config
- 'my net debug info', # netinfo.debug_info
- 'no previous run detected'
- ]
- for log in expected_logs:
- self.assertIn(log, self.stderr.getvalue())
-
- def test_main_init_run_net_calls_set_hostname_when_metadata_present(self):
- """When local-hostname metadata is present, call cc_set_hostname."""
- self.cfg['datasource'] = {
- 'None': {'metadata': {'local-hostname': 'md-hostname'}}}
- cloud_cfg = safeyaml.dumps(self.cfg)
- write_file(self.cloud_cfg_file, cloud_cfg)
- cmdargs = myargs(
- debug=False, files=None, force=False, local=False, reporter=None,
- subcommand='init')
-
- def set_hostname(name, cfg, cloud, log, args):
- self.assertEqual('set-hostname', name)
- updated_cfg = copy.deepcopy(self.cfg)
- updated_cfg.update(
- {'def_log_file': '/var/log/cloud-init.log',
- 'log_cfgs': [],
- 'syslog_fix_perms': [
- 'syslog:adm', 'root:adm', 'root:wheel', 'root:root'
- ],
- 'vendor_data': {'enabled': True, 'prefix': []},
- 'vendor_data2': {'enabled': True, 'prefix': []}})
- updated_cfg.pop('system_info')
-
- self.assertEqual(updated_cfg, cfg)
- self.assertEqual(main.LOG, log)
- self.assertIsNone(args)
-
- (_item1, item2) = wrap_and_call(
- 'cloudinit.cmd.main',
- {'util.close_stdin': True,
- 'netinfo.debug_info': 'my net debug info',
- 'cc_set_hostname.handle': {'side_effect': set_hostname},
- 'util.fixup_output': ('outfmt', 'errfmt')},
- main.main_init, 'init', cmdargs)
- self.assertEqual([], item2)
- # Instancify is called
- instance_id_path = 'var/lib/cloud/data/instance-id'
- self.assertEqual(
- 'iid-datasource-none\n',
- os.path.join(load_file(
- os.path.join(self.new_root, instance_id_path))))
- # modules are run (including write_files)
- self.assertEqual(
- 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini')))
- expected_logs = [
- 'network config is disabled by fallback', # apply_network_config
- 'my net debug info', # netinfo.debug_info
- 'no previous run detected'
- ]
- for log in expected_logs:
- self.assertIn(log, self.stderr.getvalue())
-
-
-class TestShouldBringUpInterfaces:
- @pytest.mark.parametrize('cfg_disable,args_local,expected', [
- (True, True, False),
- (True, False, False),
- (False, True, False),
- (False, False, True),
- ])
- def test_should_bring_up_interfaces(
- self, cfg_disable, args_local, expected
- ):
- init = mock.Mock()
- init.cfg = {'disable_network_activation': cfg_disable}
-
- args = mock.Mock()
- args.local = args_local
-
- result = main._should_bring_up_interfaces(init, args)
- assert result == expected
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/tests/test_query.py b/cloudinit/cmd/tests/test_query.py
deleted file mode 100644
index d96c3945..00000000
--- a/cloudinit/cmd/tests/test_query.py
+++ /dev/null
@@ -1,392 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import errno
-import gzip
-from io import BytesIO
-import json
-from textwrap import dedent
-
-import pytest
-
-from collections import namedtuple
-from cloudinit.cmd import query
-from cloudinit.helpers import Paths
-from cloudinit.sources import (
- REDACT_SENSITIVE_VALUE, INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE)
-from cloudinit.tests.helpers import mock
-
-from cloudinit.util import b64e, write_file
-
-
-def _gzip_data(data):
- with BytesIO() as iobuf:
- with gzip.GzipFile(mode="wb", fileobj=iobuf) as gzfp:
- gzfp.write(data)
- return iobuf.getvalue()
-
-
-@mock.patch("cloudinit.cmd.query.addLogHandlerCLI", lambda *args: "")
-class TestQuery:
-
- args = namedtuple(
- 'queryargs',
- ('debug dump_all format instance_data list_keys user_data vendor_data'
- ' varname'))
-
- def _setup_paths(self, tmpdir, ud_val=None, vd_val=None):
- """Write userdata and vendordata into a tmpdir.
-
- Return:
- 4-tuple : (paths, run_dir_path, userdata_path, vendordata_path)
- """
- if ud_val:
- user_data = tmpdir.join('user-data')
- write_file(user_data.strpath, ud_val)
- else:
- user_data = None
- if vd_val:
- vendor_data = tmpdir.join('vendor-data')
- write_file(vendor_data.strpath, vd_val)
- else:
- vendor_data = None
- run_dir = tmpdir.join('run_dir')
- run_dir.ensure_dir()
- return (
- Paths({'run_dir': run_dir.strpath}),
- run_dir,
- user_data,
- vendor_data
- )
-
- def test_handle_args_error_on_missing_param(self, caplog, capsys):
- """Error when missing required parameters and print usage."""
- args = self.args(
- debug=False, dump_all=False, format=None, instance_data=None,
- list_keys=False, user_data=None, vendor_data=None, varname=None)
- with mock.patch(
- "cloudinit.cmd.query.addLogHandlerCLI", return_value=""
- ) as m_cli_log:
- assert 1 == query.handle_args('anyname', args)
- expected_error = (
- 'Expected one of the options: --all, --format, --list-keys'
- ' or varname\n')
- assert expected_error in caplog.text
- out, _err = capsys.readouterr()
- assert 'usage: query' in out
- assert 1 == m_cli_log.call_count
-
- @pytest.mark.parametrize(
- "inst_data,varname,expected_error", (
- (
- '{"v1": {"key-2": "value-2"}}',
- 'v1.absent_leaf',
- "instance-data 'v1' has no 'absent_leaf'\n"
- ),
- (
- '{"v1": {"key-2": "value-2"}}',
- 'absent_key',
- "Undefined instance-data key 'absent_key'\n"
- ),
- )
- )
- def test_handle_args_error_on_invalid_vaname_paths(
- self, inst_data, varname, expected_error, caplog, tmpdir
- ):
- """Error when varname is not a valid instance-data variable path."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write(inst_data)
- args = self.args(
- debug=False, dump_all=False, format=None,
- instance_data=instance_data.strpath,
- list_keys=False, user_data=None, vendor_data=None, varname=varname
- )
- paths, _, _, _ = self._setup_paths(tmpdir)
- with mock.patch('cloudinit.cmd.query.read_cfg_paths') as m_paths:
- m_paths.return_value = paths
- with mock.patch(
- "cloudinit.cmd.query.addLogHandlerCLI", return_value=""
- ):
- assert 1 == query.handle_args('anyname', args)
- assert expected_error in caplog.text
-
- def test_handle_args_error_on_missing_instance_data(self, caplog, tmpdir):
- """When instance_data file path does not exist, log an error."""
- absent_fn = tmpdir.join('absent')
- args = self.args(
- debug=False, dump_all=True, format=None,
- instance_data=absent_fn.strpath,
- list_keys=False, user_data='ud', vendor_data='vd', varname=None)
- assert 1 == query.handle_args('anyname', args)
-
- msg = 'Missing instance-data file: %s' % absent_fn
- assert msg in caplog.text
-
- def test_handle_args_error_when_no_read_permission_instance_data(
- self, caplog, tmpdir
- ):
- """When instance_data file is unreadable, log an error."""
- noread_fn = tmpdir.join('unreadable')
- noread_fn.write('thou shall not pass')
- args = self.args(
- debug=False, dump_all=True, format=None,
- instance_data=noread_fn.strpath,
- list_keys=False, user_data='ud', vendor_data='vd', varname=None)
- with mock.patch('cloudinit.cmd.query.util.load_file') as m_load:
- m_load.side_effect = OSError(errno.EACCES, 'Not allowed')
- assert 1 == query.handle_args('anyname', args)
- msg = "No read permission on '%s'. Try sudo" % noread_fn
- assert msg in caplog.text
-
- def test_handle_args_defaults_instance_data(self, caplog, tmpdir):
- """When no instance_data argument, default to configured run_dir."""
- args = self.args(
- debug=False, dump_all=True, format=None, instance_data=None,
- list_keys=False, user_data=None, vendor_data=None, varname=None)
- paths, run_dir, _, _ = self._setup_paths(tmpdir)
- with mock.patch('cloudinit.cmd.query.read_cfg_paths') as m_paths:
- m_paths.return_value = paths
- assert 1 == query.handle_args('anyname', args)
- json_file = run_dir.join(INSTANCE_JSON_FILE)
- msg = 'Missing instance-data file: %s' % json_file.strpath
- assert msg in caplog.text
-
- def test_handle_args_root_fallsback_to_instance_data(self, caplog, tmpdir):
- """When no instance_data argument, root falls back to redacted json."""
- args = self.args(
- debug=False, dump_all=True, format=None, instance_data=None,
- list_keys=False, user_data=None, vendor_data=None, varname=None)
- paths, run_dir, _, _ = self._setup_paths(tmpdir)
- with mock.patch('cloudinit.cmd.query.read_cfg_paths') as m_paths:
- m_paths.return_value = paths
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 0
- assert 1 == query.handle_args('anyname', args)
- json_file = run_dir.join(INSTANCE_JSON_FILE)
- sensitive_file = run_dir.join(INSTANCE_JSON_SENSITIVE_FILE)
- msg = (
- 'Missing root-readable %s. Using redacted %s instead.' %
- (
- sensitive_file.strpath, json_file.strpath
- )
- )
- assert msg in caplog.text
-
- @pytest.mark.parametrize(
- 'ud_src,ud_expected,vd_src,vd_expected',
- (
- ('hi mom', 'hi mom', 'hi pops', 'hi pops'),
- ('ud'.encode('utf-8'), 'ud', 'vd'.encode('utf-8'), 'vd'),
- (_gzip_data(b'ud'), 'ud', _gzip_data(b'vd'), 'vd'),
- (_gzip_data('ud'.encode('utf-8')), 'ud', _gzip_data(b'vd'), 'vd'),
- )
- )
- def test_handle_args_root_processes_user_data(
- self, ud_src, ud_expected, vd_src, vd_expected, capsys, tmpdir
- ):
- """Support reading multiple user-data file content types"""
- paths, run_dir, user_data, vendor_data = self._setup_paths(
- tmpdir, ud_val=ud_src, vd_val=vd_src
- )
- sensitive_file = run_dir.join(INSTANCE_JSON_SENSITIVE_FILE)
- sensitive_file.write('{"my-var": "it worked"}')
- args = self.args(
- debug=False, dump_all=True, format=None, instance_data=None,
- list_keys=False, user_data=user_data.strpath,
- vendor_data=vendor_data.strpath, varname=None)
- with mock.patch('cloudinit.cmd.query.read_cfg_paths') as m_paths:
- m_paths.return_value = paths
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 0
- assert 0 == query.handle_args('anyname', args)
- out, _err = capsys.readouterr()
- cmd_output = json.loads(out)
- assert "it worked" == cmd_output['my-var']
- if ud_expected == "ci-b64:":
- ud_expected = "ci-b64:{}".format(b64e(ud_src))
- if vd_expected == "ci-b64:":
- vd_expected = "ci-b64:{}".format(b64e(vd_src))
- assert ud_expected == cmd_output['userdata']
- assert vd_expected == cmd_output['vendordata']
-
- def test_handle_args_root_uses_instance_sensitive_data(
- self, capsys, tmpdir
- ):
- """When no instance_data argument, root uses sensitive json."""
- paths, run_dir, user_data, vendor_data = self._setup_paths(
- tmpdir, ud_val='ud', vd_val='vd'
- )
- sensitive_file = run_dir.join(INSTANCE_JSON_SENSITIVE_FILE)
- sensitive_file.write('{"my-var": "it worked"}')
- args = self.args(
- debug=False, dump_all=True, format=None, instance_data=None,
- list_keys=False, user_data=user_data.strpath,
- vendor_data=vendor_data.strpath, varname=None)
- with mock.patch('cloudinit.cmd.query.read_cfg_paths') as m_paths:
- m_paths.return_value = paths
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 0
- assert 0 == query.handle_args('anyname', args)
- expected = (
- '{\n "my-var": "it worked",\n '
- '"userdata": "ud",\n "vendordata": "vd"\n}\n'
- )
- out, _err = capsys.readouterr()
- assert expected == out
-
- def test_handle_args_dumps_all_instance_data(self, capsys, tmpdir):
- """When --all is specified query will dump all instance data vars."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write('{"my-var": "it worked"}')
- args = self.args(
- debug=False, dump_all=True, format=None,
- instance_data=instance_data.strpath, list_keys=False,
- user_data='ud', vendor_data='vd', varname=None)
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 0 == query.handle_args('anyname', args)
- expected = (
- '{\n "my-var": "it worked",\n "userdata": "<%s> file:ud",\n'
- ' "vendordata": "<%s> file:vd"\n}\n' % (
- REDACT_SENSITIVE_VALUE, REDACT_SENSITIVE_VALUE
- )
- )
- out, _err = capsys.readouterr()
- assert expected == out
-
- def test_handle_args_returns_top_level_varname(self, capsys, tmpdir):
- """When the argument varname is passed, report its value."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write('{"my-var": "it worked"}')
- args = self.args(
- debug=False, dump_all=True, format=None,
- instance_data=instance_data.strpath, list_keys=False,
- user_data='ud', vendor_data='vd', varname='my_var')
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 0 == query.handle_args('anyname', args)
- out, _err = capsys.readouterr()
- assert 'it worked\n' == out
-
- @pytest.mark.parametrize(
- 'inst_data,varname,expected',
- (
- (
- '{"v1": {"key-2": "value-2"}, "my-var": "it worked"}',
- 'v1.key_2',
- 'value-2\n'
- ),
- # Assert no jinja underscore-delimited aliases are reported on CLI
- (
- '{"v1": {"something-hyphenated": {"no.underscores":"x",'
- ' "no-alias": "y"}}, "my-var": "it worked"}',
- 'v1.something_hyphenated',
- '{\n "no-alias": "y",\n "no.underscores": "x"\n}\n'
- ),
- )
- )
- def test_handle_args_returns_nested_varname(
- self, inst_data, varname, expected, capsys, tmpdir
- ):
- """If user_data file is a jinja template render instance-data vars."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write(inst_data)
- args = self.args(
- debug=False, dump_all=False, format=None,
- instance_data=instance_data.strpath, user_data='ud',
- vendor_data='vd', list_keys=False, varname=varname)
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 0 == query.handle_args('anyname', args)
- out, _err = capsys.readouterr()
- assert expected == out
-
- def test_handle_args_returns_standardized_vars_to_top_level_aliases(
- self, capsys, tmpdir
- ):
- """Any standardized vars under v# are promoted as top-level aliases."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write(
- '{"v1": {"v1_1": "val1.1"}, "v2": {"v2_2": "val2.2"},'
- ' "top": "gun"}')
- expected = dedent("""\
- {
- "top": "gun",
- "userdata": "<redacted for non-root user> file:ud",
- "v1": {
- "v1_1": "val1.1"
- },
- "v1_1": "val1.1",
- "v2": {
- "v2_2": "val2.2"
- },
- "v2_2": "val2.2",
- "vendordata": "<redacted for non-root user> file:vd"
- }
- """)
- args = self.args(
- debug=False, dump_all=True, format=None,
- instance_data=instance_data.strpath, user_data='ud',
- vendor_data='vd', list_keys=False, varname=None)
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 0 == query.handle_args('anyname', args)
- out, _err = capsys.readouterr()
- assert expected == out
-
- def test_handle_args_list_keys_sorts_top_level_keys_when_no_varname(
- self, capsys, tmpdir
- ):
- """Sort all top-level keys when only --list-keys provided."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write(
- '{"v1": {"v1_1": "val1.1"}, "v2": {"v2_2": "val2.2"},'
- ' "top": "gun"}')
- expected = 'top\nuserdata\nv1\nv1_1\nv2\nv2_2\nvendordata\n'
- args = self.args(
- debug=False, dump_all=False, format=None,
- instance_data=instance_data.strpath, list_keys=True,
- user_data='ud', vendor_data='vd', varname=None)
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 0 == query.handle_args('anyname', args)
- out, _err = capsys.readouterr()
- assert expected == out
-
- def test_handle_args_list_keys_sorts_nested_keys_when_varname(
- self, capsys, tmpdir
- ):
- """Sort all nested keys of varname object when --list-keys provided."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write(
- '{"v1": {"v1_1": "val1.1", "v1_2": "val1.2"}, "v2":' +
- ' {"v2_2": "val2.2"}, "top": "gun"}')
- expected = 'v1_1\nv1_2\n'
- args = self.args(
- debug=False, dump_all=False, format=None,
- instance_data=instance_data.strpath, list_keys=True,
- user_data='ud', vendor_data='vd', varname='v1')
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 0 == query.handle_args('anyname', args)
- out, _err = capsys.readouterr()
- assert expected == out
-
- def test_handle_args_list_keys_errors_when_varname_is_not_a_dict(
- self, caplog, tmpdir
- ):
- """Raise an error when --list-keys and varname specify a non-list."""
- instance_data = tmpdir.join('instance-data')
- instance_data.write(
- '{"v1": {"v1_1": "val1.1", "v1_2": "val1.2"}, "v2": ' +
- '{"v2_2": "val2.2"}, "top": "gun"}')
- expected_error = "--list-keys provided but 'top' is not a dict"
- args = self.args(
- debug=False, dump_all=False, format=None,
- instance_data=instance_data.strpath, list_keys=True,
- user_data='ud', vendor_data='vd', varname='top')
- with mock.patch('os.getuid') as m_getuid:
- m_getuid.return_value = 100
- assert 1 == query.handle_args('anyname', args)
- assert expected_error in caplog.text
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/tests/test_status.py b/cloudinit/cmd/tests/test_status.py
deleted file mode 100644
index 1c9eec37..00000000
--- a/cloudinit/cmd/tests/test_status.py
+++ /dev/null
@@ -1,391 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from collections import namedtuple
-import os
-from io import StringIO
-from textwrap import dedent
-
-from cloudinit.atomic_helper import write_json
-from cloudinit.cmd import status
-from cloudinit.util import ensure_file
-from cloudinit.tests.helpers import CiTestCase, wrap_and_call, mock
-
-mypaths = namedtuple('MyPaths', 'run_dir')
-myargs = namedtuple('MyArgs', 'long wait')
-
-
-class TestStatus(CiTestCase):
-
- def setUp(self):
- super(TestStatus, self).setUp()
- self.new_root = self.tmp_dir()
- self.status_file = self.tmp_path('status.json', self.new_root)
- self.disable_file = self.tmp_path('cloudinit-disable', self.new_root)
- self.paths = mypaths(run_dir=self.new_root)
-
- class FakeInit(object):
- paths = self.paths
-
- def __init__(self, ds_deps):
- pass
-
- def read_cfg(self):
- pass
-
- self.init_class = FakeInit
-
- def test__is_cloudinit_disabled_false_on_sysvinit(self):
- '''When not in an environment using systemd, return False.'''
- ensure_file(self.disable_file) # Create the ignored disable file
- (is_disabled, reason) = wrap_and_call(
- 'cloudinit.cmd.status',
- {'uses_systemd': False,
- 'get_cmdline': "root=/dev/my-root not-important"},
- status._is_cloudinit_disabled, self.disable_file, self.paths)
- self.assertFalse(
- is_disabled, 'expected enabled cloud-init on sysvinit')
- self.assertEqual('Cloud-init enabled on sysvinit', reason)
-
- def test__is_cloudinit_disabled_true_on_disable_file(self):
- '''When using systemd and disable_file is present return disabled.'''
- ensure_file(self.disable_file) # Create observed disable file
- (is_disabled, reason) = wrap_and_call(
- 'cloudinit.cmd.status',
- {'uses_systemd': True,
- 'get_cmdline': "root=/dev/my-root not-important"},
- status._is_cloudinit_disabled, self.disable_file, self.paths)
- self.assertTrue(is_disabled, 'expected disabled cloud-init')
- self.assertEqual(
- 'Cloud-init disabled by {0}'.format(self.disable_file), reason)
-
- def test__is_cloudinit_disabled_false_on_kernel_cmdline_enable(self):
- '''Not disabled when using systemd and enabled via commandline.'''
- ensure_file(self.disable_file) # Create ignored disable file
- (is_disabled, reason) = wrap_and_call(
- 'cloudinit.cmd.status',
- {'uses_systemd': True,
- 'get_cmdline': 'something cloud-init=enabled else'},
- status._is_cloudinit_disabled, self.disable_file, self.paths)
- self.assertFalse(is_disabled, 'expected enabled cloud-init')
- self.assertEqual(
- 'Cloud-init enabled by kernel command line cloud-init=enabled',
- reason)
-
- def test__is_cloudinit_disabled_true_on_kernel_cmdline(self):
- '''When using systemd and disable_file is present return disabled.'''
- (is_disabled, reason) = wrap_and_call(
- 'cloudinit.cmd.status',
- {'uses_systemd': True,
- 'get_cmdline': 'something cloud-init=disabled else'},
- status._is_cloudinit_disabled, self.disable_file, self.paths)
- self.assertTrue(is_disabled, 'expected disabled cloud-init')
- self.assertEqual(
- 'Cloud-init disabled by kernel parameter cloud-init=disabled',
- reason)
-
- def test__is_cloudinit_disabled_true_when_generator_disables(self):
- '''When cloud-init-generator doesn't write enabled file return True.'''
- enabled_file = os.path.join(self.paths.run_dir, 'enabled')
- self.assertFalse(os.path.exists(enabled_file))
- (is_disabled, reason) = wrap_and_call(
- 'cloudinit.cmd.status',
- {'uses_systemd': True,
- 'get_cmdline': 'something'},
- status._is_cloudinit_disabled, self.disable_file, self.paths)
- self.assertTrue(is_disabled, 'expected disabled cloud-init')
- self.assertEqual('Cloud-init disabled by cloud-init-generator', reason)
-
- def test__is_cloudinit_disabled_false_when_enabled_in_systemd(self):
- '''Report enabled when systemd generator creates the enabled file.'''
- enabled_file = os.path.join(self.paths.run_dir, 'enabled')
- ensure_file(enabled_file)
- (is_disabled, reason) = wrap_and_call(
- 'cloudinit.cmd.status',
- {'uses_systemd': True,
- 'get_cmdline': 'something ignored'},
- status._is_cloudinit_disabled, self.disable_file, self.paths)
- self.assertFalse(is_disabled, 'expected enabled cloud-init')
- self.assertEqual(
- 'Cloud-init enabled by systemd cloud-init-generator', reason)
-
- def test_status_returns_not_run(self):
- '''When status.json does not exist yet, return 'not run'.'''
- self.assertFalse(
- os.path.exists(self.status_file), 'Unexpected status.json found')
- cmdargs = myargs(long=False, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual('status: not run\n', m_stdout.getvalue())
-
- def test_status_returns_disabled_long_on_presence_of_disable_file(self):
- '''When cloudinit is disabled, return disabled reason.'''
-
- checked_files = []
-
- def fakeexists(filepath):
- checked_files.append(filepath)
- status_file = os.path.join(self.paths.run_dir, 'status.json')
- return bool(not filepath == status_file)
-
- cmdargs = myargs(long=True, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'os.path.exists': {'side_effect': fakeexists},
- '_is_cloudinit_disabled': (True, 'disabled for some reason'),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual(
- [os.path.join(self.paths.run_dir, 'status.json')],
- checked_files)
- expected = dedent('''\
- status: disabled
- detail:
- disabled for some reason
- ''')
- self.assertEqual(expected, m_stdout.getvalue())
-
- def test_status_returns_running_on_no_results_json(self):
- '''Report running when status.json exists but result.json does not.'''
- result_file = self.tmp_path('result.json', self.new_root)
- write_json(self.status_file, {})
- self.assertFalse(
- os.path.exists(result_file), 'Unexpected result.json found')
- cmdargs = myargs(long=False, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual('status: running\n', m_stdout.getvalue())
-
- def test_status_returns_running(self):
- '''Report running when status exists with an unfinished stage.'''
- ensure_file(self.tmp_path('result.json', self.new_root))
- write_json(self.status_file,
- {'v1': {'init': {'start': 1, 'finished': None}}})
- cmdargs = myargs(long=False, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual('status: running\n', m_stdout.getvalue())
-
- def test_status_returns_done(self):
- '''Report done results.json exists no stages are unfinished.'''
- ensure_file(self.tmp_path('result.json', self.new_root))
- write_json(
- self.status_file,
- {'v1': {'stage': None, # No current stage running
- 'datasource': (
- 'DataSourceNoCloud [seed=/var/.../seed/nocloud-net]'
- '[dsmode=net]'),
- 'blah': {'finished': 123.456},
- 'init': {'errors': [], 'start': 124.567,
- 'finished': 125.678},
- 'init-local': {'start': 123.45, 'finished': 123.46}}})
- cmdargs = myargs(long=False, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual('status: done\n', m_stdout.getvalue())
-
- def test_status_returns_done_long(self):
- '''Long format of done status includes datasource info.'''
- ensure_file(self.tmp_path('result.json', self.new_root))
- write_json(
- self.status_file,
- {'v1': {'stage': None,
- 'datasource': (
- 'DataSourceNoCloud [seed=/var/.../seed/nocloud-net]'
- '[dsmode=net]'),
- 'init': {'start': 124.567, 'finished': 125.678},
- 'init-local': {'start': 123.45, 'finished': 123.46}}})
- cmdargs = myargs(long=True, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- expected = dedent('''\
- status: done
- time: Thu, 01 Jan 1970 00:02:05 +0000
- detail:
- DataSourceNoCloud [seed=/var/.../seed/nocloud-net][dsmode=net]
- ''')
- self.assertEqual(expected, m_stdout.getvalue())
-
- def test_status_on_errors(self):
- '''Reports error when any stage has errors.'''
- write_json(
- self.status_file,
- {'v1': {'stage': None,
- 'blah': {'errors': [], 'finished': 123.456},
- 'init': {'errors': ['error1'], 'start': 124.567,
- 'finished': 125.678},
- 'init-local': {'start': 123.45, 'finished': 123.46}}})
- cmdargs = myargs(long=False, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(1, retcode)
- self.assertEqual('status: error\n', m_stdout.getvalue())
-
- def test_status_on_errors_long(self):
- '''Long format of error status includes all error messages.'''
- write_json(
- self.status_file,
- {'v1': {'stage': None,
- 'datasource': (
- 'DataSourceNoCloud [seed=/var/.../seed/nocloud-net]'
- '[dsmode=net]'),
- 'init': {'errors': ['error1'], 'start': 124.567,
- 'finished': 125.678},
- 'init-local': {'errors': ['error2', 'error3'],
- 'start': 123.45, 'finished': 123.46}}})
- cmdargs = myargs(long=True, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(1, retcode)
- expected = dedent('''\
- status: error
- time: Thu, 01 Jan 1970 00:02:05 +0000
- detail:
- error1
- error2
- error3
- ''')
- self.assertEqual(expected, m_stdout.getvalue())
-
- def test_status_returns_running_long_format(self):
- '''Long format reports the stage in which we are running.'''
- write_json(
- self.status_file,
- {'v1': {'stage': 'init',
- 'init': {'start': 124.456, 'finished': None},
- 'init-local': {'start': 123.45, 'finished': 123.46}}})
- cmdargs = myargs(long=True, wait=False)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- expected = dedent('''\
- status: running
- time: Thu, 01 Jan 1970 00:02:04 +0000
- detail:
- Running in stage: init
- ''')
- self.assertEqual(expected, m_stdout.getvalue())
-
- def test_status_wait_blocks_until_done(self):
- '''Specifying wait will poll every 1/4 second until done state.'''
- running_json = {
- 'v1': {'stage': 'init',
- 'init': {'start': 124.456, 'finished': None},
- 'init-local': {'start': 123.45, 'finished': 123.46}}}
- done_json = {
- 'v1': {'stage': None,
- 'init': {'start': 124.456, 'finished': 125.678},
- 'init-local': {'start': 123.45, 'finished': 123.46}}}
-
- self.sleep_calls = 0
-
- def fake_sleep(interval):
- self.assertEqual(0.25, interval)
- self.sleep_calls += 1
- if self.sleep_calls == 2:
- write_json(self.status_file, running_json)
- elif self.sleep_calls == 3:
- write_json(self.status_file, done_json)
- result_file = self.tmp_path('result.json', self.new_root)
- ensure_file(result_file)
-
- cmdargs = myargs(long=False, wait=True)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'sleep': {'side_effect': fake_sleep},
- '_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(0, retcode)
- self.assertEqual(4, self.sleep_calls)
- self.assertEqual('....\nstatus: done\n', m_stdout.getvalue())
-
- def test_status_wait_blocks_until_error(self):
- '''Specifying wait will poll every 1/4 second until error state.'''
- running_json = {
- 'v1': {'stage': 'init',
- 'init': {'start': 124.456, 'finished': None},
- 'init-local': {'start': 123.45, 'finished': 123.46}}}
- error_json = {
- 'v1': {'stage': None,
- 'init': {'errors': ['error1'], 'start': 124.456,
- 'finished': 125.678},
- 'init-local': {'start': 123.45, 'finished': 123.46}}}
-
- self.sleep_calls = 0
-
- def fake_sleep(interval):
- self.assertEqual(0.25, interval)
- self.sleep_calls += 1
- if self.sleep_calls == 2:
- write_json(self.status_file, running_json)
- elif self.sleep_calls == 3:
- write_json(self.status_file, error_json)
-
- cmdargs = myargs(long=False, wait=True)
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- retcode = wrap_and_call(
- 'cloudinit.cmd.status',
- {'sleep': {'side_effect': fake_sleep},
- '_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.handle_status_args, 'ignored', cmdargs)
- self.assertEqual(1, retcode)
- self.assertEqual(4, self.sleep_calls)
- self.assertEqual('....\nstatus: error\n', m_stdout.getvalue())
-
- def test_status_main(self):
- '''status.main can be run as a standalone script.'''
- write_json(self.status_file,
- {'v1': {'init': {'start': 1, 'finished': None}}})
- with self.assertRaises(SystemExit) as context_manager:
- with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
- wrap_and_call(
- 'cloudinit.cmd.status',
- {'sys.argv': {'new': ['status']},
- '_is_cloudinit_disabled': (False, ''),
- 'Init': {'side_effect': self.init_class}},
- status.main)
- self.assertEqual(0, context_manager.exception.code)
- self.assertEqual('status: running\n', m_stdout.getvalue())
-
-# vi: ts=4 expandtab syntax=python
diff --git a/cloudinit/config/tests/test_apt_pipelining.py b/cloudinit/config/tests/test_apt_pipelining.py
deleted file mode 100644
index 2a6bb10b..00000000
--- a/cloudinit/config/tests/test_apt_pipelining.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests cc_apt_pipelining handler"""
-
-import cloudinit.config.cc_apt_pipelining as cc_apt_pipelining
-
-from cloudinit.tests.helpers import CiTestCase, mock
-
-
-class TestAptPipelining(CiTestCase):
-
- @mock.patch('cloudinit.config.cc_apt_pipelining.util.write_file')
- def test_not_disabled_by_default(self, m_write_file):
- """ensure that default behaviour is to not disable pipelining"""
- cc_apt_pipelining.handle('foo', {}, None, mock.MagicMock(), None)
- self.assertEqual(0, m_write_file.call_count)
-
- @mock.patch('cloudinit.config.cc_apt_pipelining.util.write_file')
- def test_false_disables_pipelining(self, m_write_file):
- """ensure that pipelining can be disabled with correct config"""
- cc_apt_pipelining.handle(
- 'foo', {'apt_pipelining': 'false'}, None, mock.MagicMock(), None)
- self.assertEqual(1, m_write_file.call_count)
- args, _ = m_write_file.call_args
- self.assertEqual(cc_apt_pipelining.DEFAULT_FILE, args[0])
- self.assertIn('Pipeline-Depth "0"', args[1])
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/cloudinit/config/tests/test_disable_ec2_metadata.py
deleted file mode 100644
index b00f2083..00000000
--- a/cloudinit/config/tests/test_disable_ec2_metadata.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests cc_disable_ec2_metadata handler"""
-
-import cloudinit.config.cc_disable_ec2_metadata as ec2_meta
-
-from cloudinit.tests.helpers import CiTestCase, mock
-
-import logging
-
-LOG = logging.getLogger(__name__)
-
-DISABLE_CFG = {'disable_ec2_metadata': 'true'}
-
-
-class TestEC2MetadataRoute(CiTestCase):
-
- @mock.patch('cloudinit.config.cc_disable_ec2_metadata.subp.which')
- @mock.patch('cloudinit.config.cc_disable_ec2_metadata.subp.subp')
- def test_disable_ifconfig(self, m_subp, m_which):
- """Set the route if ifconfig command is available"""
- m_which.side_effect = lambda x: x if x == 'ifconfig' else None
- ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
- m_subp.assert_called_with(
- ['route', 'add', '-host', '169.254.169.254', 'reject'],
- capture=False)
-
- @mock.patch('cloudinit.config.cc_disable_ec2_metadata.subp.which')
- @mock.patch('cloudinit.config.cc_disable_ec2_metadata.subp.subp')
- def test_disable_ip(self, m_subp, m_which):
- """Set the route if ip command is available"""
- m_which.side_effect = lambda x: x if x == 'ip' else None
- ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
- m_subp.assert_called_with(
- ['ip', 'route', 'add', 'prohibit', '169.254.169.254'],
- capture=False)
-
- @mock.patch('cloudinit.config.cc_disable_ec2_metadata.subp.which')
- @mock.patch('cloudinit.config.cc_disable_ec2_metadata.subp.subp')
- def test_disable_no_tool(self, m_subp, m_which):
- """Log error when neither route nor ip commands are available"""
- m_which.return_value = None # Find neither ifconfig nor ip
- ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
- self.assertEqual(
- [mock.call('ip'), mock.call('ifconfig')], m_which.call_args_list)
- m_subp.assert_not_called()
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_final_message.py b/cloudinit/config/tests/test_final_message.py
deleted file mode 100644
index 46ba99b2..00000000
--- a/cloudinit/config/tests/test_final_message.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-import logging
-from unittest import mock
-
-import pytest
-
-from cloudinit.config.cc_final_message import handle
-
-
-class TestHandle:
- # TODO: Expand these tests to cover full functionality; currently they only
- # cover the logic around how the boot-finished file is written (and not its
- # contents).
-
- @pytest.mark.parametrize(
- "instance_dir_exists,file_is_written,expected_log_substring",
- [
- (True, True, None),
- (False, False, "Failed to write boot finished file "),
- ],
- )
- def test_boot_finished_written(
- self,
- instance_dir_exists,
- file_is_written,
- expected_log_substring,
- caplog,
- tmpdir,
- ):
- instance_dir = tmpdir.join("var/lib/cloud/instance")
- if instance_dir_exists:
- instance_dir.ensure_dir()
- boot_finished = instance_dir.join("boot-finished")
-
- m_cloud = mock.Mock(
- paths=mock.Mock(boot_finished=boot_finished.strpath)
- )
-
- handle(None, {}, m_cloud, logging.getLogger(), [])
-
- # We should not change the status of the instance directory
- assert instance_dir_exists == instance_dir.exists()
- assert file_is_written == boot_finished.exists()
-
- if expected_log_substring:
- assert expected_log_substring in caplog.text
diff --git a/cloudinit/config/tests/test_grub_dpkg.py b/cloudinit/config/tests/test_grub_dpkg.py
deleted file mode 100644
index 99c05bb5..00000000
--- a/cloudinit/config/tests/test_grub_dpkg.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import pytest
-
-from unittest import mock
-from logging import Logger
-from cloudinit.subp import ProcessExecutionError
-from cloudinit.config.cc_grub_dpkg import fetch_idevs, handle
-
-
-class TestFetchIdevs:
- """Tests cc_grub_dpkg.fetch_idevs()"""
-
- # Note: udevadm info returns devices in a large single line string
- @pytest.mark.parametrize(
- "grub_output,path_exists,expected_log_call,udevadm_output"
- ",expected_idevs",
- [
- # Inside a container, grub not installed
- (
- ProcessExecutionError(reason=FileNotFoundError()),
- False,
- mock.call("'grub-probe' not found in $PATH"),
- '',
- '',
- ),
- # Inside a container, grub installed
- (
- ProcessExecutionError(stderr="failed to get canonical path"),
- False,
- mock.call("grub-probe 'failed to get canonical path'"),
- '',
- '',
- ),
- # KVM Instance
- (
- ['/dev/vda'],
- True,
- None,
- (
- '/dev/disk/by-path/pci-0000:00:00.0 ',
- '/dev/disk/by-path/virtio-pci-0000:00:00.0 '
- ),
- '/dev/vda',
- ),
- # Xen Instance
- (
- ['/dev/xvda'],
- True,
- None,
- '',
- '/dev/xvda',
- ),
- # NVMe Hardware Instance
- (
- ['/dev/nvme1n1'],
- True,
- None,
- (
- '/dev/disk/by-id/nvme-Company_hash000 ',
- '/dev/disk/by-id/nvme-nvme.000-000-000-000-000 ',
- '/dev/disk/by-path/pci-0000:00:00.0-nvme-0 '
- ),
- '/dev/disk/by-id/nvme-Company_hash000',
- ),
- # SCSI Hardware Instance
- (
- ['/dev/sda'],
- True,
- None,
- (
- '/dev/disk/by-id/company-user-1 ',
- '/dev/disk/by-id/scsi-0Company_user-1 ',
- '/dev/disk/by-path/pci-0000:00:00.0-scsi-0:0:0:0 '
- ),
- '/dev/disk/by-id/company-user-1',
- ),
- ],
- )
- @mock.patch("cloudinit.config.cc_grub_dpkg.util.logexc")
- @mock.patch("cloudinit.config.cc_grub_dpkg.os.path.exists")
- @mock.patch("cloudinit.config.cc_grub_dpkg.subp.subp")
- def test_fetch_idevs(self, m_subp, m_exists, m_logexc, grub_output,
- path_exists, expected_log_call, udevadm_output,
- expected_idevs):
- """Tests outputs from grub-probe and udevadm info against grub-dpkg"""
- m_subp.side_effect = [
- grub_output,
- ["".join(udevadm_output)]
- ]
- m_exists.return_value = path_exists
- log = mock.Mock(spec=Logger)
- idevs = fetch_idevs(log)
- assert expected_idevs == idevs
- if expected_log_call is not None:
- assert expected_log_call in log.debug.call_args_list
-
-
-class TestHandle:
- """Tests cc_grub_dpkg.handle()"""
-
- @pytest.mark.parametrize(
- "cfg_idevs,cfg_idevs_empty,fetch_idevs_output,expected_log_output",
- [
- (
- # No configuration
- None,
- None,
- '/dev/disk/by-id/nvme-Company_hash000',
- (
- "Setting grub debconf-set-selections with ",
- "'/dev/disk/by-id/nvme-Company_hash000','false'"
- ),
- ),
- (
- # idevs set, idevs_empty unset
- '/dev/sda',
- None,
- '/dev/sda',
- (
- "Setting grub debconf-set-selections with ",
- "'/dev/sda','false'"
- ),
- ),
- (
- # idevs unset, idevs_empty set
- None,
- 'true',
- '/dev/xvda',
- (
- "Setting grub debconf-set-selections with ",
- "'/dev/xvda','true'"
- ),
- ),
- (
- # idevs set, idevs_empty set
- '/dev/vda',
- 'false',
- '/dev/disk/by-id/company-user-1',
- (
- "Setting grub debconf-set-selections with ",
- "'/dev/vda','false'"
- ),
- ),
- (
- # idevs set, idevs_empty set
- # Respect what the user defines, even if its logically wrong
- '/dev/nvme0n1',
- 'true',
- '',
- (
- "Setting grub debconf-set-selections with ",
- "'/dev/nvme0n1','true'"
- ),
- )
- ],
- )
- @mock.patch("cloudinit.config.cc_grub_dpkg.fetch_idevs")
- @mock.patch("cloudinit.config.cc_grub_dpkg.util.get_cfg_option_str")
- @mock.patch("cloudinit.config.cc_grub_dpkg.util.logexc")
- @mock.patch("cloudinit.config.cc_grub_dpkg.subp.subp")
- def test_handle(self, m_subp, m_logexc, m_get_cfg_str, m_fetch_idevs,
- cfg_idevs, cfg_idevs_empty, fetch_idevs_output,
- expected_log_output):
- """Test setting of correct debconf database entries"""
- m_get_cfg_str.side_effect = [
- cfg_idevs,
- cfg_idevs_empty
- ]
- m_fetch_idevs.return_value = fetch_idevs_output
- log = mock.Mock(spec=Logger)
- handle(mock.Mock(), mock.Mock(), mock.Mock(), log, mock.Mock())
- log.debug.assert_called_with("".join(expected_log_output))
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_keys_to_console.py b/cloudinit/config/tests/test_keys_to_console.py
deleted file mode 100644
index 4083fc54..00000000
--- a/cloudinit/config/tests/test_keys_to_console.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Tests for cc_keys_to_console."""
-from unittest import mock
-
-import pytest
-
-from cloudinit.config import cc_keys_to_console
-
-
-class TestHandle:
- """Tests for cloudinit.config.cc_keys_to_console.handle.
-
- TODO: These tests only cover the emit_keys_to_console config option, they
- should be expanded to cover the full functionality.
- """
-
- @mock.patch("cloudinit.config.cc_keys_to_console.util.multi_log")
- @mock.patch("cloudinit.config.cc_keys_to_console.os.path.exists")
- @mock.patch("cloudinit.config.cc_keys_to_console.subp.subp")
- @pytest.mark.parametrize("cfg,subp_called", [
- ({}, True), # Default to emitting keys
- ({"ssh": {}}, True), # Default even if we have the parent key
- ({"ssh": {"emit_keys_to_console": True}}, True), # Explicitly enabled
- ({"ssh": {"emit_keys_to_console": False}}, False), # Disabled
- ])
- def test_emit_keys_to_console_config(
- self, m_subp, m_path_exists, _m_multi_log, cfg, subp_called
- ):
- # Ensure we always find the helper
- m_path_exists.return_value = True
- m_subp.return_value = ("", "")
-
- cc_keys_to_console.handle("name", cfg, mock.Mock(), mock.Mock(), ())
-
- assert subp_called == (m_subp.call_count == 1)
diff --git a/cloudinit/config/tests/test_mounts.py b/cloudinit/config/tests/test_mounts.py
deleted file mode 100644
index 56510fd6..00000000
--- a/cloudinit/config/tests/test_mounts.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-from unittest import mock
-
-import pytest
-
-from cloudinit.config.cc_mounts import create_swapfile
-from cloudinit.subp import ProcessExecutionError
-
-
-M_PATH = 'cloudinit.config.cc_mounts.'
-
-
-class TestCreateSwapfile:
-
- @pytest.mark.parametrize('fstype', ('xfs', 'btrfs', 'ext4', 'other'))
- @mock.patch(M_PATH + 'util.get_mount_info')
- @mock.patch(M_PATH + 'subp.subp')
- def test_happy_path(self, m_subp, m_get_mount_info, fstype, tmpdir):
- swap_file = tmpdir.join("swap-file")
- fname = str(swap_file)
-
- # Some of the calls to subp.subp should create the swap file; this
- # roughly approximates that
- m_subp.side_effect = lambda *args, **kwargs: swap_file.write('')
-
- m_get_mount_info.return_value = (mock.ANY, fstype)
-
- create_swapfile(fname, '')
- assert mock.call(['mkswap', fname]) in m_subp.call_args_list
-
- @mock.patch(M_PATH + "util.get_mount_info")
- @mock.patch(M_PATH + "subp.subp")
- def test_fallback_from_fallocate_to_dd(
- self, m_subp, m_get_mount_info, caplog, tmpdir
- ):
- swap_file = tmpdir.join("swap-file")
- fname = str(swap_file)
-
- def subp_side_effect(cmd, *args, **kwargs):
- # Mock fallocate failing, to initiate fallback
- if cmd[0] == "fallocate":
- raise ProcessExecutionError()
-
- m_subp.side_effect = subp_side_effect
- # Use ext4 so both fallocate and dd are valid swap creation methods
- m_get_mount_info.return_value = (mock.ANY, "ext4")
-
- create_swapfile(fname, "")
-
- cmds = [args[0][0] for args, _kwargs in m_subp.call_args_list]
- assert "fallocate" in cmds, "fallocate was not called"
- assert "dd" in cmds, "fallocate failure did not fallback to dd"
-
- assert cmds.index("dd") > cmds.index(
- "fallocate"
- ), "dd ran before fallocate"
-
- assert mock.call(["mkswap", fname]) in m_subp.call_args_list
-
- msg = "fallocate swap creation failed, will attempt with dd"
- assert msg in caplog.text
diff --git a/cloudinit/config/tests/test_resolv_conf.py b/cloudinit/config/tests/test_resolv_conf.py
deleted file mode 100644
index aff110e5..00000000
--- a/cloudinit/config/tests/test_resolv_conf.py
+++ /dev/null
@@ -1,92 +0,0 @@
-import pytest
-
-from unittest import mock
-from cloudinit.config.cc_resolv_conf import generate_resolv_conf
-from tests.unittests.util import TestingDistro
-
-EXPECTED_HEADER = """\
-# Your system has been configured with 'manage-resolv-conf' set to true.
-# As a result, cloud-init has written this file with configuration data
-# that it has been provided. Cloud-init, by default, will write this file
-# a single time (PER_ONCE).
-#\n\n"""
-
-
-class TestGenerateResolvConf:
-
- dist = TestingDistro()
- tmpl_fn = "templates/resolv.conf.tmpl"
-
- @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file")
- def test_dist_resolv_conf_fn(self, m_render_to_file):
- self.dist.resolve_conf_fn = "/tmp/resolv-test.conf"
- generate_resolv_conf(self.tmpl_fn,
- mock.MagicMock(),
- self.dist.resolve_conf_fn)
-
- assert [
- mock.call(mock.ANY, self.dist.resolve_conf_fn, mock.ANY)
- ] == m_render_to_file.call_args_list
-
- @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file")
- def test_target_fname_is_used_if_passed(self, m_render_to_file):
- path = "/use/this/path"
- generate_resolv_conf(self.tmpl_fn, mock.MagicMock(), path)
-
- assert [
- mock.call(mock.ANY, path, mock.ANY)
- ] == m_render_to_file.call_args_list
-
- # Patch in templater so we can assert on the actual generated content
- @mock.patch("cloudinit.templater.util.write_file")
- # Parameterise with the value to be passed to generate_resolv_conf as the
- # params parameter, and the expected line after the header as
- # expected_extra_line.
- @pytest.mark.parametrize(
- "params,expected_extra_line",
- [
- # No options
- ({}, None),
- # Just a true flag
- ({"options": {"foo": True}}, "options foo"),
- # Just a false flag
- ({"options": {"foo": False}}, None),
- # Just an option
- ({"options": {"foo": "some_value"}}, "options foo:some_value"),
- # A true flag and an option
- (
- {"options": {"foo": "some_value", "bar": True}},
- "options bar foo:some_value",
- ),
- # Two options
- (
- {"options": {"foo": "some_value", "bar": "other_value"}},
- "options bar:other_value foo:some_value",
- ),
- # Everything
- (
- {
- "options": {
- "foo": "some_value",
- "bar": "other_value",
- "baz": False,
- "spam": True,
- }
- },
- "options spam bar:other_value foo:some_value",
- ),
- ],
- )
- def test_flags_and_options(
- self, m_write_file, params, expected_extra_line
- ):
- target_fn = "/etc/resolv.conf"
- generate_resolv_conf(self.tmpl_fn, params, target_fn)
-
- expected_content = EXPECTED_HEADER
- if expected_extra_line is not None:
- # If we have any extra lines, expect a trailing newline
- expected_content += "\n".join([expected_extra_line, ""])
- assert [
- mock.call(mock.ANY, expected_content, mode=mock.ANY)
- ] == m_write_file.call_args_list
diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py
deleted file mode 100644
index 2a27f72f..00000000
--- a/cloudinit/config/tests/test_set_passwords.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from unittest import mock
-
-from cloudinit.config import cc_set_passwords as setpass
-from cloudinit.tests.helpers import CiTestCase
-from cloudinit import util
-
-MODPATH = "cloudinit.config.cc_set_passwords."
-
-
-class TestHandleSshPwauth(CiTestCase):
- """Test cc_set_passwords handling of ssh_pwauth in handle_ssh_pwauth."""
-
- with_logs = True
-
- @mock.patch("cloudinit.distros.subp.subp")
- def test_unknown_value_logs_warning(self, m_subp):
- cloud = self.tmp_cloud(distro='ubuntu')
- setpass.handle_ssh_pwauth("floo", cloud.distro)
- self.assertIn("Unrecognized value: ssh_pwauth=floo",
- self.logs.getvalue())
- m_subp.assert_not_called()
-
- @mock.patch(MODPATH + "update_ssh_config", return_value=True)
- @mock.patch("cloudinit.distros.subp.subp")
- def test_systemctl_as_service_cmd(self, m_subp, m_update_ssh_config):
- """If systemctl in service cmd: systemctl restart name."""
- cloud = self.tmp_cloud(distro='ubuntu')
- cloud.distro.init_cmd = ['systemctl']
- setpass.handle_ssh_pwauth(True, cloud.distro)
- m_subp.assert_called_with(
- ["systemctl", "restart", "ssh"], capture=True)
-
- @mock.patch(MODPATH + "update_ssh_config", return_value=False)
- @mock.patch("cloudinit.distros.subp.subp")
- def test_not_restarted_if_not_updated(self, m_subp, m_update_ssh_config):
- """If config is not updated, then no system restart should be done."""
- cloud = self.tmp_cloud(distro='ubuntu')
- setpass.handle_ssh_pwauth(True, cloud.distro)
- m_subp.assert_not_called()
- self.assertIn("No need to restart SSH", self.logs.getvalue())
-
- @mock.patch(MODPATH + "update_ssh_config", return_value=True)
- @mock.patch("cloudinit.distros.subp.subp")
- def test_unchanged_does_nothing(self, m_subp, m_update_ssh_config):
- """If 'unchanged', then no updates to config and no restart."""
- cloud = self.tmp_cloud(distro='ubuntu')
- setpass.handle_ssh_pwauth("unchanged", cloud.distro)
- m_update_ssh_config.assert_not_called()
- m_subp.assert_not_called()
-
- @mock.patch("cloudinit.distros.subp.subp")
- def test_valid_change_values(self, m_subp):
- """If value is a valid changen value, then update should be called."""
- cloud = self.tmp_cloud(distro='ubuntu')
- upname = MODPATH + "update_ssh_config"
- optname = "PasswordAuthentication"
- for value in util.FALSE_STRINGS + util.TRUE_STRINGS:
- optval = "yes" if value in util.TRUE_STRINGS else "no"
- with mock.patch(upname, return_value=False) as m_update:
- setpass.handle_ssh_pwauth(value, cloud.distro)
- m_update.assert_called_with({optname: optval})
- m_subp.assert_not_called()
-
-
-class TestSetPasswordsHandle(CiTestCase):
- """Test cc_set_passwords.handle"""
-
- with_logs = True
-
- def test_handle_on_empty_config(self, *args):
- """handle logs that no password has changed when config is empty."""
- cloud = self.tmp_cloud(distro='ubuntu')
- setpass.handle(
- 'IGNORED', cfg={}, cloud=cloud, log=self.logger, args=[])
- self.assertEqual(
- "DEBUG: Leaving SSH config 'PasswordAuthentication' unchanged. "
- 'ssh_pwauth=None\n',
- self.logs.getvalue())
-
- @mock.patch(MODPATH + "subp.subp")
- def test_handle_on_chpasswd_list_parses_common_hashes(self, m_subp):
- """handle parses command password hashes."""
- cloud = self.tmp_cloud(distro='ubuntu')
- valid_hashed_pwds = [
- 'root:$2y$10$8BQjxjVByHA/Ee.O1bCXtO8S7Y5WojbXWqnqYpUW.BrPx/'
- 'Dlew1Va',
- 'ubuntu:$6$5hOurLPO$naywm3Ce0UlmZg9gG2Fl9acWCVEoakMMC7dR52q'
- 'SDexZbrN9z8yHxhUM2b.sxpguSwOlbOQSW/HpXazGGx3oo1']
- cfg = {'chpasswd': {'list': valid_hashed_pwds}}
- with mock.patch(MODPATH + 'subp.subp') as m_subp:
- setpass.handle(
- 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[])
- self.assertIn(
- 'DEBUG: Handling input for chpasswd as list.',
- self.logs.getvalue())
- self.assertIn(
- "DEBUG: Setting hashed password for ['root', 'ubuntu']",
- self.logs.getvalue())
- self.assertEqual(
- [mock.call(['chpasswd', '-e'],
- '\n'.join(valid_hashed_pwds) + '\n')],
- m_subp.call_args_list)
-
- @mock.patch(MODPATH + "util.is_BSD")
- @mock.patch(MODPATH + "subp.subp")
- def test_bsd_calls_custom_pw_cmds_to_set_and_expire_passwords(
- self, m_subp, m_is_bsd):
- """BSD don't use chpasswd"""
- m_is_bsd.return_value = True
- cloud = self.tmp_cloud(distro='freebsd')
- valid_pwds = ['ubuntu:passw0rd']
- cfg = {'chpasswd': {'list': valid_pwds}}
- setpass.handle(
- 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[])
- self.assertEqual([
- mock.call(['pw', 'usermod', 'ubuntu', '-h', '0'], data='passw0rd',
- logstring="chpasswd for ubuntu"),
- mock.call(['pw', 'usermod', 'ubuntu', '-p', '01-Jan-1970'])],
- m_subp.call_args_list)
-
- @mock.patch(MODPATH + "util.multi_log")
- @mock.patch(MODPATH + "subp.subp")
- def test_handle_on_chpasswd_list_creates_random_passwords(
- self, m_subp, m_multi_log
- ):
- """handle parses command set random passwords."""
- cloud = self.tmp_cloud(distro='ubuntu')
- valid_random_pwds = [
- 'root:R',
- 'ubuntu:RANDOM']
- cfg = {'chpasswd': {'expire': 'false', 'list': valid_random_pwds}}
- with mock.patch(MODPATH + 'subp.subp') as m_subp:
- setpass.handle(
- 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[])
- self.assertIn(
- 'DEBUG: Handling input for chpasswd as list.',
- self.logs.getvalue())
-
- self.assertEqual(1, m_subp.call_count)
- args, _kwargs = m_subp.call_args
- self.assertEqual(["chpasswd"], args[0])
-
- stdin = args[1]
- user_pass = {
- user: password
- for user, password
- in (line.split(":") for line in stdin.splitlines())
- }
-
- self.assertEqual(1, m_multi_log.call_count)
- self.assertEqual(
- mock.call(mock.ANY, stderr=False, fallback_to_stdout=False),
- m_multi_log.call_args
- )
-
- self.assertEqual(set(["root", "ubuntu"]), set(user_pass.keys()))
- written_lines = m_multi_log.call_args[0][0].splitlines()
- for password in user_pass.values():
- for line in written_lines:
- if password in line:
- break
- else:
- self.fail("Password not emitted to console")
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_snap.py b/cloudinit/config/tests/test_snap.py
deleted file mode 100644
index 6d4c014a..00000000
--- a/cloudinit/config/tests/test_snap.py
+++ /dev/null
@@ -1,564 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import re
-from io import StringIO
-
-from cloudinit.config.cc_snap import (
- ASSERTIONS_FILE, add_assertions, handle, maybe_install_squashfuse,
- run_commands, schema)
-from cloudinit.config.schema import validate_cloudconfig_schema
-from cloudinit import util
-from cloudinit.tests.helpers import (
- CiTestCase, SchemaTestCaseMixin, mock, wrap_and_call, skipUnlessJsonSchema)
-
-
-SYSTEM_USER_ASSERTION = """\
-type: system-user
-authority-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
-brand-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
-email: foo@bar.com
-password: $6$E5YiAuMIPAwX58jG$miomhVNui/vf7f/3ctB/f0RWSKFxG0YXzrJ9rtJ1ikvzt
-series:
-- 16
-since: 2016-09-10T16:34:00+03:00
-until: 2017-11-10T16:34:00+03:00
-username: baz
-sign-key-sha3-384: RuVvnp4n52GilycjfbbTCI3_L8Y6QlIE75wxMc0KzGV3AUQqVd9GuXoj
-
-AcLBXAQAAQoABgUCV/UU1wAKCRBKnlMoJQLkZVeLD/9/+hIeVywtzsDA3oxl+P+u9D13y9s6svP
-Jd6Wnf4FTw6sq1GjBE4ZA7lrwSaRCUJ9Vcsvf2q9OGPY7mOb2TBxaDe0PbUMjrSrqllSSQwhpNI
-zG+NxkkKuxsUmLzFa+k9m6cyojNbw5LFhQZBQCGlr3JYqC0tIREq/UsZxj+90TUC87lDJwkU8GF
-s4CR+rejZj4itIcDcVxCSnJH6hv6j2JrJskJmvObqTnoOlcab+JXdamXqbldSP3UIhWoyVjqzkj
-+to7mXgx+cCUA9+ngNCcfUG+1huGGTWXPCYkZ78HvErcRlIdeo4d3xwtz1cl/w3vYnq9og1XwsP
-Yfetr3boig2qs1Y+j/LpsfYBYncgWjeDfAB9ZZaqQz/oc8n87tIPZDJHrusTlBfop8CqcM4xsKS
-d+wnEY8e/F24mdSOYmS1vQCIDiRU3MKb6x138Ud6oHXFlRBbBJqMMctPqWDunWzb5QJ7YR0I39q
-BrnEqv5NE0G7w6HOJ1LSPG5Hae3P4T2ea+ATgkb03RPr3KnXnzXg4TtBbW1nytdlgoNc/BafE1H
-f3NThcq9gwX4xWZ2PAWnqVPYdDMyCtzW3Ck+o6sIzx+dh4gDLPHIi/6TPe/pUuMop9CBpWwez7V
-v1z+1+URx6Xlq3Jq18y5pZ6fY3IDJ6km2nQPMzcm4Q=="""
-
-ACCOUNT_ASSERTION = """\
-type: account-key
-authority-id: canonical
-revision: 2
-public-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0
-account-id: canonical
-name: store
-since: 2016-04-01T00:00:00.0Z
-body-length: 717
-sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswH
-
-AcbBTQRWhcGAARAA0KKYYQWuHOrsFVi4p4l7ZzSvX7kLgJFFeFgOkzdWKBTHEnsMKjl5mefFe9j
-qe8NlmJdfY7BenP7XeBtwKp700H/t9lLrZbpTNAPHXYxEWFJp5bPqIcJYBZ+29oLVLN1Tc5X482
-vCiDqL8+pPYqBrK2fNlyPlNNSum9wI70rDDL4r6FVvr+osTnGejibdV8JphWX+lrSQDnRSdM8KJ
-UM43vTgLGTi9W54oRhsA2OFexRfRksTrnqGoonCjqX5wO3OFSaMDzMsO2MJ/hPfLgDqw53qjzuK
-Iec9OL3k5basvu2cj5u9tKwVFDsCKK2GbKUsWWpx2KTpOifmhmiAbzkTHbH9KaoMS7p0kJwhTQG
-o9aJ9VMTWHJc/NCBx7eu451u6d46sBPCXS/OMUh2766fQmoRtO1OwCTxsRKG2kkjbMn54UdFULl
-VfzvyghMNRKIezsEkmM8wueTqGUGZWa6CEZqZKwhe/PROxOPYzqtDH18XZknbU1n5lNb7vNfem9
-2ai+3+JyFnW9UhfvpVF7gzAgdyCqNli4C6BIN43uwoS8HkykocZS/+Gv52aUQ/NZ8BKOHLw+7an
-Q0o8W9ltSLZbEMxFIPSN0stiZlkXAp6DLyvh1Y4wXSynDjUondTpej2fSvSlCz/W5v5V7qA4nIc
-vUvV7RjVzv17ut0AEQEAAQ==
-
-AcLDXAQAAQoABgUCV83k9QAKCRDUpVvql9g3IBT8IACKZ7XpiBZ3W4lqbPssY6On81WmxQLtvsM
-WTp6zZpl/wWOSt2vMNUk9pvcmrNq1jG9CuhDfWFLGXEjcrrmVkN3YuCOajMSPFCGrxsIBLSRt/b
-nrKykdLAAzMfG8rP1d82bjFFiIieE+urQ0Kcv09Jtdvavq3JT1Tek5mFyyfhHNlQEKOzWqmRWiL
-3c3VOZUs1ZD8TSlnuq/x+5T0X0YtOyGjSlVxk7UybbyMNd6MZfNaMpIG4x+mxD3KHFtBAC7O6kL
-eX3i6j5nCY5UABfA3DZEAkWP4zlmdBEOvZ9t293NaDdOpzsUHRkoi0Zez/9BHQ/kwx/uNc2WqrY
-inCmu16JGNeXqsyinnLl7Ghn2RwhvDMlLxF6RTx8xdx1yk6p3PBTwhZMUvuZGjUtN/AG8BmVJQ1
-rsGSRkkSywvnhVJRB2sudnrMBmNS2goJbzSbmJnOlBrd2WsV0T9SgNMWZBiov3LvU4o2SmAb6b+
-rYwh8H5QHcuuYJuxDjFhPswIp6Wes5T6hUicf3SWtObcDS4HSkVS4ImBjjX9YgCuFy7QdnooOWE
-aPvkRw3XCVeYq0K6w9GRsk1YFErD4XmXXZjDYY650MX9v42Sz5MmphHV8jdIY5ssbadwFSe2rCQ
-6UX08zy7RsIb19hTndE6ncvSNDChUR9eEnCm73eYaWTWTnq1cxdVP/s52r8uss++OYOkPWqh5nO
-haRn7INjH/yZX4qXjNXlTjo0PnHH0q08vNKDwLhxS+D9du+70FeacXFyLIbcWllSbJ7DmbumGpF
-yYbtj3FDDPzachFQdIG3lSt+cSUGeyfSs6wVtc3cIPka/2Urx7RprfmoWSI6+a5NcLdj0u2z8O9
-HxeIgxDpg/3gT8ZIuFKePMcLDM19Fh/p0ysCsX+84B9chNWtsMSmIaE57V+959MVtsLu7SLb9gi
-skrju0pQCwsu2wHMLTNd1f3PTHmrr49hxetTus07HSQUApMtAGKzQilF5zqFjbyaTd4xgQbd+PK
-CjFyzQTDOcUhXpuUGt/IzlqiFfsCsmbj2K4KdSNYMlqIgZ3Azu8KvZLIhsyN7v5vNIZSPfEbjde
-ClU9r0VRiJmtYBUjcSghD9LWn+yRLwOxhfQVjm0cBwIt5R/yPF/qC76yIVuWUtM5Y2/zJR1J8OF
-qWchvlImHtvDzS9FQeLyzJAOjvZ2CnWp2gILgUz0WQdOk1Dq8ax7KS9BQ42zxw9EZAEPw3PEFqR
-IQsRTONp+iVS8YxSmoYZjDlCgRMWUmawez/Fv5b9Fb/XkO5Eq4e+KfrpUujXItaipb+tV8h5v3t
-oG3Ie3WOHrVjCLXIdYslpL1O4nadqR6Xv58pHj6k"""
-
-
-class FakeCloud(object):
- def __init__(self, distro):
- self.distro = distro
-
-
-class TestAddAssertions(CiTestCase):
-
- with_logs = True
-
- def setUp(self):
- super(TestAddAssertions, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('cloudinit.config.cc_snap.subp.subp')
- def test_add_assertions_on_empty_list(self, m_subp):
- """When provided with an empty list, add_assertions does nothing."""
- add_assertions([])
- self.assertEqual('', self.logs.getvalue())
- m_subp.assert_not_called()
-
- def test_add_assertions_on_non_list_or_dict(self):
- """When provided an invalid type, add_assertions raises an error."""
- with self.assertRaises(TypeError) as context_manager:
- add_assertions(assertions="I'm Not Valid")
- self.assertEqual(
- "assertion parameter was not a list or dict: I'm Not Valid",
- str(context_manager.exception))
-
- @mock.patch('cloudinit.config.cc_snap.subp.subp')
- def test_add_assertions_adds_assertions_as_list(self, m_subp):
- """When provided with a list, add_assertions adds all assertions."""
- self.assertEqual(
- ASSERTIONS_FILE, '/var/lib/cloud/instance/snapd.assertions')
- assert_file = self.tmp_path('snapd.assertions', dir=self.tmp)
- assertions = [SYSTEM_USER_ASSERTION, ACCOUNT_ASSERTION]
- wrap_and_call(
- 'cloudinit.config.cc_snap',
- {'ASSERTIONS_FILE': {'new': assert_file}},
- add_assertions, assertions)
- self.assertIn(
- 'Importing user-provided snap assertions', self.logs.getvalue())
- self.assertIn(
- 'sertions', self.logs.getvalue())
- self.assertEqual(
- [mock.call(['snap', 'ack', assert_file], capture=True)],
- m_subp.call_args_list)
- compare_file = self.tmp_path('comparison', dir=self.tmp)
- util.write_file(compare_file, '\n'.join(assertions).encode('utf-8'))
- self.assertEqual(
- util.load_file(compare_file), util.load_file(assert_file))
-
- @mock.patch('cloudinit.config.cc_snap.subp.subp')
- def test_add_assertions_adds_assertions_as_dict(self, m_subp):
- """When provided with a dict, add_assertions adds all assertions."""
- self.assertEqual(
- ASSERTIONS_FILE, '/var/lib/cloud/instance/snapd.assertions')
- assert_file = self.tmp_path('snapd.assertions', dir=self.tmp)
- assertions = {'00': SYSTEM_USER_ASSERTION, '01': ACCOUNT_ASSERTION}
- wrap_and_call(
- 'cloudinit.config.cc_snap',
- {'ASSERTIONS_FILE': {'new': assert_file}},
- add_assertions, assertions)
- self.assertIn(
- 'Importing user-provided snap assertions', self.logs.getvalue())
- self.assertIn(
- "DEBUG: Snap acking: ['type: system-user', 'authority-id: Lqv",
- self.logs.getvalue())
- self.assertIn(
- "DEBUG: Snap acking: ['type: account-key', 'authority-id: canonic",
- self.logs.getvalue())
- self.assertEqual(
- [mock.call(['snap', 'ack', assert_file], capture=True)],
- m_subp.call_args_list)
- compare_file = self.tmp_path('comparison', dir=self.tmp)
- combined = '\n'.join(assertions.values())
- util.write_file(compare_file, combined.encode('utf-8'))
- self.assertEqual(
- util.load_file(compare_file), util.load_file(assert_file))
-
-
-class TestRunCommands(CiTestCase):
-
- with_logs = True
- allowed_subp = [CiTestCase.SUBP_SHELL_TRUE]
-
- def setUp(self):
- super(TestRunCommands, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('cloudinit.config.cc_snap.subp.subp')
- 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(
- "commands parameter was not a list or dict: I'm Not Valid",
- str(context_manager.exception))
-
- 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 = 'cloudinit.config.cc_snap.sys.stderr'
- 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]
- mock_path = 'cloudinit.config.cc_snap.sys.stderr'
- with mock.patch(mock_path, new_callable=StringIO):
- run_commands(commands=commands)
-
- self.assertIn(
- 'DEBUG: Running user-provided snap commands',
- self.logs.getvalue())
- self.assertEqual('HI\nMOM\n', util.load_file(outfile))
- self.assertIn(
- 'WARNING: Non-snap commands in snap config:', 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}
- mock_path = 'cloudinit.config.cc_snap.sys.stderr'
- with mock.patch(mock_path, new_callable=StringIO):
- run_commands(commands=commands)
-
- expected_messages = [
- 'DEBUG: Running user-provided snap commands']
- for message in expected_messages:
- self.assertIn(message, self.logs.getvalue())
- self.assertEqual('MOM\nHI\n', util.load_file(outfile))
-
-
-@skipUnlessJsonSchema()
-class TestSchema(CiTestCase, SchemaTestCaseMixin):
-
- with_logs = True
- schema = schema
-
- def test_schema_warns_on_snap_not_as_dict(self):
- """If the snap configuration is not a dict, emit a warning."""
- validate_cloudconfig_schema({'snap': 'wrong type'}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nsnap: 'wrong type' is not of type"
- " 'object'\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_schema_disallows_unknown_keys(self, _):
- """Unknown keys in the snap configuration emit warnings."""
- validate_cloudconfig_schema(
- {'snap': {'commands': ['ls'], 'invalid-key': ''}}, schema)
- self.assertIn(
- 'WARNING: Invalid config:\nsnap: Additional properties are not'
- " allowed ('invalid-key' was unexpected)",
- self.logs.getvalue())
-
- def test_warn_schema_requires_either_commands_or_assertions(self):
- """Warn when snap configuration lacks both commands and assertions."""
- validate_cloudconfig_schema(
- {'snap': {}}, schema)
- self.assertIn(
- 'WARNING: Invalid config:\nsnap: {} does not have enough'
- ' properties',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_warn_schema_commands_is_not_list_or_dict(self, _):
- """Warn when snap:commands config is not a list or dict."""
- validate_cloudconfig_schema(
- {'snap': {'commands': 'broken'}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nsnap.commands: 'broken' is not of type"
- " 'object', 'array'\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_warn_schema_when_commands_is_empty(self, _):
- """Emit warnings when snap:commands is an empty list or dict."""
- validate_cloudconfig_schema(
- {'snap': {'commands': []}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'commands': {}}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nsnap.commands: [] is too short\n"
- "WARNING: Invalid config:\nsnap.commands: {} does not have enough"
- " properties\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_schema_when_commands_are_list_or_dict(self, _):
- """No warnings when snap:commands are either a list or dict."""
- validate_cloudconfig_schema(
- {'snap': {'commands': ['valid']}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'commands': {'01': 'also valid'}}}, schema)
- self.assertEqual('', self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_schema_when_commands_values_are_invalid_type(self, _):
- """Warnings when snap:commands values are invalid type (e.g. int)"""
- validate_cloudconfig_schema(
- {'snap': {'commands': [123]}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'commands': {'01': 123}}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\n"
- "snap.commands.0: 123 is not valid under any of the given"
- " schemas\n"
- "WARNING: Invalid config:\n"
- "snap.commands.01: 123 is not valid under any of the given"
- " schemas\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_schema_when_commands_list_values_are_invalid_type(self, _):
- """Warnings when snap:commands list values are wrong type (e.g. int)"""
- validate_cloudconfig_schema(
- {'snap': {'commands': [["snap", "install", 123]]}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'commands': {'01': ["snap", "install", 123]}}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\n"
- "snap.commands.0: ['snap', 'install', 123] is not valid under any"
- " of the given schemas\n",
- "WARNING: Invalid config:\n"
- "snap.commands.0: ['snap', 'install', 123] is not valid under any"
- " of the given schemas\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- def test_schema_when_assertions_values_are_invalid_type(self, _):
- """Warnings when snap:assertions values are invalid type (e.g. int)"""
- validate_cloudconfig_schema(
- {'snap': {'assertions': [123]}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'assertions': {'01': 123}}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\n"
- "snap.assertions.0: 123 is not of type 'string'\n"
- "WARNING: Invalid config:\n"
- "snap.assertions.01: 123 is not of type 'string'\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.add_assertions')
- def test_warn_schema_assertions_is_not_list_or_dict(self, _):
- """Warn when snap:assertions config is not a list or dict."""
- validate_cloudconfig_schema(
- {'snap': {'assertions': 'broken'}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nsnap.assertions: 'broken' is not of"
- " type 'object', 'array'\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.add_assertions')
- def test_warn_schema_when_assertions_is_empty(self, _):
- """Emit warnings when snap:assertions is an empty list or dict."""
- validate_cloudconfig_schema(
- {'snap': {'assertions': []}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'assertions': {}}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nsnap.assertions: [] is too short\n"
- "WARNING: Invalid config:\nsnap.assertions: {} does not have"
- " enough properties\n",
- self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.add_assertions')
- def test_schema_when_assertions_are_list_or_dict(self, _):
- """No warnings when snap:assertions are a list or dict."""
- validate_cloudconfig_schema(
- {'snap': {'assertions': ['valid']}}, schema)
- validate_cloudconfig_schema(
- {'snap': {'assertions': {'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):
-
- with_logs = True
-
- def setUp(self):
- super(TestHandle, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- @mock.patch('cloudinit.config.cc_snap.add_assertions')
- @mock.patch('cloudinit.config.cc_snap.validate_cloudconfig_schema')
- def test_handle_no_config(self, m_schema, m_add, m_run):
- """When no snap-related configuration is provided, nothing happens."""
- cfg = {}
- handle('snap', cfg=cfg, cloud=None, log=self.logger, args=None)
- self.assertIn(
- "DEBUG: Skipping module named snap, no 'snap' key in config",
- self.logs.getvalue())
- m_schema.assert_not_called()
- m_add.assert_not_called()
- m_run.assert_not_called()
-
- @mock.patch('cloudinit.config.cc_snap.run_commands')
- @mock.patch('cloudinit.config.cc_snap.add_assertions')
- @mock.patch('cloudinit.config.cc_snap.maybe_install_squashfuse')
- def test_handle_skips_squashfuse_when_unconfigured(self, m_squash, m_add,
- m_run):
- """When squashfuse_in_container is unset, don't attempt to install."""
- handle(
- 'snap', cfg={'snap': {}}, cloud=None, log=self.logger, args=None)
- handle(
- 'snap', cfg={'snap': {'squashfuse_in_container': None}},
- cloud=None, log=self.logger, args=None)
- handle(
- 'snap', cfg={'snap': {'squashfuse_in_container': False}},
- cloud=None, log=self.logger, args=None)
- self.assertEqual([], m_squash.call_args_list) # No calls
- # snap configuration missing assertions and commands will default to []
- self.assertIn(mock.call([]), m_add.call_args_list)
- self.assertIn(mock.call([]), m_run.call_args_list)
-
- @mock.patch('cloudinit.config.cc_snap.maybe_install_squashfuse')
- def test_handle_tries_to_install_squashfuse(self, m_squash):
- """If squashfuse_in_container is True, try installing squashfuse."""
- cfg = {'snap': {'squashfuse_in_container': True}}
- mycloud = FakeCloud(None)
- handle('snap', cfg=cfg, cloud=mycloud, log=self.logger, args=None)
- self.assertEqual(
- [mock.call(mycloud)], m_squash.call_args_list)
-
- def test_handle_runs_commands_provided(self):
- """If commands are specified as a list, run them."""
- outfile = self.tmp_path('output.log', dir=self.tmp)
-
- cfg = {
- 'snap': {'commands': ['echo "HI" >> %s' % outfile,
- 'echo "MOM" >> %s' % outfile]}}
- mock_path = 'cloudinit.config.cc_snap.sys.stderr'
- with self.allow_subp([CiTestCase.SUBP_SHELL_TRUE]):
- with mock.patch(mock_path, new_callable=StringIO):
- handle('snap', cfg=cfg, cloud=None, log=self.logger, args=None)
-
- self.assertEqual('HI\nMOM\n', util.load_file(outfile))
-
- @mock.patch('cloudinit.config.cc_snap.subp.subp')
- def test_handle_adds_assertions(self, m_subp):
- """Any configured snap assertions are provided to add_assertions."""
- assert_file = self.tmp_path('snapd.assertions', dir=self.tmp)
- compare_file = self.tmp_path('comparison', dir=self.tmp)
- cfg = {
- 'snap': {'assertions': [SYSTEM_USER_ASSERTION, ACCOUNT_ASSERTION]}}
- wrap_and_call(
- 'cloudinit.config.cc_snap',
- {'ASSERTIONS_FILE': {'new': assert_file}},
- handle, 'snap', cfg=cfg, cloud=None, log=self.logger, args=None)
- content = '\n'.join(cfg['snap']['assertions'])
- util.write_file(compare_file, content.encode('utf-8'))
- self.assertEqual(
- util.load_file(compare_file), util.load_file(assert_file))
-
- @mock.patch('cloudinit.config.cc_snap.subp.subp')
- @skipUnlessJsonSchema()
- def test_handle_validates_schema(self, m_subp):
- """Any provided configuration is runs validate_cloudconfig_schema."""
- assert_file = self.tmp_path('snapd.assertions', dir=self.tmp)
- cfg = {'snap': {'invalid': ''}} # Generates schema warning
- wrap_and_call(
- 'cloudinit.config.cc_snap',
- {'ASSERTIONS_FILE': {'new': assert_file}},
- handle, 'snap', cfg=cfg, cloud=None, log=self.logger, args=None)
- self.assertEqual(
- "WARNING: Invalid config:\nsnap: Additional properties are not"
- " allowed ('invalid' was unexpected)\n",
- self.logs.getvalue())
-
-
-class TestMaybeInstallSquashFuse(CiTestCase):
-
- with_logs = True
-
- def setUp(self):
- super(TestMaybeInstallSquashFuse, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('cloudinit.config.cc_snap.util.is_container')
- def test_maybe_install_squashfuse_skips_non_containers(self, m_container):
- """maybe_install_squashfuse does nothing when not on a container."""
- m_container.return_value = False
- maybe_install_squashfuse(cloud=FakeCloud(None))
- self.assertEqual([mock.call()], m_container.call_args_list)
- self.assertEqual('', self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.util.is_container')
- def test_maybe_install_squashfuse_raises_install_errors(self, m_container):
- """maybe_install_squashfuse logs and raises package install errors."""
- m_container.return_value = True
- distro = mock.MagicMock()
- distro.update_package_sources.side_effect = RuntimeError(
- 'Some apt error')
- with self.assertRaises(RuntimeError) as context_manager:
- maybe_install_squashfuse(cloud=FakeCloud(distro))
- self.assertEqual('Some apt error', str(context_manager.exception))
- self.assertIn('Package update failed\nTraceback', self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.util.is_container')
- def test_maybe_install_squashfuse_raises_update_errors(self, m_container):
- """maybe_install_squashfuse logs and raises package update errors."""
- m_container.return_value = True
- distro = mock.MagicMock()
- distro.update_package_sources.side_effect = RuntimeError(
- 'Some apt error')
- with self.assertRaises(RuntimeError) as context_manager:
- maybe_install_squashfuse(cloud=FakeCloud(distro))
- self.assertEqual('Some apt error', str(context_manager.exception))
- self.assertIn('Package update failed\nTraceback', self.logs.getvalue())
-
- @mock.patch('cloudinit.config.cc_snap.util.is_container')
- def test_maybe_install_squashfuse_happy_path(self, m_container):
- """maybe_install_squashfuse logs and raises package install errors."""
- m_container.return_value = True
- distro = mock.MagicMock() # No errors raised
- maybe_install_squashfuse(cloud=FakeCloud(distro))
- self.assertEqual(
- [mock.call()], distro.update_package_sources.call_args_list)
- self.assertEqual(
- [mock.call(['squashfuse'])],
- distro.install_packages.call_args_list)
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
deleted file mode 100644
index 87ccdb60..00000000
--- a/cloudinit/config/tests/test_ssh.py
+++ /dev/null
@@ -1,405 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import os.path
-
-from cloudinit.config import cc_ssh
-from cloudinit import ssh_util
-from cloudinit.tests.helpers import CiTestCase, mock
-import logging
-
-LOG = logging.getLogger(__name__)
-
-MODPATH = "cloudinit.config.cc_ssh."
-KEY_NAMES_NO_DSA = [name for name in cc_ssh.GENERATE_KEY_NAMES
- if name not in 'dsa']
-
-
-@mock.patch(MODPATH + "ssh_util.setup_user_keys")
-class TestHandleSsh(CiTestCase):
- """Test cc_ssh handling of ssh config."""
-
- def _publish_hostkey_test_setup(self):
- self.test_hostkeys = {
- 'dsa': ('ssh-dss', 'AAAAB3NzaC1kc3MAAACB'),
- 'ecdsa': ('ecdsa-sha2-nistp256', 'AAAAE2VjZ'),
- 'ed25519': ('ssh-ed25519', 'AAAAC3NzaC1lZDI'),
- 'rsa': ('ssh-rsa', 'AAAAB3NzaC1yc2EAAA'),
- }
- self.test_hostkey_files = []
- hostkey_tmpdir = self.tmp_dir()
- for key_type in cc_ssh.GENERATE_KEY_NAMES:
- key_data = self.test_hostkeys[key_type]
- filename = 'ssh_host_%s_key.pub' % key_type
- filepath = os.path.join(hostkey_tmpdir, filename)
- self.test_hostkey_files.append(filepath)
- with open(filepath, 'w') as f:
- f.write(' '.join(key_data))
-
- cc_ssh.KEY_FILE_TPL = os.path.join(hostkey_tmpdir, 'ssh_host_%s_key')
-
- def test_apply_credentials_with_user(self, m_setup_keys):
- """Apply keys for the given user and root."""
- keys = ["key1"]
- user = "clouduser"
- cc_ssh.apply_credentials(keys, user, False, ssh_util.DISABLE_USER_OPTS)
- self.assertEqual([mock.call(set(keys), user),
- mock.call(set(keys), "root", options="")],
- m_setup_keys.call_args_list)
-
- def test_apply_credentials_with_no_user(self, m_setup_keys):
- """Apply keys for root only."""
- keys = ["key1"]
- user = None
- cc_ssh.apply_credentials(keys, user, False, ssh_util.DISABLE_USER_OPTS)
- self.assertEqual([mock.call(set(keys), "root", options="")],
- m_setup_keys.call_args_list)
-
- def test_apply_credentials_with_user_disable_root(self, m_setup_keys):
- """Apply keys for the given user and disable root ssh."""
- keys = ["key1"]
- user = "clouduser"
- options = ssh_util.DISABLE_USER_OPTS
- cc_ssh.apply_credentials(keys, user, True, options)
- options = options.replace("$USER", user)
- options = options.replace("$DISABLE_USER", "root")
- self.assertEqual([mock.call(set(keys), user),
- mock.call(set(keys), "root", options=options)],
- m_setup_keys.call_args_list)
-
- def test_apply_credentials_with_no_user_disable_root(self, m_setup_keys):
- """Apply keys no user and disable root ssh."""
- keys = ["key1"]
- user = None
- options = ssh_util.DISABLE_USER_OPTS
- cc_ssh.apply_credentials(keys, user, True, options)
- options = options.replace("$USER", "NONE")
- options = options.replace("$DISABLE_USER", "root")
- self.assertEqual([mock.call(set(keys), "root", options=options)],
- m_setup_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_no_cfg(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
- """Test handle with no config ignores generating existing keyfiles."""
- cfg = {}
- keys = ["key1"]
- m_glob.return_value = [] # Return no matching keys to prevent removal
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ([], {})
- cc_ssh.PUBLISH_HOST_KEYS = False
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cc_ssh.handle("name", cfg, cloud, LOG, None)
- options = ssh_util.DISABLE_USER_OPTS.replace("$USER", "NONE")
- options = options.replace("$DISABLE_USER", "root")
- m_glob.assert_called_once_with('/etc/ssh/ssh_host_*key*')
- self.assertIn(
- [mock.call('/etc/ssh/ssh_host_rsa_key'),
- mock.call('/etc/ssh/ssh_host_dsa_key'),
- mock.call('/etc/ssh/ssh_host_ecdsa_key'),
- mock.call('/etc/ssh/ssh_host_ed25519_key')],
- m_path_exists.call_args_list)
- self.assertEqual([mock.call(set(keys), "root", options=options)],
- m_setup_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_dont_allow_public_ssh_keys(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
- """Test allow_public_ssh_keys=False ignores ssh public keys from
- platform.
- """
- cfg = {"allow_public_ssh_keys": False}
- keys = ["key1"]
- user = "clouduser"
- m_glob.return_value = [] # Return no matching keys to prevent removal
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cc_ssh.handle("name", cfg, cloud, LOG, None)
-
- options = ssh_util.DISABLE_USER_OPTS.replace("$USER", user)
- options = options.replace("$DISABLE_USER", "root")
- self.assertEqual([mock.call(set(), user),
- mock.call(set(), "root", options=options)],
- m_setup_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_no_cfg_and_default_root(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
- """Test handle with no config and a default distro user."""
- cfg = {}
- keys = ["key1"]
- user = "clouduser"
- m_glob.return_value = [] # Return no matching keys to prevent removal
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cc_ssh.handle("name", cfg, cloud, LOG, None)
-
- options = ssh_util.DISABLE_USER_OPTS.replace("$USER", user)
- options = options.replace("$DISABLE_USER", "root")
- self.assertEqual([mock.call(set(keys), user),
- mock.call(set(keys), "root", options=options)],
- m_setup_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_cfg_with_explicit_disable_root(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
- """Test handle with explicit disable_root and a default distro user."""
- # This test is identical to test_handle_no_cfg_and_default_root,
- # except this uses an explicit cfg value
- cfg = {"disable_root": True}
- keys = ["key1"]
- user = "clouduser"
- m_glob.return_value = [] # Return no matching keys to prevent removal
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cc_ssh.handle("name", cfg, cloud, LOG, None)
-
- options = ssh_util.DISABLE_USER_OPTS.replace("$USER", user)
- options = options.replace("$DISABLE_USER", "root")
- self.assertEqual([mock.call(set(keys), user),
- mock.call(set(keys), "root", options=options)],
- m_setup_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_cfg_without_disable_root(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
- """Test handle with disable_root == False."""
- # When disable_root == False, the ssh redirect for root is skipped
- cfg = {"disable_root": False}
- keys = ["key1"]
- user = "clouduser"
- m_glob.return_value = [] # Return no matching keys to prevent removal
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cloud.get_public_ssh_keys = mock.Mock(return_value=keys)
- cc_ssh.handle("name", cfg, cloud, LOG, None)
-
- self.assertEqual([mock.call(set(keys), user),
- mock.call(set(keys), "root", options="")],
- m_setup_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_default(
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
- """Test handle with various configs for ssh_publish_hostkeys."""
- self._publish_hostkey_test_setup()
- cc_ssh.PUBLISH_HOST_KEYS = True
- keys = ["key1"]
- user = "clouduser"
- # Return no matching keys for first glob, test keys for second.
- m_glob.side_effect = iter([
- [],
- self.test_hostkey_files,
- ])
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cloud.datasource.publish_host_keys = mock.Mock()
-
- cfg = {}
- expected_call = [self.test_hostkeys[key_type] for key_type
- in KEY_NAMES_NO_DSA]
- cc_ssh.handle("name", cfg, cloud, LOG, None)
- self.assertEqual([mock.call(expected_call)],
- cloud.datasource.publish_host_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_config_enable(
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
- """Test handle with various configs for ssh_publish_hostkeys."""
- self._publish_hostkey_test_setup()
- cc_ssh.PUBLISH_HOST_KEYS = False
- keys = ["key1"]
- user = "clouduser"
- # Return no matching keys for first glob, test keys for second.
- m_glob.side_effect = iter([
- [],
- self.test_hostkey_files,
- ])
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cloud.datasource.publish_host_keys = mock.Mock()
-
- cfg = {'ssh_publish_hostkeys': {'enabled': True}}
- expected_call = [self.test_hostkeys[key_type] for key_type
- in KEY_NAMES_NO_DSA]
- cc_ssh.handle("name", cfg, cloud, LOG, None)
- self.assertEqual([mock.call(expected_call)],
- cloud.datasource.publish_host_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_config_disable(
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
- """Test handle with various configs for ssh_publish_hostkeys."""
- self._publish_hostkey_test_setup()
- cc_ssh.PUBLISH_HOST_KEYS = True
- keys = ["key1"]
- user = "clouduser"
- # Return no matching keys for first glob, test keys for second.
- m_glob.side_effect = iter([
- [],
- self.test_hostkey_files,
- ])
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cloud.datasource.publish_host_keys = mock.Mock()
-
- cfg = {'ssh_publish_hostkeys': {'enabled': False}}
- cc_ssh.handle("name", cfg, cloud, LOG, None)
- self.assertFalse(cloud.datasource.publish_host_keys.call_args_list)
- cloud.datasource.publish_host_keys.assert_not_called()
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_config_blacklist(
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
- """Test handle with various configs for ssh_publish_hostkeys."""
- self._publish_hostkey_test_setup()
- cc_ssh.PUBLISH_HOST_KEYS = True
- keys = ["key1"]
- user = "clouduser"
- # Return no matching keys for first glob, test keys for second.
- m_glob.side_effect = iter([
- [],
- self.test_hostkey_files,
- ])
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cloud.datasource.publish_host_keys = mock.Mock()
-
- cfg = {'ssh_publish_hostkeys': {'enabled': True,
- 'blacklist': ['dsa', 'rsa']}}
- expected_call = [self.test_hostkeys[key_type] for key_type
- in ['ecdsa', 'ed25519']]
- cc_ssh.handle("name", cfg, cloud, LOG, None)
- self.assertEqual([mock.call(expected_call)],
- cloud.datasource.publish_host_keys.call_args_list)
-
- @mock.patch(MODPATH + "glob.glob")
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_empty_blacklist(
- self, m_path_exists, m_nug, m_glob, m_setup_keys):
- """Test handle with various configs for ssh_publish_hostkeys."""
- self._publish_hostkey_test_setup()
- cc_ssh.PUBLISH_HOST_KEYS = True
- keys = ["key1"]
- user = "clouduser"
- # Return no matching keys for first glob, test keys for second.
- m_glob.side_effect = iter([
- [],
- self.test_hostkey_files,
- ])
- # Mock os.path.exits to True to short-circuit the key writing logic
- m_path_exists.return_value = True
- m_nug.return_value = ({user: {"default": user}}, {})
- cloud = self.tmp_cloud(
- distro='ubuntu', metadata={'public-keys': keys})
- cloud.datasource.publish_host_keys = mock.Mock()
-
- cfg = {'ssh_publish_hostkeys': {'enabled': True,
- 'blacklist': []}}
- expected_call = [self.test_hostkeys[key_type] for key_type
- in cc_ssh.GENERATE_KEY_NAMES]
- cc_ssh.handle("name", cfg, cloud, LOG, None)
- self.assertEqual([mock.call(expected_call)],
- cloud.datasource.publish_host_keys.call_args_list)
-
- @mock.patch(MODPATH + "ug_util.normalize_users_groups")
- @mock.patch(MODPATH + "util.write_file")
- def test_handle_ssh_keys_in_cfg(self, m_write_file, m_nug, m_setup_keys):
- """Test handle with ssh keys and certificate."""
- # Populate a config dictionary to pass to handle() as well
- # as the expected file-writing calls.
- cfg = {"ssh_keys": {}}
-
- expected_calls = []
- for key_type in cc_ssh.GENERATE_KEY_NAMES:
- private_name = "{}_private".format(key_type)
- public_name = "{}_public".format(key_type)
- cert_name = "{}_certificate".format(key_type)
-
- # Actual key contents don"t have to be realistic
- private_value = "{}_PRIVATE_KEY".format(key_type)
- public_value = "{}_PUBLIC_KEY".format(key_type)
- cert_value = "{}_CERT_KEY".format(key_type)
-
- cfg["ssh_keys"][private_name] = private_value
- cfg["ssh_keys"][public_name] = public_value
- cfg["ssh_keys"][cert_name] = cert_value
-
- expected_calls.extend([
- mock.call(
- '/etc/ssh/ssh_host_{}_key'.format(key_type),
- private_value,
- 384
- ),
- mock.call(
- '/etc/ssh/ssh_host_{}_key.pub'.format(key_type),
- public_value,
- 384
- ),
- mock.call(
- '/etc/ssh/ssh_host_{}_key-cert.pub'.format(key_type),
- cert_value,
- 384
- ),
- mock.call(
- '/etc/ssh/sshd_config',
- ('HostCertificate /etc/ssh/ssh_host_{}_key-cert.pub'
- '\n'.format(key_type)),
- preserve_mode=True
- )
- ])
-
- # Run the handler.
- m_nug.return_value = ([], {})
- with mock.patch(MODPATH + 'ssh_util.parse_ssh_config',
- return_value=[]):
- cc_ssh.handle("name", cfg, self.tmp_cloud(distro='ubuntu'),
- LOG, None)
-
- # Check that all expected output has been done.
- for call_ in expected_calls:
- self.assertIn(call_, m_write_file.call_args_list)
diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py
deleted file mode 100644
index db7fb726..00000000
--- a/cloudinit/config/tests/test_ubuntu_advantage.py
+++ /dev/null
@@ -1,333 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from cloudinit.config.cc_ubuntu_advantage import (
- configure_ua, handle, maybe_install_ua_tools, schema)
-from cloudinit.config.schema import validate_cloudconfig_schema
-from cloudinit import subp
-from cloudinit.tests.helpers import (
- CiTestCase, mock, SchemaTestCaseMixin, skipUnlessJsonSchema)
-
-
-# Module path used in mocks
-MPATH = 'cloudinit.config.cc_ubuntu_advantage'
-
-
-class FakeCloud(object):
- def __init__(self, distro):
- self.distro = distro
-
-
-class TestConfigureUA(CiTestCase):
-
- with_logs = True
- allowed_subp = [CiTestCase.SUBP_SHELL_TRUE]
-
- def setUp(self):
- super(TestConfigureUA, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('%s.subp.subp' % MPATH)
- def test_configure_ua_attach_error(self, m_subp):
- """Errors from ua attach command are raised."""
- m_subp.side_effect = subp.ProcessExecutionError(
- 'Invalid token SomeToken')
- with self.assertRaises(RuntimeError) as context_manager:
- configure_ua(token='SomeToken')
- self.assertEqual(
- 'Failure attaching Ubuntu Advantage:\nUnexpected error while'
- ' running command.\nCommand: -\nExit code: -\nReason: -\n'
- 'Stdout: Invalid token SomeToken\nStderr: -',
- str(context_manager.exception))
-
- @mock.patch('%s.subp.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.subp.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 subp.ProcessExecutionError(
- 'Invalid {} credentials'.format(svc.upper()))
-
- 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',
- self.logs.getvalue())
- 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.subp.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.subp.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.subp.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',
- self.logs.getvalue())
-
- @mock.patch('%s.subp.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())
-
-
-@skipUnlessJsonSchema()
-class TestSchema(CiTestCase, SchemaTestCaseMixin):
-
- 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)
- self.assertEqual(
- "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."""
- validate_cloudconfig_schema(
- {'ubuntu_advantage': {'token': 'winner', 'invalid-key': ''}},
- schema)
- self.assertIn(
- '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."""
- validate_cloudconfig_schema(
- {'ubuntu_advantage': {'enable': ['esm']}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nubuntu_advantage:"
- " 'token' 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."""
- validate_cloudconfig_schema(
- {'ubuntu_advantage': {'enable': 'needslist'}}, schema)
- self.assertEqual(
- "WARNING: Invalid config:\nubuntu_advantage: 'token' is a"
- " required property\nubuntu_advantage.enable: 'needslist'"
- " is not of type 'array'\n",
- self.logs.getvalue())
-
-
-class TestHandle(CiTestCase):
-
- with_logs = True
-
- def setUp(self):
- super(TestHandle, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('%s.validate_cloudconfig_schema' % MPATH)
- def test_handle_no_config(self, m_schema):
- """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',
- self.logs.getvalue())
- m_schema.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):
- """If ubuntu_advantage is provided, try installing ua-tools package."""
- cfg = {'ubuntu_advantage': {'token': 'valid'}}
- 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))
-
- @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'])
-
-
-class TestMaybeInstallUATools(CiTestCase):
-
- with_logs = True
-
- def setUp(self):
- super(TestMaybeInstallUATools, self).setUp()
- self.tmp = self.tmp_dir()
-
- @mock.patch('%s.subp.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
- distro = mock.MagicMock()
- distro.update_package_sources.side_effect = RuntimeError(
- 'Some apt error')
- maybe_install_ua_tools(cloud=FakeCloud(distro)) # No RuntimeError
-
- @mock.patch('%s.subp.which' % MPATH)
- def test_maybe_install_ua_tools_raises_update_errors(self, m_which):
- """maybe_install_ua_tools logs and raises apt update errors."""
- m_which.return_value = None
- distro = mock.MagicMock()
- distro.update_package_sources.side_effect = RuntimeError(
- 'Some apt error')
- with self.assertRaises(RuntimeError) as context_manager:
- maybe_install_ua_tools(cloud=FakeCloud(distro))
- self.assertEqual('Some apt error', str(context_manager.exception))
- self.assertIn('Package update failed\nTraceback', self.logs.getvalue())
-
- @mock.patch('%s.subp.which' % MPATH)
- def test_maybe_install_ua_raises_install_errors(self, m_which):
- """maybe_install_ua_tools logs and raises package install errors."""
- m_which.return_value = None
- distro = mock.MagicMock()
- distro.update_package_sources.return_value = None
- distro.install_packages.side_effect = RuntimeError(
- 'Some install error')
- with self.assertRaises(RuntimeError) as context_manager:
- maybe_install_ua_tools(cloud=FakeCloud(distro))
- self.assertEqual('Some install error', str(context_manager.exception))
- self.assertIn(
- 'Failed to install ubuntu-advantage-tools\n', self.logs.getvalue())
-
- @mock.patch('%s.subp.which' % MPATH)
- def test_maybe_install_ua_tools_happy_path(self, m_which):
- """maybe_install_ua_tools installs ubuntu-advantage-tools."""
- m_which.return_value = None
- distro = mock.MagicMock() # No errors raised
- maybe_install_ua_tools(cloud=FakeCloud(distro))
- distro.update_package_sources.assert_called_once_with()
- distro.install_packages.assert_called_once_with(
- ['ubuntu-advantage-tools'])
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_ubuntu_drivers.py b/cloudinit/config/tests/test_ubuntu_drivers.py
deleted file mode 100644
index 504ba356..00000000
--- a/cloudinit/config/tests/test_ubuntu_drivers.py
+++ /dev/null
@@ -1,244 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import copy
-import os
-
-from cloudinit.tests.helpers import CiTestCase, skipUnlessJsonSchema, mock
-from cloudinit.config.schema import (
- SchemaValidationError, validate_cloudconfig_schema)
-from cloudinit.config import cc_ubuntu_drivers as drivers
-from cloudinit.subp import ProcessExecutionError
-
-MPATH = "cloudinit.config.cc_ubuntu_drivers."
-M_TMP_PATH = MPATH + "temp_utils.mkdtemp"
-OLD_UBUNTU_DRIVERS_ERROR_STDERR = (
- "ubuntu-drivers: error: argument <command>: invalid choice: 'install' "
- "(choose from 'list', 'autoinstall', 'devices', 'debug')\n")
-
-
-# The tests in this module call helper methods which are decorated with
-# mock.patch. pylint doesn't understand that mock.patch passes parameters to
-# the decorated function, so it incorrectly reports that we aren't passing
-# values for all parameters. Instead of annotating every single call, we
-# disable it for the entire module:
-# pylint: disable=no-value-for-parameter
-
-class AnyTempScriptAndDebconfFile(object):
-
- def __init__(self, tmp_dir, debconf_file):
- self.tmp_dir = tmp_dir
- self.debconf_file = debconf_file
-
- def __eq__(self, cmd):
- if not len(cmd) == 2:
- return False
- script, debconf_file = cmd
- if bool(script.startswith(self.tmp_dir) and script.endswith('.sh')):
- return debconf_file == self.debconf_file
- return False
-
-
-class TestUbuntuDrivers(CiTestCase):
- cfg_accepted = {'drivers': {'nvidia': {'license-accepted': True}}}
- install_gpgpu = ['ubuntu-drivers', 'install', '--gpgpu', 'nvidia']
-
- with_logs = True
-
- @skipUnlessJsonSchema()
- def test_schema_requires_boolean_for_license_accepted(self):
- with self.assertRaisesRegex(
- SchemaValidationError, ".*license-accepted.*TRUE.*boolean"):
- validate_cloudconfig_schema(
- {'drivers': {'nvidia': {'license-accepted': "TRUE"}}},
- schema=drivers.schema, strict=True)
-
- @mock.patch(M_TMP_PATH)
- @mock.patch(MPATH + "subp.subp", return_value=('', ''))
- @mock.patch(MPATH + "subp.which", return_value=False)
- def _assert_happy_path_taken(
- self, config, m_which, m_subp, m_tmp):
- """Positive path test through handle. Package should be installed."""
- tdir = self.tmp_dir()
- debconf_file = os.path.join(tdir, 'nvidia.template')
- m_tmp.return_value = tdir
- myCloud = mock.MagicMock()
- drivers.handle('ubuntu_drivers', config, myCloud, None, None)
- self.assertEqual([mock.call(['ubuntu-drivers-common'])],
- myCloud.distro.install_packages.call_args_list)
- self.assertEqual(
- [mock.call(AnyTempScriptAndDebconfFile(tdir, debconf_file)),
- mock.call(self.install_gpgpu)],
- m_subp.call_args_list)
-
- def test_handle_does_package_install(self):
- self._assert_happy_path_taken(self.cfg_accepted)
-
- def test_trueish_strings_are_considered_approval(self):
- for true_value in ['yes', 'true', 'on', '1']:
- new_config = copy.deepcopy(self.cfg_accepted)
- new_config['drivers']['nvidia']['license-accepted'] = true_value
- self._assert_happy_path_taken(new_config)
-
- @mock.patch(M_TMP_PATH)
- @mock.patch(MPATH + "subp.subp")
- @mock.patch(MPATH + "subp.which", return_value=False)
- def test_handle_raises_error_if_no_drivers_found(
- self, m_which, m_subp, m_tmp):
- """If ubuntu-drivers doesn't install any drivers, raise an error."""
- tdir = self.tmp_dir()
- debconf_file = os.path.join(tdir, 'nvidia.template')
- m_tmp.return_value = tdir
- myCloud = mock.MagicMock()
-
- def fake_subp(cmd):
- if cmd[0].startswith(tdir):
- return
- raise ProcessExecutionError(
- stdout='No drivers found for installation.\n', exit_code=1)
- m_subp.side_effect = fake_subp
-
- with self.assertRaises(Exception):
- drivers.handle(
- 'ubuntu_drivers', self.cfg_accepted, myCloud, None, None)
- self.assertEqual([mock.call(['ubuntu-drivers-common'])],
- myCloud.distro.install_packages.call_args_list)
- self.assertEqual(
- [mock.call(AnyTempScriptAndDebconfFile(tdir, debconf_file)),
- mock.call(self.install_gpgpu)],
- m_subp.call_args_list)
- self.assertIn('ubuntu-drivers found no drivers for installation',
- self.logs.getvalue())
-
- @mock.patch(MPATH + "subp.subp", return_value=('', ''))
- @mock.patch(MPATH + "subp.which", return_value=False)
- def _assert_inert_with_config(self, config, m_which, m_subp):
- """Helper to reduce repetition when testing negative cases"""
- myCloud = mock.MagicMock()
- drivers.handle('ubuntu_drivers', config, myCloud, None, None)
- self.assertEqual(0, myCloud.distro.install_packages.call_count)
- self.assertEqual(0, m_subp.call_count)
-
- def test_handle_inert_if_license_not_accepted(self):
- """Ensure we don't do anything if the license is rejected."""
- self._assert_inert_with_config(
- {'drivers': {'nvidia': {'license-accepted': False}}})
-
- def test_handle_inert_if_garbage_in_license_field(self):
- """Ensure we don't do anything if unknown text is in license field."""
- self._assert_inert_with_config(
- {'drivers': {'nvidia': {'license-accepted': 'garbage'}}})
-
- def test_handle_inert_if_no_license_key(self):
- """Ensure we don't do anything if no license key."""
- self._assert_inert_with_config({'drivers': {'nvidia': {}}})
-
- def test_handle_inert_if_no_nvidia_key(self):
- """Ensure we don't do anything if other license accepted."""
- self._assert_inert_with_config(
- {'drivers': {'acme': {'license-accepted': True}}})
-
- def test_handle_inert_if_string_given(self):
- """Ensure we don't do anything if string refusal given."""
- for false_value in ['no', 'false', 'off', '0']:
- self._assert_inert_with_config(
- {'drivers': {'nvidia': {'license-accepted': false_value}}})
-
- @mock.patch(MPATH + "install_drivers")
- def test_handle_no_drivers_does_nothing(self, m_install_drivers):
- """If no 'drivers' key in the config, nothing should be done."""
- myCloud = mock.MagicMock()
- myLog = mock.MagicMock()
- drivers.handle('ubuntu_drivers', {'foo': 'bzr'}, myCloud, myLog, None)
- self.assertIn('Skipping module named',
- myLog.debug.call_args_list[0][0][0])
- self.assertEqual(0, m_install_drivers.call_count)
-
- @mock.patch(M_TMP_PATH)
- @mock.patch(MPATH + "subp.subp", return_value=('', ''))
- @mock.patch(MPATH + "subp.which", return_value=True)
- def test_install_drivers_no_install_if_present(
- self, m_which, m_subp, m_tmp):
- """If 'ubuntu-drivers' is present, no package install should occur."""
- tdir = self.tmp_dir()
- debconf_file = os.path.join(tdir, 'nvidia.template')
- m_tmp.return_value = tdir
- pkg_install = mock.MagicMock()
- drivers.install_drivers(self.cfg_accepted['drivers'],
- pkg_install_func=pkg_install)
- self.assertEqual(0, pkg_install.call_count)
- self.assertEqual([mock.call('ubuntu-drivers')],
- m_which.call_args_list)
- self.assertEqual(
- [mock.call(AnyTempScriptAndDebconfFile(tdir, debconf_file)),
- mock.call(self.install_gpgpu)],
- m_subp.call_args_list)
-
- def test_install_drivers_rejects_invalid_config(self):
- """install_drivers should raise TypeError if not given a config dict"""
- pkg_install = mock.MagicMock()
- with self.assertRaisesRegex(TypeError, ".*expected dict.*"):
- drivers.install_drivers("mystring", pkg_install_func=pkg_install)
- self.assertEqual(0, pkg_install.call_count)
-
- @mock.patch(M_TMP_PATH)
- @mock.patch(MPATH + "subp.subp")
- @mock.patch(MPATH + "subp.which", return_value=False)
- def test_install_drivers_handles_old_ubuntu_drivers_gracefully(
- self, m_which, m_subp, m_tmp):
- """Older ubuntu-drivers versions should emit message and raise error"""
- tdir = self.tmp_dir()
- debconf_file = os.path.join(tdir, 'nvidia.template')
- m_tmp.return_value = tdir
- myCloud = mock.MagicMock()
-
- def fake_subp(cmd):
- if cmd[0].startswith(tdir):
- return
- raise ProcessExecutionError(
- stderr=OLD_UBUNTU_DRIVERS_ERROR_STDERR, exit_code=2)
- m_subp.side_effect = fake_subp
-
- with self.assertRaises(Exception):
- drivers.handle(
- 'ubuntu_drivers', self.cfg_accepted, myCloud, None, None)
- self.assertEqual([mock.call(['ubuntu-drivers-common'])],
- myCloud.distro.install_packages.call_args_list)
- self.assertEqual(
- [mock.call(AnyTempScriptAndDebconfFile(tdir, debconf_file)),
- mock.call(self.install_gpgpu)],
- m_subp.call_args_list)
- self.assertIn('WARNING: the available version of ubuntu-drivers is'
- ' too old to perform requested driver installation',
- self.logs.getvalue())
-
-
-# Sub-class TestUbuntuDrivers to run the same test cases, but with a version
-class TestUbuntuDriversWithVersion(TestUbuntuDrivers):
- cfg_accepted = {
- 'drivers': {'nvidia': {'license-accepted': True, 'version': '123'}}}
- install_gpgpu = ['ubuntu-drivers', 'install', '--gpgpu', 'nvidia:123']
-
- @mock.patch(M_TMP_PATH)
- @mock.patch(MPATH + "subp.subp", return_value=('', ''))
- @mock.patch(MPATH + "subp.which", return_value=False)
- def test_version_none_uses_latest(self, m_which, m_subp, m_tmp):
- tdir = self.tmp_dir()
- debconf_file = os.path.join(tdir, 'nvidia.template')
- m_tmp.return_value = tdir
- myCloud = mock.MagicMock()
- version_none_cfg = {
- 'drivers': {'nvidia': {'license-accepted': True, 'version': None}}}
- drivers.handle(
- 'ubuntu_drivers', version_none_cfg, myCloud, None, None)
- self.assertEqual(
- [mock.call(AnyTempScriptAndDebconfFile(tdir, debconf_file)),
- mock.call(['ubuntu-drivers', 'install', '--gpgpu', 'nvidia'])],
- m_subp.call_args_list)
-
- def test_specifying_a_version_doesnt_override_license_acceptance(self):
- self._assert_inert_with_config({
- 'drivers': {'nvidia': {'license-accepted': False,
- 'version': '123'}}
- })
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_users_groups.py b/cloudinit/config/tests/test_users_groups.py
deleted file mode 100644
index df89ddb3..00000000
--- a/cloudinit/config/tests/test_users_groups.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-
-from cloudinit.config import cc_users_groups
-from cloudinit.tests.helpers import CiTestCase, mock
-
-MODPATH = "cloudinit.config.cc_users_groups"
-
-
-@mock.patch('cloudinit.distros.ubuntu.Distro.create_group')
-@mock.patch('cloudinit.distros.ubuntu.Distro.create_user')
-class TestHandleUsersGroups(CiTestCase):
- """Test cc_users_groups handling of config."""
-
- with_logs = True
-
- def test_handle_no_cfg_creates_no_users_or_groups(self, m_user, m_group):
- """Test handle with no config will not create users or groups."""
- cfg = {} # merged cloud-config
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'ubuntu', 'lock_passwd': True,
- 'groups': ['lxd', 'sudo'],
- 'shell': '/bin/bash'}}
- metadata = {}
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- m_user.assert_not_called()
- m_group.assert_not_called()
-
- def test_handle_users_in_cfg_calls_create_users(self, m_user, m_group):
- """When users in config, create users with distro.create_user."""
- cfg = {'users': ['default', {'name': 'me2'}]} # merged cloud-config
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'ubuntu', 'lock_passwd': True,
- 'groups': ['lxd', 'sudo'],
- 'shell': '/bin/bash'}}
- metadata = {}
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- self.assertCountEqual(
- m_user.call_args_list,
- [mock.call('ubuntu', groups='lxd,sudo', lock_passwd=True,
- shell='/bin/bash'),
- mock.call('me2', default=False)])
- m_group.assert_not_called()
-
- @mock.patch('cloudinit.distros.freebsd.Distro.create_group')
- @mock.patch('cloudinit.distros.freebsd.Distro.create_user')
- def test_handle_users_in_cfg_calls_create_users_on_bsd(
- self,
- m_fbsd_user,
- m_fbsd_group,
- m_linux_user,
- m_linux_group,
- ):
- """When users in config, create users with freebsd.create_user."""
- cfg = {'users': ['default', {'name': 'me2'}]} # merged cloud-config
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'freebsd', 'lock_passwd': True,
- 'groups': ['wheel'],
- 'shell': '/bin/tcsh'}}
- metadata = {}
- cloud = self.tmp_cloud(
- distro='freebsd', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- self.assertCountEqual(
- m_fbsd_user.call_args_list,
- [mock.call('freebsd', groups='wheel', lock_passwd=True,
- shell='/bin/tcsh'),
- mock.call('me2', default=False)])
- m_fbsd_group.assert_not_called()
- m_linux_group.assert_not_called()
- m_linux_user.assert_not_called()
-
- def test_users_with_ssh_redirect_user_passes_keys(self, m_user, m_group):
- """When ssh_redirect_user is True pass default user and cloud keys."""
- cfg = {
- 'users': ['default', {'name': 'me2', 'ssh_redirect_user': True}]}
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'ubuntu', 'lock_passwd': True,
- 'groups': ['lxd', 'sudo'],
- 'shell': '/bin/bash'}}
- metadata = {'public-keys': ['key1']}
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- self.assertCountEqual(
- m_user.call_args_list,
- [mock.call('ubuntu', groups='lxd,sudo', lock_passwd=True,
- shell='/bin/bash'),
- mock.call('me2', cloud_public_ssh_keys=['key1'], default=False,
- ssh_redirect_user='ubuntu')])
- m_group.assert_not_called()
-
- def test_users_with_ssh_redirect_user_default_str(self, m_user, m_group):
- """When ssh_redirect_user is 'default' pass default username."""
- cfg = {
- 'users': ['default', {'name': 'me2',
- 'ssh_redirect_user': 'default'}]}
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'ubuntu', 'lock_passwd': True,
- 'groups': ['lxd', 'sudo'],
- 'shell': '/bin/bash'}}
- metadata = {'public-keys': ['key1']}
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- self.assertCountEqual(
- m_user.call_args_list,
- [mock.call('ubuntu', groups='lxd,sudo', lock_passwd=True,
- shell='/bin/bash'),
- mock.call('me2', cloud_public_ssh_keys=['key1'], default=False,
- ssh_redirect_user='ubuntu')])
- m_group.assert_not_called()
-
- def test_users_with_ssh_redirect_user_non_default(self, m_user, m_group):
- """Warn when ssh_redirect_user is not 'default'."""
- cfg = {
- 'users': ['default', {'name': 'me2',
- 'ssh_redirect_user': 'snowflake'}]}
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'ubuntu', 'lock_passwd': True,
- 'groups': ['lxd', 'sudo'],
- 'shell': '/bin/bash'}}
- metadata = {'public-keys': ['key1']}
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- with self.assertRaises(ValueError) as context_manager:
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- m_group.assert_not_called()
- self.assertEqual(
- 'Not creating user me2. Invalid value of ssh_redirect_user:'
- ' snowflake. Expected values: true, default or false.',
- str(context_manager.exception))
-
- def test_users_with_ssh_redirect_user_default_false(self, m_user, m_group):
- """When unspecified ssh_redirect_user is false and not set up."""
- cfg = {'users': ['default', {'name': 'me2'}]}
- # System config defines a default user for the distro.
- sys_cfg = {'default_user': {'name': 'ubuntu', 'lock_passwd': True,
- 'groups': ['lxd', 'sudo'],
- 'shell': '/bin/bash'}}
- metadata = {'public-keys': ['key1']}
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- self.assertCountEqual(
- m_user.call_args_list,
- [mock.call('ubuntu', groups='lxd,sudo', lock_passwd=True,
- shell='/bin/bash'),
- mock.call('me2', default=False)])
- m_group.assert_not_called()
-
- def test_users_ssh_redirect_user_and_no_default(self, m_user, m_group):
- """Warn when ssh_redirect_user is True and no default user present."""
- cfg = {
- 'users': ['default', {'name': 'me2', 'ssh_redirect_user': True}]}
- # System config defines *no* default user for the distro.
- sys_cfg = {}
- metadata = {} # no public-keys defined
- cloud = self.tmp_cloud(
- distro='ubuntu', sys_cfg=sys_cfg, metadata=metadata)
- cc_users_groups.handle('modulename', cfg, cloud, None, None)
- m_user.assert_called_once_with('me2', default=False)
- m_group.assert_not_called()
- self.assertEqual(
- 'WARNING: Ignoring ssh_redirect_user: True for me2. No'
- ' default_user defined. Perhaps missing'
- ' cloud configuration users: [default, ..].\n',
- self.logs.getvalue())
diff --git a/cloudinit/distros/tests/__init__.py b/cloudinit/distros/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/cloudinit/distros/tests/__init__.py
+++ /dev/null
diff --git a/cloudinit/distros/tests/test_init.py b/cloudinit/distros/tests/test_init.py
deleted file mode 100644
index fd64a322..00000000
--- a/cloudinit/distros/tests/test_init.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# Copyright (C) 2020 Canonical Ltd.
-#
-# Author: Daniel Watkins <oddbloke@ubuntu.com>
-#
-# This file is part of cloud-init. See LICENSE file for license information.
-"""Tests for cloudinit/distros/__init__.py"""
-
-from unittest import mock
-
-import pytest
-
-from cloudinit.distros import _get_package_mirror_info, LDH_ASCII_CHARS
-
-# In newer versions of Python, these characters will be omitted instead
-# of substituted because of security concerns.
-# See https://bugs.python.org/issue43882
-SECURITY_URL_CHARS = '\n\r\t'
-
-# Define a set of characters we would expect to be replaced
-INVALID_URL_CHARS = [
- chr(x) for x in range(127)
- if chr(x) not in LDH_ASCII_CHARS + SECURITY_URL_CHARS
-]
-for separator in [":", ".", "/", "#", "?", "@", "[", "]"]:
- # Remove from the set characters that either separate hostname parts (":",
- # "."), terminate hostnames ("/", "#", "?", "@"), or cause Python to be
- # unable to parse URLs ("[", "]").
- INVALID_URL_CHARS.remove(separator)
-
-
-class TestGetPackageMirrorInfo:
- """
- Tests for cloudinit.distros._get_package_mirror_info.
-
- These supplement the tests in tests/unittests/test_distros/test_generic.py
- which are more focused on testing a single production-like configuration.
- These tests are more focused on specific aspects of the unit under test.
- """
-
- @pytest.mark.parametrize('mirror_info,expected', [
- # Empty info gives empty return
- ({}, {}),
- # failsafe values used if present
- ({'failsafe': {'primary': 'http://value', 'security': 'http://other'}},
- {'primary': 'http://value', 'security': 'http://other'}),
- # search values used if present
- ({'search': {'primary': ['http://value'],
- 'security': ['http://other']}},
- {'primary': ['http://value'], 'security': ['http://other']}),
- # failsafe values used if search value not present
- ({'search': {'primary': ['http://value']},
- 'failsafe': {'security': 'http://other'}},
- {'primary': ['http://value'], 'security': 'http://other'})
- ])
- def test_get_package_mirror_info_failsafe(self, mirror_info, expected):
- """
- Test the interaction between search and failsafe inputs
-
- (This doesn't test the case where the mirror_filter removes all search
- options; test_failsafe_used_if_all_search_results_filtered_out covers
- that.)
- """
- assert expected == _get_package_mirror_info(mirror_info,
- mirror_filter=lambda x: x)
-
- def test_failsafe_used_if_all_search_results_filtered_out(self):
- """Test the failsafe option used if all search options eliminated."""
- mirror_info = {
- 'search': {'primary': ['http://value']},
- 'failsafe': {'primary': 'http://other'}
- }
- assert {'primary': 'http://other'} == _get_package_mirror_info(
- mirror_info, mirror_filter=lambda x: False)
-
- @pytest.mark.parametrize('allow_ec2_mirror, platform_type', [
- (True, 'ec2')
- ])
- @pytest.mark.parametrize('availability_zone,region,patterns,expected', (
- # Test ec2_region alone
- ('fk-fake-1f', None, ['http://EC2-%(ec2_region)s/ubuntu'],
- ['http://ec2-fk-fake-1/ubuntu']),
- # Test availability_zone alone
- ('fk-fake-1f', None, ['http://AZ-%(availability_zone)s/ubuntu'],
- ['http://az-fk-fake-1f/ubuntu']),
- # Test region alone
- (None, 'fk-fake-1', ['http://RG-%(region)s/ubuntu'],
- ['http://rg-fk-fake-1/ubuntu']),
- # Test that ec2_region is not available for non-matching AZs
- ('fake-fake-1f', None,
- ['http://EC2-%(ec2_region)s/ubuntu',
- 'http://AZ-%(availability_zone)s/ubuntu'],
- ['http://az-fake-fake-1f/ubuntu']),
- # Test that template order maintained
- (None, 'fake-region',
- ['http://RG-%(region)s-2/ubuntu', 'http://RG-%(region)s-1/ubuntu'],
- ['http://rg-fake-region-2/ubuntu', 'http://rg-fake-region-1/ubuntu']),
- # Test that non-ASCII hostnames are IDNA encoded;
- # "IDNA-ТεЅТ̣".encode('idna') == b"xn--idna--4kd53hh6aba3q"
- (None, 'ТεЅТ̣', ['http://www.IDNA-%(region)s.com/ubuntu'],
- ['http://www.xn--idna--4kd53hh6aba3q.com/ubuntu']),
- # Test that non-ASCII hostnames with a port are IDNA encoded;
- # "IDNA-ТεЅТ̣".encode('idna') == b"xn--idna--4kd53hh6aba3q"
- (None, 'ТεЅТ̣', ['http://www.IDNA-%(region)s.com:8080/ubuntu'],
- ['http://www.xn--idna--4kd53hh6aba3q.com:8080/ubuntu']),
- # Test that non-ASCII non-hostname parts of URLs are unchanged
- (None, 'ТεЅТ̣', ['http://www.example.com/%(region)s/ubuntu'],
- ['http://www.example.com/ТεЅТ̣/ubuntu']),
- # Test that IPv4 addresses are unchanged
- (None, 'fk-fake-1', ['http://192.168.1.1:8080/%(region)s/ubuntu'],
- ['http://192.168.1.1:8080/fk-fake-1/ubuntu']),
- # Test that IPv6 addresses are unchanged
- (None, 'fk-fake-1',
- ['http://[2001:67c:1360:8001::23]/%(region)s/ubuntu'],
- ['http://[2001:67c:1360:8001::23]/fk-fake-1/ubuntu']),
- # Test that unparseable URLs are filtered out of the mirror list
- (None, 'inv[lid',
- ['http://%(region)s.in.hostname/should/be/filtered',
- 'http://but.not.in.the.path/%(region)s'],
- ['http://but.not.in.the.path/inv[lid']),
- (None, '-some-region-',
- ['http://-lead-ing.%(region)s.trail-ing-.example.com/ubuntu'],
- ['http://lead-ing.some-region.trail-ing.example.com/ubuntu']),
- ) + tuple(
- # Dynamically generate a test case for each non-LDH
- # (Letters/Digits/Hyphen) ASCII character, testing that it is
- # substituted with a hyphen
- (None, 'fk{0}fake{0}1'.format(invalid_char),
- ['http://%(region)s/ubuntu'], ['http://fk-fake-1/ubuntu'])
- for invalid_char in INVALID_URL_CHARS
- ))
- def test_valid_substitution(self,
- allow_ec2_mirror,
- platform_type,
- availability_zone,
- region,
- patterns,
- expected):
- """Test substitution works as expected."""
- flag_path = "cloudinit.distros." \
- "ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES"
-
- m_data_source = mock.Mock(
- availability_zone=availability_zone,
- region=region,
- platform_type=platform_type
- )
- mirror_info = {'search': {'primary': patterns}}
-
- with mock.patch(flag_path, allow_ec2_mirror):
- ret = _get_package_mirror_info(
- mirror_info,
- data_source=m_data_source,
- mirror_filter=lambda x: x
- )
- print(allow_ec2_mirror)
- print(platform_type)
- print(availability_zone)
- print(region)
- print(patterns)
- print(expected)
- assert {'primary': expected} == ret
diff --git a/cloudinit/distros/tests/test_networking.py b/cloudinit/distros/tests/test_networking.py
deleted file mode 100644
index ec508f4d..00000000
--- a/cloudinit/distros/tests/test_networking.py
+++ /dev/null
@@ -1,223 +0,0 @@
-from unittest import mock
-
-import pytest
-
-from cloudinit import net
-from cloudinit.distros.networking import (
- BSDNetworking,
- LinuxNetworking,
- Networking,
-)
-
-# See https://docs.pytest.org/en/stable/example
-# /parametrize.html#parametrizing-conditional-raising
-from contextlib import ExitStack as does_not_raise
-
-
-@pytest.yield_fixture
-def generic_networking_cls():
- """Returns a direct Networking subclass which errors on /sys usage.
-
- This enables the direct testing of functionality only present on the
- ``Networking`` super-class, and provides a check on accidentally using /sys
- in that context.
- """
-
- class TestNetworking(Networking):
- def is_physical(self, *args, **kwargs):
- raise NotImplementedError
-
- def settle(self, *args, **kwargs):
- raise NotImplementedError
-
- def try_set_link_up(self, *args, **kwargs):
- raise NotImplementedError
-
- error = AssertionError("Unexpectedly used /sys in generic networking code")
- with mock.patch(
- "cloudinit.net.get_sys_class_path", side_effect=error,
- ):
- yield TestNetworking
-
-
-@pytest.yield_fixture
-def sys_class_net(tmpdir):
- sys_class_net_path = tmpdir.join("sys/class/net")
- sys_class_net_path.ensure_dir()
- with mock.patch(
- "cloudinit.net.get_sys_class_path",
- return_value=sys_class_net_path.strpath + "/",
- ):
- yield sys_class_net_path
-
-
-class TestBSDNetworkingIsPhysical:
- def test_raises_notimplementederror(self):
- with pytest.raises(NotImplementedError):
- BSDNetworking().is_physical("eth0")
-
-
-class TestLinuxNetworkingIsPhysical:
- def test_returns_false_by_default(self, sys_class_net):
- assert not LinuxNetworking().is_physical("eth0")
-
- def test_returns_false_if_devname_exists_but_not_physical(
- self, sys_class_net
- ):
- devname = "eth0"
- sys_class_net.join(devname).mkdir()
- assert not LinuxNetworking().is_physical(devname)
-
- def test_returns_true_if_device_is_physical(self, sys_class_net):
- devname = "eth0"
- device_dir = sys_class_net.join(devname)
- device_dir.mkdir()
- device_dir.join("device").write("")
-
- assert LinuxNetworking().is_physical(devname)
-
-
-class TestBSDNetworkingTrySetLinkUp:
- def test_raises_notimplementederror(self):
- with pytest.raises(NotImplementedError):
- BSDNetworking().try_set_link_up("eth0")
-
-
-@mock.patch("cloudinit.net.is_up")
-@mock.patch("cloudinit.distros.networking.subp.subp")
-class TestLinuxNetworkingTrySetLinkUp:
- def test_calls_subp_return_true(self, m_subp, m_is_up):
- devname = "eth0"
- m_is_up.return_value = True
- is_success = LinuxNetworking().try_set_link_up(devname)
-
- assert (mock.call(['ip', 'link', 'set', devname, 'up']) ==
- m_subp.call_args_list[-1])
- assert is_success
-
- def test_calls_subp_return_false(self, m_subp, m_is_up):
- devname = "eth0"
- m_is_up.return_value = False
- is_success = LinuxNetworking().try_set_link_up(devname)
-
- assert (mock.call(['ip', 'link', 'set', devname, 'up']) ==
- m_subp.call_args_list[-1])
- assert not is_success
-
-
-class TestBSDNetworkingSettle:
- def test_settle_doesnt_error(self):
- # This also implicitly tests that it doesn't use subp.subp
- BSDNetworking().settle()
-
-
-@pytest.mark.usefixtures("sys_class_net")
-@mock.patch("cloudinit.distros.networking.util.udevadm_settle", autospec=True)
-class TestLinuxNetworkingSettle:
- def test_no_arguments(self, m_udevadm_settle):
- LinuxNetworking().settle()
-
- assert [mock.call(exists=None)] == m_udevadm_settle.call_args_list
-
- def test_exists_argument(self, m_udevadm_settle):
- LinuxNetworking().settle(exists="ens3")
-
- expected_path = net.sys_dev_path("ens3")
- assert [
- mock.call(exists=expected_path)
- ] == m_udevadm_settle.call_args_list
-
-
-class TestNetworkingWaitForPhysDevs:
- @pytest.fixture
- def wait_for_physdevs_netcfg(self):
- """This config is shared across all the tests in this class."""
-
- def ethernet(mac, name, driver=None, device_id=None):
- v2_cfg = {"set-name": name, "match": {"macaddress": mac}}
- if driver:
- v2_cfg["match"].update({"driver": driver})
- if device_id:
- v2_cfg["match"].update({"device_id": device_id})
-
- return v2_cfg
-
- physdevs = [
- ["aa:bb:cc:dd:ee:ff", "eth0", "virtio", "0x1000"],
- ["00:11:22:33:44:55", "ens3", "e1000", "0x1643"],
- ]
- netcfg = {
- "version": 2,
- "ethernets": {args[1]: ethernet(*args) for args in physdevs},
- }
- return netcfg
-
- def test_skips_settle_if_all_present(
- self, generic_networking_cls, wait_for_physdevs_netcfg,
- ):
- networking = generic_networking_cls()
- with mock.patch.object(
- networking, "get_interfaces_by_mac"
- ) as m_get_interfaces_by_mac:
- m_get_interfaces_by_mac.side_effect = iter(
- [{"aa:bb:cc:dd:ee:ff": "eth0", "00:11:22:33:44:55": "ens3"}]
- )
- with mock.patch.object(
- networking, "settle", autospec=True
- ) as m_settle:
- networking.wait_for_physdevs(wait_for_physdevs_netcfg)
- assert 0 == m_settle.call_count
-
- def test_calls_udev_settle_on_missing(
- self, generic_networking_cls, wait_for_physdevs_netcfg,
- ):
- networking = generic_networking_cls()
- with mock.patch.object(
- networking, "get_interfaces_by_mac"
- ) as m_get_interfaces_by_mac:
- m_get_interfaces_by_mac.side_effect = iter(
- [
- {
- "aa:bb:cc:dd:ee:ff": "eth0"
- }, # first call ens3 is missing
- {
- "aa:bb:cc:dd:ee:ff": "eth0",
- "00:11:22:33:44:55": "ens3",
- }, # second call has both
- ]
- )
- with mock.patch.object(
- networking, "settle", autospec=True
- ) as m_settle:
- networking.wait_for_physdevs(wait_for_physdevs_netcfg)
- m_settle.assert_called_with(exists="ens3")
-
- @pytest.mark.parametrize(
- "strict,expectation",
- [(True, pytest.raises(RuntimeError)), (False, does_not_raise())],
- )
- def test_retrying_and_strict_behaviour(
- self,
- strict,
- expectation,
- generic_networking_cls,
- wait_for_physdevs_netcfg,
- ):
- networking = generic_networking_cls()
- with mock.patch.object(
- networking, "get_interfaces_by_mac"
- ) as m_get_interfaces_by_mac:
- m_get_interfaces_by_mac.return_value = {}
-
- with mock.patch.object(
- networking, "settle", autospec=True
- ) as m_settle:
- with expectation:
- networking.wait_for_physdevs(
- wait_for_physdevs_netcfg, strict=strict
- )
-
- assert (
- 5 * len(wait_for_physdevs_netcfg["ethernets"])
- == m_settle.call_count
- )
diff --git a/cloudinit/net/tests/__init__.py b/cloudinit/net/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/cloudinit/net/tests/__init__.py
+++ /dev/null
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
deleted file mode 100644
index 28b4ecf7..00000000
--- a/cloudinit/net/tests/test_dhcp.py
+++ /dev/null
@@ -1,647 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import httpretty
-import os
-import signal
-from textwrap import dedent
-
-import cloudinit.net as net
-from cloudinit.net.dhcp import (
- InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
- parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases,
- parse_static_routes)
-from cloudinit.util import ensure_file, write_file
-from cloudinit.tests.helpers import (
- CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
-
-
-class TestParseDHCPLeasesFile(CiTestCase):
-
- def test_parse_empty_lease_file_errors(self):
- """parse_dhcp_lease_file errors when file content is empty."""
- empty_file = self.tmp_path('leases')
- ensure_file(empty_file)
- with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager:
- parse_dhcp_lease_file(empty_file)
- error = context_manager.exception
- self.assertIn('Cannot parse empty dhcp lease file', str(error))
-
- def test_parse_malformed_lease_file_content_errors(self):
- """parse_dhcp_lease_file errors when file content isn't dhcp leases."""
- non_lease_file = self.tmp_path('leases')
- write_file(non_lease_file, 'hi mom.')
- with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager:
- parse_dhcp_lease_file(non_lease_file)
- error = context_manager.exception
- self.assertIn('Cannot parse dhcp lease file', str(error))
-
- def test_parse_multiple_leases(self):
- """parse_dhcp_lease_file returns a list of all leases within."""
- lease_file = self.tmp_path('leases')
- content = dedent("""
- lease {
- interface "wlp3s0";
- fixed-address 192.168.2.74;
- filename "http://192.168.2.50/boot.php?mac=${netX}";
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- renew 4 2017/07/27 18:02:30;
- expire 5 2017/07/28 07:08:15;
- }
- lease {
- interface "wlp3s0";
- fixed-address 192.168.2.74;
- filename "http://192.168.2.50/boot.php?mac=${netX}";
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- }
- """)
- expected = [
- {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
- 'renew': '4 2017/07/27 18:02:30',
- 'expire': '5 2017/07/28 07:08:15',
- 'filename': 'http://192.168.2.50/boot.php?mac=${netX}'},
- {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
- 'filename': 'http://192.168.2.50/boot.php?mac=${netX}',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}]
- write_file(lease_file, content)
- self.assertCountEqual(expected, parse_dhcp_lease_file(lease_file))
-
-
-class TestDHCPRFC3442(CiTestCase):
-
- def test_parse_lease_finds_rfc3442_classless_static_routes(self):
- """parse_dhcp_lease_file returns rfc3442-classless-static-routes."""
- lease_file = self.tmp_path('leases')
- content = dedent("""
- lease {
- interface "wlp3s0";
- fixed-address 192.168.2.74;
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- option rfc3442-classless-static-routes 0,130,56,240,1;
- renew 4 2017/07/27 18:02:30;
- expire 5 2017/07/28 07:08:15;
- }
- """)
- expected = [
- {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
- 'rfc3442-classless-static-routes': '0,130,56,240,1',
- 'renew': '4 2017/07/27 18:02:30',
- 'expire': '5 2017/07/28 07:08:15'}]
- write_file(lease_file, content)
- self.assertCountEqual(expected, parse_dhcp_lease_file(lease_file))
-
- def test_parse_lease_finds_classless_static_routes(self):
- """
- parse_dhcp_lease_file returns classless-static-routes
- for Centos lease format.
- """
- lease_file = self.tmp_path('leases')
- content = dedent("""
- lease {
- interface "wlp3s0";
- fixed-address 192.168.2.74;
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- option classless-static-routes 0 130.56.240.1;
- renew 4 2017/07/27 18:02:30;
- expire 5 2017/07/28 07:08:15;
- }
- """)
- expected = [
- {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
- 'classless-static-routes': '0 130.56.240.1',
- 'renew': '4 2017/07/27 18:02:30',
- 'expire': '5 2017/07/28 07:08:15'}]
- write_file(lease_file, content)
- self.assertCountEqual(expected, parse_dhcp_lease_file(lease_file))
-
- @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
- @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
- def test_obtain_lease_parses_static_routes(self, m_maybe, m_ipv4):
- """EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network"""
- lease = [
- {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
- 'rfc3442-classless-static-routes': '0,130,56,240,1',
- 'renew': '4 2017/07/27 18:02:30',
- 'expire': '5 2017/07/28 07:08:15'}]
- m_maybe.return_value = lease
- eph = net.dhcp.EphemeralDHCPv4()
- eph.obtain_lease()
- expected_kwargs = {
- 'interface': 'wlp3s0',
- 'ip': '192.168.2.74',
- 'prefix_or_mask': '255.255.255.0',
- 'broadcast': '192.168.2.255',
- 'static_routes': [('0.0.0.0/0', '130.56.240.1')],
- 'router': '192.168.2.1'}
- m_ipv4.assert_called_with(**expected_kwargs)
-
- @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
- @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
- def test_obtain_centos_lease_parses_static_routes(self, m_maybe, m_ipv4):
- """
- EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network
- for Centos Lease format
- """
- lease = [
- {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
- 'classless-static-routes': '0 130.56.240.1',
- 'renew': '4 2017/07/27 18:02:30',
- 'expire': '5 2017/07/28 07:08:15'}]
- m_maybe.return_value = lease
- eph = net.dhcp.EphemeralDHCPv4()
- eph.obtain_lease()
- expected_kwargs = {
- 'interface': 'wlp3s0',
- 'ip': '192.168.2.74',
- 'prefix_or_mask': '255.255.255.0',
- 'broadcast': '192.168.2.255',
- 'static_routes': [('0.0.0.0/0', '130.56.240.1')],
- 'router': '192.168.2.1'}
- m_ipv4.assert_called_with(**expected_kwargs)
-
-
-class TestDHCPParseStaticRoutes(CiTestCase):
-
- with_logs = True
-
- def parse_static_routes_empty_string(self):
- self.assertEqual([], parse_static_routes(""))
-
- def test_parse_static_routes_invalid_input_returns_empty_list(self):
- rfc3442 = "32,169,254,169,254,130,56,248"
- self.assertEqual([], parse_static_routes(rfc3442))
-
- def test_parse_static_routes_bogus_width_returns_empty_list(self):
- rfc3442 = "33,169,254,169,254,130,56,248"
- self.assertEqual([], parse_static_routes(rfc3442))
-
- def test_parse_static_routes_single_ip(self):
- rfc3442 = "32,169,254,169,254,130,56,248,255"
- self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
- parse_static_routes(rfc3442))
-
- def test_parse_static_routes_single_ip_handles_trailing_semicolon(self):
- rfc3442 = "32,169,254,169,254,130,56,248,255;"
- self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
- parse_static_routes(rfc3442))
-
- def test_parse_static_routes_default_route(self):
- rfc3442 = "0,130,56,240,1"
- self.assertEqual([('0.0.0.0/0', '130.56.240.1')],
- parse_static_routes(rfc3442))
-
- def test_unspecified_gateway(self):
- rfc3442 = "32,169,254,169,254,0,0,0,0"
- self.assertEqual([('169.254.169.254/32', '0.0.0.0')],
- parse_static_routes(rfc3442))
-
- def test_parse_static_routes_class_c_b_a(self):
- class_c = "24,192,168,74,192,168,0,4"
- class_b = "16,172,16,172,16,0,4"
- class_a = "8,10,10,0,0,4"
- rfc3442 = ",".join([class_c, class_b, class_a])
- self.assertEqual(sorted([
- ("192.168.74.0/24", "192.168.0.4"),
- ("172.16.0.0/16", "172.16.0.4"),
- ("10.0.0.0/8", "10.0.0.4")
- ]), sorted(parse_static_routes(rfc3442)))
-
- def test_parse_static_routes_logs_error_truncated(self):
- bad_rfc3442 = {
- "class_c": "24,169,254,169,10",
- "class_b": "16,172,16,10",
- "class_a": "8,10,10",
- "gateway": "0,0",
- "netlen": "33,0",
- }
- for rfc3442 in bad_rfc3442.values():
- self.assertEqual([], parse_static_routes(rfc3442))
-
- logs = self.logs.getvalue()
- self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines()))
-
- def test_parse_static_routes_returns_valid_routes_until_parse_err(self):
- class_c = "24,192,168,74,192,168,0,4"
- class_b = "16,172,16,172,16,0,4"
- class_a_error = "8,10,10,0,0"
- rfc3442 = ",".join([class_c, class_b, class_a_error])
- self.assertEqual(sorted([
- ("192.168.74.0/24", "192.168.0.4"),
- ("172.16.0.0/16", "172.16.0.4"),
- ]), sorted(parse_static_routes(rfc3442)))
-
- logs = self.logs.getvalue()
- self.assertIn(rfc3442, logs.splitlines()[0])
-
- def test_redhat_format(self):
- redhat_format = "24.191.168.128 192.168.128.1,0 192.168.128.1"
- self.assertEqual(sorted([
- ("191.168.128.0/24", "192.168.128.1"),
- ("0.0.0.0/0", "192.168.128.1")
- ]), sorted(parse_static_routes(redhat_format)))
-
- def test_redhat_format_with_a_space_too_much_after_comma(self):
- redhat_format = "24.191.168.128 192.168.128.1, 0 192.168.128.1"
- self.assertEqual(sorted([
- ("191.168.128.0/24", "192.168.128.1"),
- ("0.0.0.0/0", "192.168.128.1")
- ]), sorted(parse_static_routes(redhat_format)))
-
-
-class TestDHCPDiscoveryClean(CiTestCase):
- with_logs = True
-
- @mock.patch('cloudinit.net.dhcp.find_fallback_nic')
- def test_no_fallback_nic_found(self, m_fallback_nic):
- """Log and do nothing when nic is absent and no fallback is found."""
- m_fallback_nic.return_value = None # No fallback nic found
- self.assertEqual([], maybe_perform_dhcp_discovery())
- self.assertIn(
- 'Skip dhcp_discovery: Unable to find fallback nic.',
- self.logs.getvalue())
-
- def test_provided_nic_does_not_exist(self):
- """When the provided nic doesn't exist, log a message and no-op."""
- self.assertEqual([], maybe_perform_dhcp_discovery('idontexist'))
- self.assertIn(
- 'Skip dhcp_discovery: nic idontexist not found in get_devicelist.',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.net.dhcp.subp.which')
- @mock.patch('cloudinit.net.dhcp.find_fallback_nic')
- def test_absent_dhclient_command(self, m_fallback, m_which):
- """When dhclient doesn't exist in the OS, log the issue and no-op."""
- m_fallback.return_value = 'eth9'
- m_which.return_value = None # dhclient isn't found
- self.assertEqual([], maybe_perform_dhcp_discovery())
- self.assertIn(
- 'Skip dhclient configuration: No dhclient command found.',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.temp_utils.os.getuid')
- @mock.patch('cloudinit.net.dhcp.dhcp_discovery')
- @mock.patch('cloudinit.net.dhcp.subp.which')
- @mock.patch('cloudinit.net.dhcp.find_fallback_nic')
- def test_dhclient_run_with_tmpdir(self, m_fback, m_which, m_dhcp, m_uid):
- """maybe_perform_dhcp_discovery passes tmpdir to dhcp_discovery."""
- m_uid.return_value = 0 # Fake root user for tmpdir
- m_fback.return_value = 'eth9'
- m_which.return_value = '/sbin/dhclient'
- m_dhcp.return_value = {'address': '192.168.2.2'}
- retval = wrap_and_call(
- 'cloudinit.temp_utils',
- {'_TMPDIR': {'new': None},
- 'os.getuid': 0},
- maybe_perform_dhcp_discovery)
- self.assertEqual({'address': '192.168.2.2'}, retval)
- self.assertEqual(
- 1, m_dhcp.call_count, 'dhcp_discovery not called once')
- call = m_dhcp.call_args_list[0]
- self.assertEqual('/sbin/dhclient', call[0][0])
- self.assertEqual('eth9', call[0][1])
- self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])
-
- @mock.patch('time.sleep', mock.MagicMock())
- @mock.patch('cloudinit.net.dhcp.os.kill')
- @mock.patch('cloudinit.net.dhcp.subp.subp')
- def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp,
- m_kill):
- """dhcp_discovery logs a warning when pidfile contains invalid content.
-
- Lease processing still occurs and no proc kill is attempted.
- """
- m_subp.return_value = ('', '')
- tmpdir = self.tmp_dir()
- dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
- script_content = '#!/bin/bash\necho fake-dhclient'
- write_file(dhclient_script, script_content, mode=0o755)
- write_file(self.tmp_path('dhclient.pid', tmpdir), '') # Empty pid ''
- lease_content = dedent("""
- lease {
- interface "eth9";
- fixed-address 192.168.2.74;
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- }
- """)
- write_file(self.tmp_path('dhcp.leases', tmpdir), lease_content)
-
- self.assertCountEqual(
- [{'interface': 'eth9', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
- dhcp_discovery(dhclient_script, 'eth9', tmpdir))
- self.assertIn(
- "dhclient(pid=, parentpid=unknown) failed "
- "to daemonize after 10.0 seconds",
- self.logs.getvalue())
- m_kill.assert_not_called()
-
- @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
- @mock.patch('cloudinit.net.dhcp.os.kill')
- @mock.patch('cloudinit.net.dhcp.util.wait_for_files')
- @mock.patch('cloudinit.net.dhcp.subp.subp')
- def test_dhcp_discovery_run_in_sandbox_waits_on_lease_and_pid(self,
- m_subp,
- m_wait,
- m_kill,
- m_getppid):
- """dhcp_discovery waits for the presence of pidfile and dhcp.leases."""
- m_subp.return_value = ('', '')
- tmpdir = self.tmp_dir()
- dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
- script_content = '#!/bin/bash\necho fake-dhclient'
- write_file(dhclient_script, script_content, mode=0o755)
- # Don't create pid or leases file
- pidfile = self.tmp_path('dhclient.pid', tmpdir)
- leasefile = self.tmp_path('dhcp.leases', tmpdir)
- m_wait.return_value = [pidfile] # Return the missing pidfile wait for
- m_getppid.return_value = 1 # Indicate that dhclient has daemonized
- self.assertEqual([], dhcp_discovery(dhclient_script, 'eth9', tmpdir))
- self.assertEqual(
- mock.call([pidfile, leasefile], maxwait=5, naplen=0.01),
- m_wait.call_args_list[0])
- self.assertIn(
- 'WARNING: dhclient did not produce expected files: dhclient.pid',
- self.logs.getvalue())
- m_kill.assert_not_called()
-
- @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
- @mock.patch('cloudinit.net.dhcp.os.kill')
- @mock.patch('cloudinit.net.dhcp.subp.subp')
- def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill, m_getppid):
- """dhcp_discovery brings up the interface and runs dhclient.
-
- It also returns the parsed dhcp.leases file generated in the sandbox.
- """
- m_subp.return_value = ('', '')
- tmpdir = self.tmp_dir()
- dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
- script_content = '#!/bin/bash\necho fake-dhclient'
- write_file(dhclient_script, script_content, mode=0o755)
- lease_content = dedent("""
- lease {
- interface "eth9";
- fixed-address 192.168.2.74;
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- }
- """)
- lease_file = os.path.join(tmpdir, 'dhcp.leases')
- write_file(lease_file, lease_content)
- pid_file = os.path.join(tmpdir, 'dhclient.pid')
- my_pid = 1
- write_file(pid_file, "%d\n" % my_pid)
- m_getppid.return_value = 1 # Indicate that dhclient has daemonized
-
- self.assertCountEqual(
- [{'interface': 'eth9', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
- dhcp_discovery(dhclient_script, 'eth9', tmpdir))
- # dhclient script got copied
- with open(os.path.join(tmpdir, 'dhclient')) as stream:
- self.assertEqual(script_content, stream.read())
- # Interface was brought up before dhclient called from sandbox
- m_subp.assert_has_calls([
- mock.call(
- ['ip', 'link', 'set', 'dev', 'eth9', 'up'], capture=True),
- mock.call(
- [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf',
- lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),
- 'eth9', '-sf', '/bin/true'], capture=True)])
- m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
-
- @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
- @mock.patch('cloudinit.net.dhcp.os.kill')
- @mock.patch('cloudinit.net.dhcp.subp.subp')
- def test_dhcp_discovery_outside_sandbox(self, m_subp, m_kill, m_getppid):
- """dhcp_discovery brings up the interface and runs dhclient.
-
- It also returns the parsed dhcp.leases file generated in the sandbox.
- """
- m_subp.return_value = ('', '')
- tmpdir = self.tmp_dir()
- dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
- script_content = '#!/bin/bash\necho fake-dhclient'
- write_file(dhclient_script, script_content, mode=0o755)
- lease_content = dedent("""
- lease {
- interface "eth9";
- fixed-address 192.168.2.74;
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- }
- """)
- lease_file = os.path.join(tmpdir, 'dhcp.leases')
- write_file(lease_file, lease_content)
- pid_file = os.path.join(tmpdir, 'dhclient.pid')
- my_pid = 1
- write_file(pid_file, "%d\n" % my_pid)
- m_getppid.return_value = 1 # Indicate that dhclient has daemonized
-
- with mock.patch('os.access', return_value=False):
- self.assertCountEqual(
- [{'interface': 'eth9', 'fixed-address': '192.168.2.74',
- 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
- dhcp_discovery(dhclient_script, 'eth9', tmpdir))
- # dhclient script got copied
- with open(os.path.join(tmpdir, 'dhclient.orig')) as stream:
- self.assertEqual(script_content, stream.read())
- # Interface was brought up before dhclient called from sandbox
- m_subp.assert_has_calls([
- mock.call(
- ['ip', 'link', 'set', 'dev', 'eth9', 'up'], capture=True),
- mock.call(
- [os.path.join(tmpdir, 'dhclient.orig'), '-1', '-v', '-lf',
- lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),
- 'eth9', '-sf', '/bin/true'], capture=True)])
- m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
-
- @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
- @mock.patch('cloudinit.net.dhcp.os.kill')
- @mock.patch('cloudinit.net.dhcp.subp.subp')
- def test_dhcp_output_error_stream(self, m_subp, m_kill, m_getppid):
- """"dhcp_log_func is called with the output and error streams of
- dhclinet when the callable is passed."""
- dhclient_err = 'FAKE DHCLIENT ERROR'
- dhclient_out = 'FAKE DHCLIENT OUT'
- m_subp.return_value = (dhclient_out, dhclient_err)
- tmpdir = self.tmp_dir()
- dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
- script_content = '#!/bin/bash\necho fake-dhclient'
- write_file(dhclient_script, script_content, mode=0o755)
- lease_content = dedent("""
- lease {
- interface "eth9";
- fixed-address 192.168.2.74;
- option subnet-mask 255.255.255.0;
- option routers 192.168.2.1;
- }
- """)
- lease_file = os.path.join(tmpdir, 'dhcp.leases')
- write_file(lease_file, lease_content)
- pid_file = os.path.join(tmpdir, 'dhclient.pid')
- my_pid = 1
- write_file(pid_file, "%d\n" % my_pid)
- m_getppid.return_value = 1 # Indicate that dhclient has daemonized
-
- def dhcp_log_func(out, err):
- self.assertEqual(out, dhclient_out)
- self.assertEqual(err, dhclient_err)
-
- dhcp_discovery(
- dhclient_script, 'eth9', tmpdir, dhcp_log_func=dhcp_log_func)
-
-
-class TestSystemdParseLeases(CiTestCase):
-
- lxd_lease = dedent("""\
- # This is private data. Do not parse.
- ADDRESS=10.75.205.242
- NETMASK=255.255.255.0
- ROUTER=10.75.205.1
- SERVER_ADDRESS=10.75.205.1
- NEXT_SERVER=10.75.205.1
- BROADCAST=10.75.205.255
- T1=1580
- T2=2930
- LIFETIME=3600
- DNS=10.75.205.1
- DOMAINNAME=lxd
- HOSTNAME=a1
- CLIENTID=ffe617693400020000ab110c65a6a0866931c2
- """)
-
- lxd_parsed = {
- 'ADDRESS': '10.75.205.242',
- 'NETMASK': '255.255.255.0',
- 'ROUTER': '10.75.205.1',
- 'SERVER_ADDRESS': '10.75.205.1',
- 'NEXT_SERVER': '10.75.205.1',
- 'BROADCAST': '10.75.205.255',
- 'T1': '1580',
- 'T2': '2930',
- 'LIFETIME': '3600',
- 'DNS': '10.75.205.1',
- 'DOMAINNAME': 'lxd',
- 'HOSTNAME': 'a1',
- 'CLIENTID': 'ffe617693400020000ab110c65a6a0866931c2',
- }
-
- azure_lease = dedent("""\
- # This is private data. Do not parse.
- ADDRESS=10.132.0.5
- NETMASK=255.255.255.255
- ROUTER=10.132.0.1
- SERVER_ADDRESS=169.254.169.254
- NEXT_SERVER=10.132.0.1
- MTU=1460
- T1=43200
- T2=75600
- LIFETIME=86400
- DNS=169.254.169.254
- NTP=169.254.169.254
- DOMAINNAME=c.ubuntu-foundations.internal
- DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal
- HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal
- ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1
- CLIENTID=ff405663a200020000ab11332859494d7a8b4c
- OPTION_245=624c3620
- """)
-
- azure_parsed = {
- 'ADDRESS': '10.132.0.5',
- 'NETMASK': '255.255.255.255',
- 'ROUTER': '10.132.0.1',
- 'SERVER_ADDRESS': '169.254.169.254',
- 'NEXT_SERVER': '10.132.0.1',
- 'MTU': '1460',
- 'T1': '43200',
- 'T2': '75600',
- 'LIFETIME': '86400',
- 'DNS': '169.254.169.254',
- 'NTP': '169.254.169.254',
- 'DOMAINNAME': 'c.ubuntu-foundations.internal',
- 'DOMAIN_SEARCH_LIST': 'c.ubuntu-foundations.internal google.internal',
- 'HOSTNAME': 'tribaal-test-171002-1349.c.ubuntu-foundations.internal',
- 'ROUTES': '10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1',
- 'CLIENTID': 'ff405663a200020000ab11332859494d7a8b4c',
- 'OPTION_245': '624c3620'}
-
- def setUp(self):
- super(TestSystemdParseLeases, self).setUp()
- self.lease_d = self.tmp_dir()
-
- def test_no_leases_returns_empty_dict(self):
- """A leases dir with no lease files should return empty dictionary."""
- self.assertEqual({}, networkd_load_leases(self.lease_d))
-
- def test_no_leases_dir_returns_empty_dict(self):
- """A non-existing leases dir should return empty dict."""
- enodir = os.path.join(self.lease_d, 'does-not-exist')
- self.assertEqual({}, networkd_load_leases(enodir))
-
- def test_single_leases_file(self):
- """A leases dir with one leases file."""
- populate_dir(self.lease_d, {'2': self.lxd_lease})
- self.assertEqual(
- {'2': self.lxd_parsed}, networkd_load_leases(self.lease_d))
-
- def test_single_azure_leases_file(self):
- """On Azure, option 245 should be present, verify it specifically."""
- populate_dir(self.lease_d, {'1': self.azure_lease})
- self.assertEqual(
- {'1': self.azure_parsed}, networkd_load_leases(self.lease_d))
-
- def test_multiple_files(self):
- """Multiple leases files on azure with one found return that value."""
- self.maxDiff = None
- populate_dir(self.lease_d, {'1': self.azure_lease,
- '9': self.lxd_lease})
- self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed},
- networkd_load_leases(self.lease_d))
-
-
-class TestEphemeralDhcpNoNetworkSetup(HttprettyTestCase):
-
- @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
- def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp):
- """No EphemeralDhcp4 network setup when connectivity_url succeeds."""
- url = 'http://example.org/index.html'
-
- httpretty.register_uri(httpretty.GET, url)
- with net.dhcp.EphemeralDHCPv4(
- connectivity_url_data={'url': url},
- ) as lease:
- self.assertIsNone(lease)
- # Ensure that no teardown happens:
- m_dhcp.assert_not_called()
-
- @mock.patch('cloudinit.net.dhcp.subp.subp')
- @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
- def test_ephemeral_dhcp_setup_network_if_url_connectivity(
- self, m_dhcp, m_subp):
- """No EphemeralDhcp4 network setup when connectivity_url succeeds."""
- url = 'http://example.org/index.html'
- fake_lease = {
- 'interface': 'eth9', 'fixed-address': '192.168.2.2',
- 'subnet-mask': '255.255.0.0'}
- m_dhcp.return_value = [fake_lease]
- m_subp.return_value = ('', '')
-
- httpretty.register_uri(httpretty.GET, url, body={}, status=404)
- with net.dhcp.EphemeralDHCPv4(
- connectivity_url_data={'url': url},
- ) as lease:
- self.assertEqual(fake_lease, lease)
- # Ensure that dhcp discovery occurs
- m_dhcp.called_once_with()
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
deleted file mode 100644
index f9102f7b..00000000
--- a/cloudinit/net/tests/test_init.py
+++ /dev/null
@@ -1,1402 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import copy
-import errno
-import ipaddress
-import os
-import textwrap
-from unittest import mock
-
-import httpretty
-import pytest
-import requests
-
-import cloudinit.net as net
-from cloudinit import safeyaml as yaml
-from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase
-from cloudinit.subp import ProcessExecutionError
-from cloudinit.util import ensure_file, write_file
-
-
-class TestSysDevPath(CiTestCase):
-
- def test_sys_dev_path(self):
- """sys_dev_path returns a path under SYS_CLASS_NET for a device."""
- dev = 'something'
- path = 'attribute'
- expected = net.SYS_CLASS_NET + dev + '/' + path
- self.assertEqual(expected, net.sys_dev_path(dev, path))
-
- def test_sys_dev_path_without_path(self):
- """When path param isn't provided it defaults to empty string."""
- dev = 'something'
- expected = net.SYS_CLASS_NET + dev + '/'
- self.assertEqual(expected, net.sys_dev_path(dev))
-
-
-class TestReadSysNet(CiTestCase):
- with_logs = True
-
- def setUp(self):
- super(TestReadSysNet, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
-
- def test_read_sys_net_strips_contents_of_sys_path(self):
- """read_sys_net strips whitespace from the contents of a sys file."""
- content = 'some stuff with trailing whitespace\t\r\n'
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
- self.assertEqual(content.strip(), net.read_sys_net('dev', 'attr'))
-
- def test_read_sys_net_reraises_oserror(self):
- """read_sys_net raises OSError/IOError when file doesn't exist."""
- # Non-specific Exception because versions of python OSError vs IOError.
- with self.assertRaises(Exception) as context_manager: # noqa: H202
- net.read_sys_net('dev', 'attr')
- error = context_manager.exception
- self.assertIn('No such file or directory', str(error))
-
- def test_read_sys_net_handles_error_with_on_enoent(self):
- """read_sys_net handles OSError/IOError with on_enoent if provided."""
- handled_errors = []
-
- def on_enoent(e):
- handled_errors.append(e)
-
- net.read_sys_net('dev', 'attr', on_enoent=on_enoent)
- error = handled_errors[0]
- self.assertIsInstance(error, Exception)
- self.assertIn('No such file or directory', str(error))
-
- def test_read_sys_net_translates_content(self):
- """read_sys_net translates content when translate dict is provided."""
- content = "you're welcome\n"
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
- translate = {"you're welcome": 'de nada'}
- self.assertEqual(
- 'de nada',
- net.read_sys_net('dev', 'attr', translate=translate))
-
- def test_read_sys_net_errors_on_translation_failures(self):
- """read_sys_net raises a KeyError and logs details on failure."""
- content = "you're welcome\n"
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
- with self.assertRaises(KeyError) as context_manager:
- net.read_sys_net('dev', 'attr', translate={})
- error = context_manager.exception
- self.assertEqual('"you\'re welcome"', str(error))
- self.assertIn(
- "Found unexpected (not translatable) value 'you're welcome' in "
- "'{0}dev/attr".format(self.sysdir),
- self.logs.getvalue())
-
- def test_read_sys_net_handles_handles_with_onkeyerror(self):
- """read_sys_net handles translation errors calling on_keyerror."""
- content = "you're welcome\n"
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
- handled_errors = []
-
- def on_keyerror(e):
- handled_errors.append(e)
-
- net.read_sys_net('dev', 'attr', translate={}, on_keyerror=on_keyerror)
- error = handled_errors[0]
- self.assertIsInstance(error, KeyError)
- self.assertEqual('"you\'re welcome"', str(error))
-
- def test_read_sys_net_safe_false_on_translate_failure(self):
- """read_sys_net_safe returns False on translation failures."""
- content = "you're welcome\n"
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
- self.assertFalse(net.read_sys_net_safe('dev', 'attr', translate={}))
-
- def test_read_sys_net_safe_returns_false_on_noent_failure(self):
- """read_sys_net_safe returns False on file not found failures."""
- self.assertFalse(net.read_sys_net_safe('dev', 'attr'))
-
- def test_read_sys_net_int_returns_none_on_error(self):
- """read_sys_net_safe returns None on failures."""
- self.assertFalse(net.read_sys_net_int('dev', 'attr'))
-
- def test_read_sys_net_int_returns_none_on_valueerror(self):
- """read_sys_net_safe returns None when content is not an int."""
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), 'NOTINT\n')
- self.assertFalse(net.read_sys_net_int('dev', 'attr'))
-
- def test_read_sys_net_int_returns_integer_from_content(self):
- """read_sys_net_safe returns None on failures."""
- write_file(os.path.join(self.sysdir, 'dev', 'attr'), '1\n')
- self.assertEqual(1, net.read_sys_net_int('dev', 'attr'))
-
- def test_is_up_true(self):
- """is_up is True if sys/net/devname/operstate is 'up' or 'unknown'."""
- for state in ['up', 'unknown']:
- write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
- self.assertTrue(net.is_up('eth0'))
-
- def test_is_up_false(self):
- """is_up is False if sys/net/devname/operstate is 'down' or invalid."""
- for state in ['down', 'incomprehensible']:
- write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
- self.assertFalse(net.is_up('eth0'))
-
- def test_is_bridge(self):
- """is_bridge is True when /sys/net/devname/bridge exists."""
- self.assertFalse(net.is_bridge('eth0'))
- ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
- self.assertTrue(net.is_bridge('eth0'))
-
- def test_is_bond(self):
- """is_bond is True when /sys/net/devname/bonding exists."""
- self.assertFalse(net.is_bond('eth0'))
- ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
- self.assertTrue(net.is_bond('eth0'))
-
- def test_get_master(self):
- """get_master returns the path when /sys/net/devname/master exists."""
- self.assertIsNone(net.get_master('enP1s1'))
- master_path = os.path.join(self.sysdir, 'enP1s1', 'master')
- ensure_file(master_path)
- self.assertEqual(master_path, net.get_master('enP1s1'))
-
- def test_master_is_bridge_or_bond(self):
- bridge_mac = 'aa:bb:cc:aa:bb:cc'
- bond_mac = 'cc:bb:aa:cc:bb:aa'
-
- # No master => False
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), bridge_mac)
- write_file(os.path.join(self.sysdir, 'eth2', 'address'), bond_mac)
-
- self.assertFalse(net.master_is_bridge_or_bond('eth1'))
- self.assertFalse(net.master_is_bridge_or_bond('eth2'))
-
- # masters without bridge/bonding => False
- write_file(os.path.join(self.sysdir, 'br0', 'address'), bridge_mac)
- write_file(os.path.join(self.sysdir, 'bond0', 'address'), bond_mac)
-
- os.symlink('../br0', os.path.join(self.sysdir, 'eth1', 'master'))
- os.symlink('../bond0', os.path.join(self.sysdir, 'eth2', 'master'))
-
- self.assertFalse(net.master_is_bridge_or_bond('eth1'))
- self.assertFalse(net.master_is_bridge_or_bond('eth2'))
-
- # masters with bridge/bonding => True
- write_file(os.path.join(self.sysdir, 'br0', 'bridge'), '')
- write_file(os.path.join(self.sysdir, 'bond0', 'bonding'), '')
-
- self.assertTrue(net.master_is_bridge_or_bond('eth1'))
- self.assertTrue(net.master_is_bridge_or_bond('eth2'))
-
- def test_master_is_openvswitch(self):
- ovs_mac = 'bb:cc:aa:bb:cc:aa'
-
- # No master => False
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), ovs_mac)
-
- self.assertFalse(net.master_is_bridge_or_bond('eth1'))
-
- # masters without ovs-system => False
- write_file(os.path.join(self.sysdir, 'ovs-system', 'address'), ovs_mac)
-
- os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth1',
- 'master'))
-
- self.assertFalse(net.master_is_openvswitch('eth1'))
-
- # masters with ovs-system => True
- os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth1',
- 'upper_ovs-system'))
-
- self.assertTrue(net.master_is_openvswitch('eth1'))
-
- def test_is_vlan(self):
- """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""
- ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))
- self.assertFalse(net.is_vlan('eth0'))
- content = 'junk\nDEVTYPE=vlan\njunk\n'
- write_file(os.path.join(self.sysdir, 'eth0', 'uevent'), content)
- self.assertTrue(net.is_vlan('eth0'))
-
-
-class TestGenerateFallbackConfig(CiTestCase):
-
- def setUp(self):
- super(TestGenerateFallbackConfig, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
- self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
- return_value=False)
- self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
- self.add_patch('cloudinit.net.is_netfailover', 'm_netfail',
- return_value=False)
- self.add_patch('cloudinit.net.is_netfail_master', 'm_netfail_master',
- return_value=False)
-
- def test_generate_fallback_finds_connected_eth_with_mac(self):
- """generate_fallback_config finds any connected device with a mac."""
- write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
- write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
- expected = {
- 'ethernets': {'eth1': {'match': {'macaddress': mac},
- 'dhcp4': True, 'set-name': 'eth1'}},
- 'version': 2}
- self.assertEqual(expected, net.generate_fallback_config())
-
- def test_generate_fallback_finds_dormant_eth_with_mac(self):
- """generate_fallback_config finds any dormant device with a mac."""
- write_file(os.path.join(self.sysdir, 'eth0', 'dormant'), '1')
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
- expected = {
- 'ethernets': {'eth0': {'match': {'macaddress': mac}, 'dhcp4': True,
- 'set-name': 'eth0'}},
- 'version': 2}
- self.assertEqual(expected, net.generate_fallback_config())
-
- def test_generate_fallback_finds_eth_by_operstate(self):
- """generate_fallback_config finds any dormant device with a mac."""
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
- expected = {
- 'ethernets': {
- 'eth0': {'dhcp4': True, 'match': {'macaddress': mac},
- 'set-name': 'eth0'}},
- 'version': 2}
- valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown']
- for state in valid_operstates:
- write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
- self.assertEqual(expected, net.generate_fallback_config())
- write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'noworky')
- self.assertIsNone(net.generate_fallback_config())
-
- def test_generate_fallback_config_skips_veth(self):
- """generate_fallback_config will skip any veth interfaces."""
- # A connected veth which gets ignored
- write_file(os.path.join(self.sysdir, 'veth0', 'carrier'), '1')
- self.assertIsNone(net.generate_fallback_config())
-
- def test_generate_fallback_config_skips_bridges(self):
- """generate_fallback_config will skip any bridges interfaces."""
- # A connected veth which gets ignored
- write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
- ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
- self.assertIsNone(net.generate_fallback_config())
-
- def test_generate_fallback_config_skips_bonds(self):
- """generate_fallback_config will skip any bonded interfaces."""
- # A connected veth which gets ignored
- write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
- ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
- self.assertIsNone(net.generate_fallback_config())
-
- def test_generate_fallback_config_skips_netfail_devs(self):
- """gen_fallback_config ignores netfail primary,sby no mac on master."""
- mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
- for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
- write_file(os.path.join(self.sysdir, iface, 'carrier'), '1')
- write_file(
- os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
- write_file(
- os.path.join(self.sysdir, iface, 'address'), mac)
-
- def is_netfail(iface, _driver=None):
- # ens3 is the master
- if iface == 'ens3':
- return False
- return True
- self.m_netfail.side_effect = is_netfail
-
- def is_netfail_master(iface, _driver=None):
- # ens3 is the master
- if iface == 'ens3':
- return True
- return False
- self.m_netfail_master.side_effect = is_netfail_master
- expected = {
- 'ethernets': {
- 'ens3': {'dhcp4': True, 'match': {'name': 'ens3'},
- 'set-name': 'ens3'}},
- 'version': 2}
- result = net.generate_fallback_config()
- self.assertEqual(expected, result)
-
-
-class TestNetFindFallBackNic(CiTestCase):
-
- def setUp(self):
- super(TestNetFindFallBackNic, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
- self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
- return_value=False)
- self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
-
- def test_generate_fallback_finds_first_connected_eth_with_mac(self):
- """find_fallback_nic finds any connected device with a mac."""
- write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
- write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
- self.assertEqual('eth1', net.find_fallback_nic())
-
-
-class TestGetDeviceList(CiTestCase):
-
- def setUp(self):
- super(TestGetDeviceList, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
-
- def test_get_devicelist_raise_oserror(self):
- """get_devicelist raise any non-ENOENT OSerror."""
- error = OSError('Can not do it')
- error.errno = errno.EPERM # Set non-ENOENT
- self.m_sys_path.side_effect = error
- with self.assertRaises(OSError) as context_manager:
- net.get_devicelist()
- exception = context_manager.exception
- self.assertEqual('Can not do it', str(exception))
-
- def test_get_devicelist_empty_without_sys_net(self):
- """get_devicelist returns empty list when missing SYS_CLASS_NET."""
- self.m_sys_path.return_value = 'idontexist'
- self.assertEqual([], net.get_devicelist())
-
- def test_get_devicelist_empty_with_no_devices_in_sys_net(self):
- """get_devicelist returns empty directoty listing for SYS_CLASS_NET."""
- self.assertEqual([], net.get_devicelist())
-
- def test_get_devicelist_lists_any_subdirectories_in_sys_net(self):
- """get_devicelist returns a directory listing for SYS_CLASS_NET."""
- write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'up')
- write_file(os.path.join(self.sysdir, 'eth1', 'operstate'), 'up')
- self.assertCountEqual(['eth0', 'eth1'], net.get_devicelist())
-
-
-@mock.patch(
- "cloudinit.net.is_openvswitch_internal_interface",
- mock.Mock(return_value=False),
-)
-class TestGetInterfaceMAC(CiTestCase):
-
- def setUp(self):
- super(TestGetInterfaceMAC, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
-
- def test_get_interface_mac_false_with_no_mac(self):
- """get_device_list returns False when no mac is reported."""
- ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
- mac_path = os.path.join(self.sysdir, 'eth0', 'address')
- self.assertFalse(os.path.exists(mac_path))
- self.assertFalse(net.get_interface_mac('eth0'))
-
- def test_get_interface_mac(self):
- """get_interfaces returns the mac from SYS_CLASS_NET/dev/address."""
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
- self.assertEqual(mac, net.get_interface_mac('eth1'))
-
- def test_get_interface_mac_grabs_bonding_address(self):
- """get_interfaces returns the source device mac for bonded devices."""
- source_dev_mac = 'aa:bb:cc:aa:bb:cc'
- bonded_mac = 'dd:ee:ff:dd:ee:ff'
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), bonded_mac)
- write_file(
- os.path.join(self.sysdir, 'eth1', 'bonding_slave', 'perm_hwaddr'),
- source_dev_mac)
- self.assertEqual(source_dev_mac, net.get_interface_mac('eth1'))
-
- def test_get_interfaces_empty_list_without_sys_net(self):
- """get_interfaces returns an empty list when missing SYS_CLASS_NET."""
- self.m_sys_path.return_value = 'idontexist'
- self.assertEqual([], net.get_interfaces())
-
- def test_get_interfaces_by_mac_skips_empty_mac(self):
- """Ignore 00:00:00:00:00:00 addresses from get_interfaces_by_mac."""
- empty_mac = '00:00:00:00:00:00'
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), empty_mac)
- write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
- write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
- write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
- expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
- self.assertEqual(expected, net.get_interfaces())
-
- def test_get_interfaces_by_mac_skips_missing_mac(self):
- """Ignore interfaces without an address from get_interfaces_by_mac."""
- write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
- address_path = os.path.join(self.sysdir, 'eth1', 'address')
- self.assertFalse(os.path.exists(address_path))
- mac = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
- write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
- expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
- self.assertEqual(expected, net.get_interfaces())
-
- def test_get_interfaces_by_mac_skips_master_devs(self):
- """Ignore interfaces with a master device which would have dup mac."""
- mac1 = mac2 = 'aa:bb:cc:aa:bb:cc'
- write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac1)
- write_file(os.path.join(self.sysdir, 'eth1', 'master'), "blah")
- write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
- write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac2)
- expected = [('eth2', mac2, None, None)]
- self.assertEqual(expected, net.get_interfaces())
-
- @mock.patch('cloudinit.net.is_netfailover')
- def test_get_interfaces_by_mac_skips_netfailvoer(self, m_netfail):
- """Ignore interfaces if netfailover primary or standby."""
- mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
- for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
- write_file(
- os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
- write_file(
- os.path.join(self.sysdir, iface, 'address'), mac)
-
- def is_netfail(iface, _driver=None):
- # ens3 is the master
- if iface == 'ens3':
- return False
- else:
- return True
- m_netfail.side_effect = is_netfail
- expected = [('ens3', mac, None, None)]
- self.assertEqual(expected, net.get_interfaces())
-
- def test_get_interfaces_does_not_skip_phys_members_of_bridges_and_bonds(
- self
- ):
- bridge_mac = 'aa:bb:cc:aa:bb:cc'
- bond_mac = 'cc:bb:aa:cc:bb:aa'
- ovs_mac = 'bb:cc:aa:bb:cc:aa'
-
- write_file(os.path.join(self.sysdir, 'br0', 'address'), bridge_mac)
- write_file(os.path.join(self.sysdir, 'br0', 'bridge'), '')
-
- write_file(os.path.join(self.sysdir, 'bond0', 'address'), bond_mac)
- write_file(os.path.join(self.sysdir, 'bond0', 'bonding'), '')
-
- write_file(os.path.join(self.sysdir, 'ovs-system', 'address'),
- ovs_mac)
-
- write_file(os.path.join(self.sysdir, 'eth1', 'address'), bridge_mac)
- os.symlink('../br0', os.path.join(self.sysdir, 'eth1', 'master'))
-
- write_file(os.path.join(self.sysdir, 'eth2', 'address'), bond_mac)
- os.symlink('../bond0', os.path.join(self.sysdir, 'eth2', 'master'))
-
- write_file(os.path.join(self.sysdir, 'eth3', 'address'), ovs_mac)
- os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth3',
- 'master'))
- os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth3',
- 'upper_ovs-system'))
-
- interface_names = [interface[0] for interface in net.get_interfaces()]
- self.assertEqual(['eth1', 'eth2', 'eth3', 'ovs-system'],
- sorted(interface_names))
-
-
-class TestInterfaceHasOwnMAC(CiTestCase):
-
- def setUp(self):
- super(TestInterfaceHasOwnMAC, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
-
- def test_interface_has_own_mac_false_when_stolen(self):
- """Return False from interface_has_own_mac when address is stolen."""
- write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '2')
- self.assertFalse(net.interface_has_own_mac('eth1'))
-
- def test_interface_has_own_mac_true_when_not_stolen(self):
- """Return False from interface_has_own_mac when mac isn't stolen."""
- valid_assign_types = ['0', '1', '3']
- assign_path = os.path.join(self.sysdir, 'eth1', 'addr_assign_type')
- for _type in valid_assign_types:
- write_file(assign_path, _type)
- self.assertTrue(net.interface_has_own_mac('eth1'))
-
- def test_interface_has_own_mac_strict_errors_on_absent_assign_type(self):
- """When addr_assign_type is absent, interface_has_own_mac errors."""
- with self.assertRaises(ValueError):
- net.interface_has_own_mac('eth1', strict=True)
-
-
-@mock.patch('cloudinit.net.subp.subp')
-class TestEphemeralIPV4Network(CiTestCase):
-
- with_logs = True
-
- def setUp(self):
- super(TestEphemeralIPV4Network, self).setUp()
- sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
- self.m_sys_path = sys_mock.start()
- self.sysdir = self.tmp_dir() + '/'
- self.m_sys_path.return_value = self.sysdir
- self.addCleanup(sys_mock.stop)
-
- def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp):
- """No required params for EphemeralIPv4Network can be None."""
- required_params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
- for key in required_params.keys():
- params = copy.deepcopy(required_params)
- params[key] = None
- with self.assertRaises(ValueError) as context_manager:
- net.EphemeralIPv4Network(**params)
- error = context_manager.exception
- self.assertIn('Cannot init network on', str(error))
- self.assertEqual(0, m_subp.call_count)
-
- def test_ephemeral_ipv4_network_errors_invalid_mask_prefix(self, m_subp):
- """Raise an error when prefix_or_mask is not a netmask or prefix."""
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'broadcast': '192.168.2.255'}
- invalid_masks = ('invalid', 'invalid.', '123.123.123')
- for error_val in invalid_masks:
- params['prefix_or_mask'] = error_val
- with self.assertRaises(ValueError) as context_manager:
- with net.EphemeralIPv4Network(**params):
- pass
- error = context_manager.exception
- self.assertIn('Cannot setup network: netmask', str(error))
- self.assertEqual(0, m_subp.call_count)
-
- def test_ephemeral_ipv4_network_performs_teardown(self, m_subp):
- """EphemeralIPv4Network performs teardown on the device if setup."""
- expected_setup_calls = [
- mock.call(
- ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
- 'broadcast', '192.168.2.255', 'dev', 'eth0'],
- capture=True, update_env={'LANG': 'C'}),
- mock.call(
- ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
- capture=True)]
- expected_teardown_calls = [
- mock.call(
- ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0',
- 'down'], capture=True),
- mock.call(
- ['ip', '-family', 'inet', 'addr', 'del', '192.168.2.2/24',
- 'dev', 'eth0'], capture=True)]
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
- with net.EphemeralIPv4Network(**params):
- self.assertEqual(expected_setup_calls, m_subp.call_args_list)
- m_subp.assert_has_calls(expected_teardown_calls)
-
- @mock.patch('cloudinit.net.readurl')
- def test_ephemeral_ipv4_no_network_if_url_connectivity(
- self, m_readurl, m_subp):
- """No network setup is performed if we can successfully connect to
- connectivity_url."""
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
- 'connectivity_url_data': {'url': 'http://example.org/index.html'}
- }
-
- with net.EphemeralIPv4Network(**params):
- self.assertEqual(
- [mock.call(url='http://example.org/index.html', timeout=5)],
- m_readurl.call_args_list
- )
- # Ensure that no teardown happens:
- m_subp.assert_has_calls([])
-
- def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):
- """EphemeralIPv4Network handles exception when address is setup.
-
- It performs no cleanup as the interface was already setup.
- """
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
- m_subp.side_effect = ProcessExecutionError(
- '', 'RTNETLINK answers: File exists', 2)
- expected_calls = [
- mock.call(
- ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
- 'broadcast', '192.168.2.255', 'dev', 'eth0'],
- capture=True, update_env={'LANG': 'C'})]
- with net.EphemeralIPv4Network(**params):
- pass
- self.assertEqual(expected_calls, m_subp.call_args_list)
- self.assertIn(
- 'Skip ephemeral network setup, eth0 already has address',
- self.logs.getvalue())
-
- def test_ephemeral_ipv4_network_with_prefix(self, m_subp):
- """EphemeralIPv4Network takes a valid prefix to setup the network."""
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '24', 'broadcast': '192.168.2.255'}
- for prefix_val in ['24', 16]: # prefix can be int or string
- params['prefix_or_mask'] = prefix_val
- with net.EphemeralIPv4Network(**params):
- pass
- m_subp.assert_has_calls([mock.call(
- ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
- 'broadcast', '192.168.2.255', 'dev', 'eth0'],
- capture=True, update_env={'LANG': 'C'})])
- m_subp.assert_has_calls([mock.call(
- ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/16',
- 'broadcast', '192.168.2.255', 'dev', 'eth0'],
- capture=True, update_env={'LANG': 'C'})])
-
- def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp):
- """Add the route when router is set and no default route exists."""
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
- 'router': '192.168.2.1'}
- m_subp.return_value = '', '' # Empty response from ip route gw check
- expected_setup_calls = [
- mock.call(
- ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
- 'broadcast', '192.168.2.255', 'dev', 'eth0'],
- capture=True, update_env={'LANG': 'C'}),
- mock.call(
- ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
- capture=True),
- mock.call(
- ['ip', 'route', 'show', '0.0.0.0/0'], capture=True),
- mock.call(['ip', '-4', 'route', 'add', '192.168.2.1',
- 'dev', 'eth0', 'src', '192.168.2.2'], capture=True),
- mock.call(
- ['ip', '-4', 'route', 'add', 'default', 'via',
- '192.168.2.1', 'dev', 'eth0'], capture=True)]
- expected_teardown_calls = [
- mock.call(['ip', '-4', 'route', 'del', 'default', 'dev', 'eth0'],
- capture=True),
- mock.call(['ip', '-4', 'route', 'del', '192.168.2.1',
- 'dev', 'eth0', 'src', '192.168.2.2'], capture=True),
- ]
-
- with net.EphemeralIPv4Network(**params):
- self.assertEqual(expected_setup_calls, m_subp.call_args_list)
- m_subp.assert_has_calls(expected_teardown_calls)
-
- def test_ephemeral_ipv4_network_with_rfc3442_static_routes(self, m_subp):
- params = {
- 'interface': 'eth0', 'ip': '192.168.2.2',
- 'prefix_or_mask': '255.255.255.255', 'broadcast': '192.168.2.255',
- 'static_routes': [('192.168.2.1/32', '0.0.0.0'),
- ('169.254.169.254/32', '192.168.2.1'),
- ('0.0.0.0/0', '192.168.2.1')],
- 'router': '192.168.2.1'}
- expected_setup_calls = [
- mock.call(
- ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/32',
- 'broadcast', '192.168.2.255', 'dev', 'eth0'],
- capture=True, update_env={'LANG': 'C'}),
- mock.call(
- ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
- capture=True),
- mock.call(
- ['ip', '-4', 'route', 'add', '192.168.2.1/32',
- 'dev', 'eth0'], capture=True),
- mock.call(
- ['ip', '-4', 'route', 'add', '169.254.169.254/32',
- 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
- mock.call(
- ['ip', '-4', 'route', 'add', '0.0.0.0/0',
- 'via', '192.168.2.1', 'dev', 'eth0'], capture=True)]
- expected_teardown_calls = [
- mock.call(
- ['ip', '-4', 'route', 'del', '0.0.0.0/0',
- 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
- mock.call(
- ['ip', '-4', 'route', 'del', '169.254.169.254/32',
- 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
- mock.call(
- ['ip', '-4', 'route', 'del', '192.168.2.1/32',
- 'dev', 'eth0'], capture=True),
- mock.call(
- ['ip', '-family', 'inet', 'link', 'set', 'dev',
- 'eth0', 'down'], capture=True),
- mock.call(
- ['ip', '-family', 'inet', 'addr', 'del',
- '192.168.2.2/32', 'dev', 'eth0'], capture=True)
- ]
- with net.EphemeralIPv4Network(**params):
- self.assertEqual(expected_setup_calls, m_subp.call_args_list)
- m_subp.assert_has_calls(expected_setup_calls + expected_teardown_calls)
-
-
-class TestApplyNetworkCfgNames(CiTestCase):
- V1_CONFIG = textwrap.dedent("""\
- version: 1
- config:
- - type: physical
- name: interface0
- mac_address: "52:54:00:12:34:00"
- subnets:
- - type: static
- address: 10.0.2.15
- netmask: 255.255.255.0
- gateway: 10.0.2.2
- """)
- V2_CONFIG = textwrap.dedent("""\
- version: 2
- ethernets:
- interface0:
- match:
- macaddress: "52:54:00:12:34:00"
- addresses:
- - 10.0.2.15/24
- gateway4: 10.0.2.2
- set-name: interface0
- """)
-
- V2_CONFIG_NO_SETNAME = textwrap.dedent("""\
- version: 2
- ethernets:
- interface0:
- match:
- macaddress: "52:54:00:12:34:00"
- addresses:
- - 10.0.2.15/24
- gateway4: 10.0.2.2
- """)
-
- V2_CONFIG_NO_MAC = textwrap.dedent("""\
- version: 2
- ethernets:
- interface0:
- match:
- driver: virtio-net
- addresses:
- - 10.0.2.15/24
- gateway4: 10.0.2.2
- set-name: interface0
- """)
-
- @mock.patch('cloudinit.net.device_devid')
- @mock.patch('cloudinit.net.device_driver')
- @mock.patch('cloudinit.net._rename_interfaces')
- def test_apply_v1_renames(self, m_rename_interfaces, m_device_driver,
- m_device_devid):
- m_device_driver.return_value = 'virtio_net'
- m_device_devid.return_value = '0x15d8'
-
- net.apply_network_config_names(yaml.load(self.V1_CONFIG))
-
- call = ['52:54:00:12:34:00', 'interface0', 'virtio_net', '0x15d8']
- m_rename_interfaces.assert_called_with([call])
-
- @mock.patch('cloudinit.net.device_devid')
- @mock.patch('cloudinit.net.device_driver')
- @mock.patch('cloudinit.net._rename_interfaces')
- def test_apply_v2_renames(self, m_rename_interfaces, m_device_driver,
- m_device_devid):
- m_device_driver.return_value = 'virtio_net'
- m_device_devid.return_value = '0x15d8'
-
- net.apply_network_config_names(yaml.load(self.V2_CONFIG))
-
- call = ['52:54:00:12:34:00', 'interface0', 'virtio_net', '0x15d8']
- m_rename_interfaces.assert_called_with([call])
-
- @mock.patch('cloudinit.net._rename_interfaces')
- def test_apply_v2_renames_skips_without_setname(self, m_rename_interfaces):
- net.apply_network_config_names(yaml.load(self.V2_CONFIG_NO_SETNAME))
- m_rename_interfaces.assert_called_with([])
-
- @mock.patch('cloudinit.net._rename_interfaces')
- def test_apply_v2_renames_skips_without_mac(self, m_rename_interfaces):
- net.apply_network_config_names(yaml.load(self.V2_CONFIG_NO_MAC))
- m_rename_interfaces.assert_called_with([])
-
- def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self):
- with self.assertRaises(RuntimeError):
- net.apply_network_config_names(yaml.load("version: 3"))
-
-
-class TestHasURLConnectivity(HttprettyTestCase):
-
- def setUp(self):
- super(TestHasURLConnectivity, self).setUp()
- self.url = 'http://fake/'
- self.kwargs = {'allow_redirects': True, 'timeout': 5.0}
-
- @mock.patch('cloudinit.net.readurl')
- def test_url_timeout_on_connectivity_check(self, m_readurl):
- """A timeout of 5 seconds is provided when reading a url."""
- self.assertTrue(
- net.has_url_connectivity({'url': self.url}),
- 'Expected True on url connect')
-
- def test_true_on_url_connectivity_success(self):
- httpretty.register_uri(httpretty.GET, self.url)
- self.assertTrue(
- net.has_url_connectivity({'url': self.url}),
- 'Expected True on url connect')
-
- @mock.patch('requests.Session.request')
- def test_true_on_url_connectivity_timeout(self, m_request):
- """A timeout raised accessing the url will return False."""
- m_request.side_effect = requests.Timeout('Fake Connection Timeout')
- self.assertFalse(
- net.has_url_connectivity({'url': self.url}),
- 'Expected False on url timeout')
-
- def test_true_on_url_connectivity_failure(self):
- httpretty.register_uri(httpretty.GET, self.url, body={}, status=404)
- self.assertFalse(
- net.has_url_connectivity({'url': self.url}),
- 'Expected False on url fail')
-
-
-def _mk_v1_phys(mac, name, driver, device_id):
- v1_cfg = {'type': 'physical', 'name': name, 'mac_address': mac}
- params = {}
- if driver:
- params.update({'driver': driver})
- if device_id:
- params.update({'device_id': device_id})
-
- if params:
- v1_cfg.update({'params': params})
-
- return v1_cfg
-
-
-def _mk_v2_phys(mac, name, driver=None, device_id=None):
- v2_cfg = {'set-name': name, 'match': {'macaddress': mac}}
- if driver:
- v2_cfg['match'].update({'driver': driver})
- if device_id:
- v2_cfg['match'].update({'device_id': device_id})
-
- return v2_cfg
-
-
-class TestExtractPhysdevs(CiTestCase):
-
- def setUp(self):
- super(TestExtractPhysdevs, self).setUp()
- self.add_patch('cloudinit.net.device_driver', 'm_driver')
- self.add_patch('cloudinit.net.device_devid', 'm_devid')
-
- def test_extract_physdevs_looks_up_driver_v1(self):
- driver = 'virtio'
- self.m_driver.return_value = driver
- physdevs = [
- ['aa:bb:cc:dd:ee:ff', 'eth0', None, '0x1000'],
- ]
- netcfg = {
- 'version': 1,
- 'config': [_mk_v1_phys(*args) for args in physdevs],
- }
- # insert the driver value for verification
- physdevs[0][2] = driver
- self.assertEqual(sorted(physdevs),
- sorted(net.extract_physdevs(netcfg)))
- self.m_driver.assert_called_with('eth0')
-
- def test_extract_physdevs_looks_up_driver_v2(self):
- driver = 'virtio'
- self.m_driver.return_value = driver
- physdevs = [
- ['aa:bb:cc:dd:ee:ff', 'eth0', None, '0x1000'],
- ]
- netcfg = {
- 'version': 2,
- 'ethernets': {args[1]: _mk_v2_phys(*args) for args in physdevs},
- }
- # insert the driver value for verification
- physdevs[0][2] = driver
- self.assertEqual(sorted(physdevs),
- sorted(net.extract_physdevs(netcfg)))
- self.m_driver.assert_called_with('eth0')
-
- def test_extract_physdevs_looks_up_devid_v1(self):
- devid = '0x1000'
- self.m_devid.return_value = devid
- physdevs = [
- ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', None],
- ]
- netcfg = {
- 'version': 1,
- 'config': [_mk_v1_phys(*args) for args in physdevs],
- }
- # insert the driver value for verification
- physdevs[0][3] = devid
- self.assertEqual(sorted(physdevs),
- sorted(net.extract_physdevs(netcfg)))
- self.m_devid.assert_called_with('eth0')
-
- def test_extract_physdevs_looks_up_devid_v2(self):
- devid = '0x1000'
- self.m_devid.return_value = devid
- physdevs = [
- ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', None],
- ]
- netcfg = {
- 'version': 2,
- 'ethernets': {args[1]: _mk_v2_phys(*args) for args in physdevs},
- }
- # insert the driver value for verification
- physdevs[0][3] = devid
- self.assertEqual(sorted(physdevs),
- sorted(net.extract_physdevs(netcfg)))
- self.m_devid.assert_called_with('eth0')
-
- def test_get_v1_type_physical(self):
- physdevs = [
- ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
- ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
- ['09:87:65:43:21:10', 'ens0p1', 'mlx4_core', '0:0:1000'],
- ]
- netcfg = {
- 'version': 1,
- 'config': [_mk_v1_phys(*args) for args in physdevs],
- }
- self.assertEqual(sorted(physdevs),
- sorted(net.extract_physdevs(netcfg)))
-
- def test_get_v2_type_physical(self):
- physdevs = [
- ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
- ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
- ['09:87:65:43:21:10', 'ens0p1', 'mlx4_core', '0:0:1000'],
- ]
- netcfg = {
- 'version': 2,
- 'ethernets': {args[1]: _mk_v2_phys(*args) for args in physdevs},
- }
- self.assertEqual(sorted(physdevs),
- sorted(net.extract_physdevs(netcfg)))
-
- def test_get_v2_type_physical_skips_if_no_set_name(self):
- netcfg = {
- 'version': 2,
- 'ethernets': {
- 'ens3': {
- 'match': {'macaddress': '00:11:22:33:44:55'},
- }
- }
- }
- self.assertEqual([], net.extract_physdevs(netcfg))
-
- def test_runtime_error_on_unknown_netcfg_version(self):
- with self.assertRaises(RuntimeError):
- net.extract_physdevs({'version': 3, 'awesome_config': []})
-
-
-class TestNetFailOver(CiTestCase):
-
- def setUp(self):
- super(TestNetFailOver, self).setUp()
- self.add_patch('cloudinit.net.util', 'm_util')
- self.add_patch('cloudinit.net.read_sys_net', 'm_read_sys_net')
- self.add_patch('cloudinit.net.device_driver', 'm_device_driver')
-
- def test_get_dev_features(self):
- devname = self.random_string()
- features = self.random_string()
- self.m_read_sys_net.return_value = features
-
- self.assertEqual(features, net.get_dev_features(devname))
- self.assertEqual(1, self.m_read_sys_net.call_count)
- self.assertEqual(mock.call(devname, 'device/features'),
- self.m_read_sys_net.call_args_list[0])
-
- def test_get_dev_features_none_returns_empty_string(self):
- devname = self.random_string()
- self.m_read_sys_net.side_effect = Exception('error')
- self.assertEqual('', net.get_dev_features(devname))
- self.assertEqual(1, self.m_read_sys_net.call_count)
- self.assertEqual(mock.call(devname, 'device/features'),
- self.m_read_sys_net.call_args_list[0])
-
- @mock.patch('cloudinit.net.get_dev_features')
- def test_has_netfail_standby_feature(self, m_dev_features):
- devname = self.random_string()
- standby_features = ('0' * 62) + '1' + '0'
- m_dev_features.return_value = standby_features
- self.assertTrue(net.has_netfail_standby_feature(devname))
-
- @mock.patch('cloudinit.net.get_dev_features')
- def test_has_netfail_standby_feature_short_is_false(self, m_dev_features):
- devname = self.random_string()
- standby_features = self.random_string()
- m_dev_features.return_value = standby_features
- self.assertFalse(net.has_netfail_standby_feature(devname))
-
- @mock.patch('cloudinit.net.get_dev_features')
- def test_has_netfail_standby_feature_not_present_is_false(self,
- m_dev_features):
- devname = self.random_string()
- standby_features = '0' * 64
- m_dev_features.return_value = standby_features
- self.assertFalse(net.has_netfail_standby_feature(devname))
-
- @mock.patch('cloudinit.net.get_dev_features')
- def test_has_netfail_standby_feature_no_features_is_false(self,
- m_dev_features):
- devname = self.random_string()
- standby_features = None
- m_dev_features.return_value = standby_features
- self.assertFalse(net.has_netfail_standby_feature(devname))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_master(self, m_exists, m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- m_exists.return_value = False # no master sysfs attr
- m_standby.return_value = True # has standby feature flag
- self.assertTrue(net.is_netfail_master(devname, driver))
-
- @mock.patch('cloudinit.net.sys_dev_path')
- def test_is_netfail_master_checks_master_attr(self, m_sysdev):
- devname = self.random_string()
- driver = 'virtio_net'
- m_sysdev.return_value = self.random_string()
- self.assertFalse(net.is_netfail_master(devname, driver))
- self.assertEqual(1, m_sysdev.call_count)
- self.assertEqual(mock.call(devname, path='master'),
- m_sysdev.call_args_list[0])
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_master_wrong_driver(self, m_exists, m_standby):
- devname = self.random_string()
- driver = self.random_string()
- self.assertFalse(net.is_netfail_master(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_master_has_master_attr(self, m_exists, m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- m_exists.return_value = True # has master sysfs attr
- self.assertFalse(net.is_netfail_master(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_master_no_standby_feat(self, m_exists, m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- m_exists.return_value = False # no master sysfs attr
- m_standby.return_value = False # no standby feature flag
- self.assertFalse(net.is_netfail_master(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- @mock.patch('cloudinit.net.sys_dev_path')
- def test_is_netfail_primary(self, m_sysdev, m_exists, m_standby):
- devname = self.random_string()
- driver = self.random_string() # device not virtio_net
- master_devname = self.random_string()
- m_sysdev.return_value = "%s/%s" % (self.random_string(),
- master_devname)
- m_exists.return_value = True # has master sysfs attr
- self.m_device_driver.return_value = 'virtio_net' # master virtio_net
- m_standby.return_value = True # has standby feature flag
- self.assertTrue(net.is_netfail_primary(devname, driver))
- self.assertEqual(1, self.m_device_driver.call_count)
- self.assertEqual(mock.call(master_devname),
- self.m_device_driver.call_args_list[0])
- self.assertEqual(1, m_standby.call_count)
- self.assertEqual(mock.call(master_devname),
- m_standby.call_args_list[0])
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- @mock.patch('cloudinit.net.sys_dev_path')
- def test_is_netfail_primary_wrong_driver(self, m_sysdev, m_exists,
- m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- self.assertFalse(net.is_netfail_primary(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- @mock.patch('cloudinit.net.sys_dev_path')
- def test_is_netfail_primary_no_master(self, m_sysdev, m_exists, m_standby):
- devname = self.random_string()
- driver = self.random_string() # device not virtio_net
- m_exists.return_value = False # no master sysfs attr
- self.assertFalse(net.is_netfail_primary(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- @mock.patch('cloudinit.net.sys_dev_path')
- def test_is_netfail_primary_bad_master(self, m_sysdev, m_exists,
- m_standby):
- devname = self.random_string()
- driver = self.random_string() # device not virtio_net
- master_devname = self.random_string()
- m_sysdev.return_value = "%s/%s" % (self.random_string(),
- master_devname)
- m_exists.return_value = True # has master sysfs attr
- self.m_device_driver.return_value = 'XXXX' # master not virtio_net
- self.assertFalse(net.is_netfail_primary(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- @mock.patch('cloudinit.net.sys_dev_path')
- def test_is_netfail_primary_no_standby(self, m_sysdev, m_exists,
- m_standby):
- devname = self.random_string()
- driver = self.random_string() # device not virtio_net
- master_devname = self.random_string()
- m_sysdev.return_value = "%s/%s" % (self.random_string(),
- master_devname)
- m_exists.return_value = True # has master sysfs attr
- self.m_device_driver.return_value = 'virtio_net' # master virtio_net
- m_standby.return_value = False # master has no standby feature flag
- self.assertFalse(net.is_netfail_primary(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_standby(self, m_exists, m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- m_exists.return_value = True # has master sysfs attr
- m_standby.return_value = True # has standby feature flag
- self.assertTrue(net.is_netfail_standby(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_standby_wrong_driver(self, m_exists, m_standby):
- devname = self.random_string()
- driver = self.random_string()
- self.assertFalse(net.is_netfail_standby(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_standby_no_master(self, m_exists, m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- m_exists.return_value = False # has master sysfs attr
- self.assertFalse(net.is_netfail_standby(devname, driver))
-
- @mock.patch('cloudinit.net.has_netfail_standby_feature')
- @mock.patch('cloudinit.net.os.path.exists')
- def test_is_netfail_standby_no_standby_feature(self, m_exists, m_standby):
- devname = self.random_string()
- driver = 'virtio_net'
- m_exists.return_value = True # has master sysfs attr
- m_standby.return_value = False # has standby feature flag
- self.assertFalse(net.is_netfail_standby(devname, driver))
-
- @mock.patch('cloudinit.net.is_netfail_standby')
- @mock.patch('cloudinit.net.is_netfail_primary')
- def test_is_netfailover_primary(self, m_primary, m_standby):
- devname = self.random_string()
- driver = self.random_string()
- m_primary.return_value = True
- m_standby.return_value = False
- self.assertTrue(net.is_netfailover(devname, driver))
-
- @mock.patch('cloudinit.net.is_netfail_standby')
- @mock.patch('cloudinit.net.is_netfail_primary')
- def test_is_netfailover_standby(self, m_primary, m_standby):
- devname = self.random_string()
- driver = self.random_string()
- m_primary.return_value = False
- m_standby.return_value = True
- self.assertTrue(net.is_netfailover(devname, driver))
-
- @mock.patch('cloudinit.net.is_netfail_standby')
- @mock.patch('cloudinit.net.is_netfail_primary')
- def test_is_netfailover_returns_false(self, m_primary, m_standby):
- devname = self.random_string()
- driver = self.random_string()
- m_primary.return_value = False
- m_standby.return_value = False
- self.assertFalse(net.is_netfailover(devname, driver))
-
-
-class TestOpenvswitchIsInstalled:
- """Test cloudinit.net.openvswitch_is_installed.
-
- Uses the ``clear_lru_cache`` local autouse fixture to allow us to test
- despite the ``lru_cache`` decorator on the unit under test.
- """
-
- @pytest.fixture(autouse=True)
- def clear_lru_cache(self):
- net.openvswitch_is_installed.cache_clear()
-
- @pytest.mark.parametrize(
- "expected,which_return", [(True, "/some/path"), (False, None)]
- )
- @mock.patch("cloudinit.net.subp.which")
- def test_mirrors_which_result(self, m_which, expected, which_return):
- m_which.return_value = which_return
- assert expected == net.openvswitch_is_installed()
-
- @mock.patch("cloudinit.net.subp.which")
- def test_only_calls_which_once(self, m_which):
- net.openvswitch_is_installed()
- net.openvswitch_is_installed()
- assert 1 == m_which.call_count
-
-
-@mock.patch("cloudinit.net.subp.subp", return_value=("", ""))
-class TestGetOVSInternalInterfaces:
- """Test cloudinit.net.get_ovs_internal_interfaces.
-
- Uses the ``clear_lru_cache`` local autouse fixture to allow us to test
- despite the ``lru_cache`` decorator on the unit under test.
- """
- @pytest.fixture(autouse=True)
- def clear_lru_cache(self):
- net.get_ovs_internal_interfaces.cache_clear()
-
- def test_command_used(self, m_subp):
- """Test we use the correct command when we call subp"""
- net.get_ovs_internal_interfaces()
-
- assert [
- mock.call(net.OVS_INTERNAL_INTERFACE_LOOKUP_CMD)
- ] == m_subp.call_args_list
-
- def test_subp_contents_split_and_returned(self, m_subp):
- """Test that the command output is appropriately mangled."""
- stdout = "iface1\niface2\niface3\n"
- m_subp.return_value = (stdout, "")
-
- assert [
- "iface1",
- "iface2",
- "iface3",
- ] == net.get_ovs_internal_interfaces()
-
- def test_database_connection_error_handled_gracefully(self, m_subp):
- """Test that the error indicating OVS is down is handled gracefully."""
- m_subp.side_effect = ProcessExecutionError(
- stderr="database connection failed"
- )
-
- assert [] == net.get_ovs_internal_interfaces()
-
- def test_other_errors_raised(self, m_subp):
- """Test that only database connection errors are handled."""
- m_subp.side_effect = ProcessExecutionError()
-
- with pytest.raises(ProcessExecutionError):
- net.get_ovs_internal_interfaces()
-
- def test_only_runs_once(self, m_subp):
- """Test that we cache the value."""
- net.get_ovs_internal_interfaces()
- net.get_ovs_internal_interfaces()
-
- assert 1 == m_subp.call_count
-
-
-@mock.patch("cloudinit.net.get_ovs_internal_interfaces")
-@mock.patch("cloudinit.net.openvswitch_is_installed")
-class TestIsOpenVSwitchInternalInterface:
- def test_false_if_ovs_not_installed(
- self, m_openvswitch_is_installed, _m_get_ovs_internal_interfaces
- ):
- """Test that OVS' absence returns False."""
- m_openvswitch_is_installed.return_value = False
-
- assert not net.is_openvswitch_internal_interface("devname")
-
- @pytest.mark.parametrize(
- "detected_interfaces,devname,expected_return",
- [
- ([], "devname", False),
- (["notdevname"], "devname", False),
- (["devname"], "devname", True),
- (["some", "other", "devices", "and", "ours"], "ours", True),
- ],
- )
- def test_return_value_based_on_detected_interfaces(
- self,
- m_openvswitch_is_installed,
- m_get_ovs_internal_interfaces,
- detected_interfaces,
- devname,
- expected_return,
- ):
- """Test that the detected interfaces are used correctly."""
- m_openvswitch_is_installed.return_value = True
- m_get_ovs_internal_interfaces.return_value = detected_interfaces
- assert expected_return == net.is_openvswitch_internal_interface(
- devname
- )
-
-
-class TestIsIpAddress:
- """Tests for net.is_ip_address.
-
- Instead of testing with values we rely on the ipaddress stdlib module to
- handle all values correctly, so simply test that is_ip_address defers to
- the ipaddress module correctly.
- """
-
- @pytest.mark.parametrize('ip_address_side_effect,expected_return', (
- (ValueError, False),
- (lambda _: ipaddress.IPv4Address('192.168.0.1'), True),
- (lambda _: ipaddress.IPv6Address('2001:db8::'), True),
- ))
- def test_is_ip_address(self, ip_address_side_effect, expected_return):
- with mock.patch('cloudinit.net.ipaddress.ip_address',
- side_effect=ip_address_side_effect) as m_ip_address:
- ret = net.is_ip_address(mock.sentinel.ip_address_in)
- assert expected_return == ret
- expected_call = mock.call(mock.sentinel.ip_address_in)
- assert [expected_call] == m_ip_address.call_args_list
-
-
-class TestIsIpv4Address:
- """Tests for net.is_ipv4_address.
-
- Instead of testing with values we rely on the ipaddress stdlib module to
- handle all values correctly, so simply test that is_ipv4_address defers to
- the ipaddress module correctly.
- """
-
- @pytest.mark.parametrize('ipv4address_mock,expected_return', (
- (mock.Mock(side_effect=ValueError), False),
- (mock.Mock(return_value=ipaddress.IPv4Address('192.168.0.1')), True),
- ))
- def test_is_ip_address(self, ipv4address_mock, expected_return):
- with mock.patch('cloudinit.net.ipaddress.IPv4Address',
- ipv4address_mock) as m_ipv4address:
- ret = net.is_ipv4_address(mock.sentinel.ip_address_in)
- assert expected_return == ret
- expected_call = mock.call(mock.sentinel.ip_address_in)
- assert [expected_call] == m_ipv4address.call_args_list
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py
deleted file mode 100644
index 45e99171..00000000
--- a/cloudinit/net/tests/test_network_state.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from unittest import mock
-
-import pytest
-
-from cloudinit import safeyaml
-from cloudinit.net import network_state
-from cloudinit.tests.helpers import CiTestCase
-
-netstate_path = 'cloudinit.net.network_state'
-
-
-_V1_CONFIG_NAMESERVERS = """\
-network:
- version: 1
- config:
- - type: nameserver
- interface: {iface}
- address:
- - 192.168.1.1
- - 8.8.8.8
- search:
- - spam.local
- - type: nameserver
- address:
- - 192.168.1.0
- - 4.4.4.4
- search:
- - eggs.local
- - type: physical
- name: eth0
- mac_address: '00:11:22:33:44:55'
- - type: physical
- name: eth1
- mac_address: '66:77:88:99:00:11'
-"""
-
-V1_CONFIG_NAMESERVERS_VALID = _V1_CONFIG_NAMESERVERS.format(iface='eth1')
-V1_CONFIG_NAMESERVERS_INVALID = _V1_CONFIG_NAMESERVERS.format(iface='eth90')
-
-V2_CONFIG_NAMESERVERS = """\
-network:
- version: 2
- ethernets:
- eth0:
- match:
- macaddress: '00:11:22:33:44:55'
- nameservers:
- search: [spam.local, eggs.local]
- addresses: [8.8.8.8]
- eth1:
- match:
- macaddress: '66:77:88:99:00:11'
- set-name: "ens92"
- nameservers:
- search: [foo.local, bar.local]
- addresses: [4.4.4.4]
-"""
-
-
-class TestNetworkStateParseConfig(CiTestCase):
-
- def setUp(self):
- super(TestNetworkStateParseConfig, self).setUp()
- nsi_path = netstate_path + '.NetworkStateInterpreter'
- self.add_patch(nsi_path, 'm_nsi')
-
- def test_missing_version_returns_none(self):
- ncfg = {}
- with self.assertRaises(RuntimeError):
- network_state.parse_net_config_data(ncfg)
-
- def test_unknown_versions_returns_none(self):
- ncfg = {'version': 13.2}
- with self.assertRaises(RuntimeError):
- network_state.parse_net_config_data(ncfg)
-
- def test_version_2_passes_self_as_config(self):
- ncfg = {'version': 2, 'otherconfig': {}, 'somemore': [1, 2, 3]}
- network_state.parse_net_config_data(ncfg)
- self.assertEqual([mock.call(version=2, config=ncfg)],
- self.m_nsi.call_args_list)
-
- def test_valid_config_gets_network_state(self):
- ncfg = {'version': 2, 'otherconfig': {}, 'somemore': [1, 2, 3]}
- result = network_state.parse_net_config_data(ncfg)
- self.assertNotEqual(None, result)
-
- def test_empty_v1_config_gets_network_state(self):
- ncfg = {'version': 1, 'config': []}
- result = network_state.parse_net_config_data(ncfg)
- self.assertNotEqual(None, result)
-
- def test_empty_v2_config_gets_network_state(self):
- ncfg = {'version': 2}
- result = network_state.parse_net_config_data(ncfg)
- self.assertNotEqual(None, result)
-
-
-class TestNetworkStateParseConfigV2(CiTestCase):
-
- def test_version_2_ignores_renderer_key(self):
- ncfg = {'version': 2, 'renderer': 'networkd', 'ethernets': {}}
- nsi = network_state.NetworkStateInterpreter(version=ncfg['version'],
- config=ncfg)
- nsi.parse_config(skip_broken=False)
- self.assertEqual(ncfg, nsi.as_dict()['config'])
-
-
-class TestNetworkStateParseNameservers:
- def _parse_network_state_from_config(self, config):
- yaml = safeyaml.load(config)
- return network_state.parse_net_config_data(yaml['network'])
-
- def test_v1_nameservers_valid(self):
- config = self._parse_network_state_from_config(
- V1_CONFIG_NAMESERVERS_VALID)
-
- # If an interface was specified, DNS shouldn't be in the global list
- assert ['192.168.1.0', '4.4.4.4'] == sorted(
- config.dns_nameservers)
- assert ['eggs.local'] == config.dns_searchdomains
-
- # If an interface was specified, DNS should be part of the interface
- for iface in config.iter_interfaces():
- if iface['name'] == 'eth1':
- assert iface['dns']['addresses'] == ['192.168.1.1', '8.8.8.8']
- assert iface['dns']['search'] == ['spam.local']
- else:
- assert 'dns' not in iface
-
- def test_v1_nameservers_invalid(self):
- with pytest.raises(ValueError):
- self._parse_network_state_from_config(
- V1_CONFIG_NAMESERVERS_INVALID)
-
- def test_v2_nameservers(self):
- config = self._parse_network_state_from_config(V2_CONFIG_NAMESERVERS)
-
- # Ensure DNS defined on interface exists on interface
- for iface in config.iter_interfaces():
- if iface['name'] == 'eth0':
- assert iface['dns'] == {
- 'nameservers': ['8.8.8.8'],
- 'search': ['spam.local', 'eggs.local'],
- }
- else:
- assert iface['dns'] == {
- 'nameservers': ['4.4.4.4'],
- 'search': ['foo.local', 'bar.local']
- }
-
- # Ensure DNS defined on interface also exists globally (since there
- # is no global DNS definitions in v2)
- assert ['4.4.4.4', '8.8.8.8'] == sorted(config.dns_nameservers)
- assert [
- 'bar.local',
- 'eggs.local',
- 'foo.local',
- 'spam.local',
- ] == sorted(config.dns_searchdomains)
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_networkd.py b/cloudinit/net/tests/test_networkd.py
deleted file mode 100644
index 8dc90b48..00000000
--- a/cloudinit/net/tests/test_networkd.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from cloudinit import safeyaml
-from cloudinit.net import networkd, network_state
-
-V2_CONFIG_SET_NAME = """\
-network:
- version: 2
- ethernets:
- eth0:
- match:
- macaddress: '00:11:22:33:44:55'
- nameservers:
- search: [spam.local, eggs.local]
- addresses: [8.8.8.8]
- eth1:
- match:
- macaddress: '66:77:88:99:00:11'
- set-name: "ens92"
- nameservers:
- search: [foo.local, bar.local]
- addresses: [4.4.4.4]
-"""
-
-V2_CONFIG_SET_NAME_RENDERED_ETH0 = """[Match]
-MACAddress=00:11:22:33:44:55
-Name=eth0
-
-[Network]
-DHCP=no
-DNS=8.8.8.8
-Domains=spam.local eggs.local
-
-"""
-
-V2_CONFIG_SET_NAME_RENDERED_ETH1 = """[Match]
-MACAddress=66:77:88:99:00:11
-Name=ens92
-
-[Network]
-DHCP=no
-DNS=4.4.4.4
-Domains=foo.local bar.local
-
-"""
-
-
-class TestNetworkdRenderState:
- def _parse_network_state_from_config(self, config):
- yaml = safeyaml.load(config)
- return network_state.parse_net_config_data(yaml["network"])
-
- def test_networkd_render_with_set_name(self):
- ns = self._parse_network_state_from_config(V2_CONFIG_SET_NAME)
- renderer = networkd.Renderer()
- rendered_content = renderer._render_content(ns)
-
- assert "eth0" in rendered_content
- assert rendered_content["eth0"] == V2_CONFIG_SET_NAME_RENDERED_ETH0
- assert "ens92" in rendered_content
- assert rendered_content["ens92"] == V2_CONFIG_SET_NAME_RENDERED_ETH1
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/tests/test_netlink.py b/cloudinit/sources/helpers/tests/test_netlink.py
deleted file mode 100644
index cafe3961..00000000
--- a/cloudinit/sources/helpers/tests/test_netlink.py
+++ /dev/null
@@ -1,480 +0,0 @@
-# Author: Tamilmani Manoharan <tamanoha@microsoft.com>
-#
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from cloudinit.tests.helpers import CiTestCase, mock
-import socket
-import struct
-import codecs
-from cloudinit.sources.helpers.netlink import (
- NetlinkCreateSocketError, create_bound_netlink_socket, read_netlink_socket,
- read_rta_oper_state, unpack_rta_attr, wait_for_media_disconnect_connect,
- wait_for_nic_attach_event, wait_for_nic_detach_event,
- OPER_DOWN, OPER_UP, OPER_DORMANT, OPER_LOWERLAYERDOWN, OPER_NOTPRESENT,
- OPER_TESTING, OPER_UNKNOWN, RTATTR_START_OFFSET, RTM_NEWLINK, RTM_DELLINK,
- RTM_SETLINK, RTM_GETLINK, MAX_SIZE)
-
-
-def int_to_bytes(i):
- '''convert integer to binary: eg: 1 to \x01'''
- hex_value = '{0:x}'.format(i)
- hex_value = '0' * (len(hex_value) % 2) + hex_value
- return codecs.decode(hex_value, 'hex_codec')
-
-
-class TestCreateBoundNetlinkSocket(CiTestCase):
-
- @mock.patch('cloudinit.sources.helpers.netlink.socket.socket')
- def test_socket_error_on_create(self, m_socket):
- '''create_bound_netlink_socket catches socket creation exception'''
-
- """NetlinkCreateSocketError is raised when socket creation errors."""
- m_socket.side_effect = socket.error("Fake socket failure")
- with self.assertRaises(NetlinkCreateSocketError) as ctx_mgr:
- create_bound_netlink_socket()
- self.assertEqual(
- 'Exception during netlink socket create: Fake socket failure',
- str(ctx_mgr.exception))
-
-
-class TestReadNetlinkSocket(CiTestCase):
-
- @mock.patch('cloudinit.sources.helpers.netlink.socket.socket')
- @mock.patch('cloudinit.sources.helpers.netlink.select.select')
- def test_read_netlink_socket(self, m_select, m_socket):
- '''read_netlink_socket able to receive data'''
- data = 'netlinktest'
- m_select.return_value = [m_socket], None, None
- m_socket.recv.return_value = data
- recv_data = read_netlink_socket(m_socket, 2)
- m_select.assert_called_with([m_socket], [], [], 2)
- m_socket.recv.assert_called_with(MAX_SIZE)
- self.assertIsNotNone(recv_data)
- self.assertEqual(recv_data, data)
-
- @mock.patch('cloudinit.sources.helpers.netlink.socket.socket')
- @mock.patch('cloudinit.sources.helpers.netlink.select.select')
- def test_netlink_read_timeout(self, m_select, m_socket):
- '''read_netlink_socket should timeout if nothing to read'''
- m_select.return_value = [], None, None
- data = read_netlink_socket(m_socket, 1)
- m_select.assert_called_with([m_socket], [], [], 1)
- self.assertEqual(m_socket.recv.call_count, 0)
- self.assertIsNone(data)
-
- def test_read_invalid_socket(self):
- '''read_netlink_socket raises assert error if socket is invalid'''
- socket = None
- with self.assertRaises(AssertionError) as context:
- read_netlink_socket(socket, 1)
- self.assertTrue('netlink socket is none' in str(context.exception))
-
-
-class TestParseNetlinkMessage(CiTestCase):
-
- def test_read_rta_oper_state(self):
- '''read_rta_oper_state could parse netlink message and extract data'''
- ifname = "eth0"
- bytes = ifname.encode("utf-8")
- buf = bytearray(48)
- struct.pack_into("HH4sHHc", buf, RTATTR_START_OFFSET, 8, 3, bytes, 5,
- 16, int_to_bytes(OPER_DOWN))
- interface_state = read_rta_oper_state(buf)
- self.assertEqual(interface_state.ifname, ifname)
- self.assertEqual(interface_state.operstate, OPER_DOWN)
-
- def test_read_none_data(self):
- '''read_rta_oper_state raises assert error if data is none'''
- data = None
- with self.assertRaises(AssertionError) as context:
- read_rta_oper_state(data)
- self.assertEqual('data is none', str(context.exception))
-
- def test_read_invalid_rta_operstate_none(self):
- '''read_rta_oper_state returns none if operstate is none'''
- ifname = "eth0"
- buf = bytearray(40)
- bytes = ifname.encode("utf-8")
- struct.pack_into("HH4s", buf, RTATTR_START_OFFSET, 8, 3, bytes)
- interface_state = read_rta_oper_state(buf)
- self.assertIsNone(interface_state)
-
- def test_read_invalid_rta_ifname_none(self):
- '''read_rta_oper_state returns none if ifname is none'''
- buf = bytearray(40)
- struct.pack_into("HHc", buf, RTATTR_START_OFFSET, 5, 16,
- int_to_bytes(OPER_DOWN))
- interface_state = read_rta_oper_state(buf)
- self.assertIsNone(interface_state)
-
- def test_read_invalid_data_len(self):
- '''raise assert error if data size is smaller than required size'''
- buf = bytearray(32)
- with self.assertRaises(AssertionError) as context:
- read_rta_oper_state(buf)
- self.assertTrue('length of data is smaller than RTATTR_START_OFFSET' in
- str(context.exception))
-
- def test_unpack_rta_attr_none_data(self):
- '''unpack_rta_attr raises assert error if data is none'''
- data = None
- with self.assertRaises(AssertionError) as context:
- unpack_rta_attr(data, RTATTR_START_OFFSET)
- self.assertTrue('data is none' in str(context.exception))
-
- def test_unpack_rta_attr_invalid_offset(self):
- '''unpack_rta_attr raises assert error if offset is invalid'''
- data = bytearray(48)
- with self.assertRaises(AssertionError) as context:
- unpack_rta_attr(data, "offset")
- self.assertTrue('offset is not integer' in str(context.exception))
- with self.assertRaises(AssertionError) as context:
- unpack_rta_attr(data, 31)
- self.assertTrue('rta offset is less than expected length' in
- str(context.exception))
-
-
-@mock.patch('cloudinit.sources.helpers.netlink.socket.socket')
-@mock.patch('cloudinit.sources.helpers.netlink.read_netlink_socket')
-class TestNicAttachDetach(CiTestCase):
- with_logs = True
-
- def _media_switch_data(self, ifname, msg_type, operstate):
- '''construct netlink data with specified fields'''
- if ifname and operstate is not None:
- data = bytearray(48)
- bytes = ifname.encode("utf-8")
- struct.pack_into("HH4sHHc", data, RTATTR_START_OFFSET, 8, 3,
- bytes, 5, 16, int_to_bytes(operstate))
- elif ifname:
- data = bytearray(40)
- bytes = ifname.encode("utf-8")
- struct.pack_into("HH4s", data, RTATTR_START_OFFSET, 8, 3, bytes)
- elif operstate:
- data = bytearray(40)
- struct.pack_into("HHc", data, RTATTR_START_OFFSET, 5, 16,
- int_to_bytes(operstate))
- struct.pack_into("=LHHLL", data, 0, len(data), msg_type, 0, 0, 0)
- return data
-
- def test_nic_attached_oper_down(self, m_read_netlink_socket, m_socket):
- '''Test for a new nic attached'''
- ifname = "eth0"
- data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN)
- m_read_netlink_socket.side_effect = [data_op_down]
- ifread = wait_for_nic_attach_event(m_socket, [])
- self.assertEqual(m_read_netlink_socket.call_count, 1)
- self.assertEqual(ifname, ifread)
-
- def test_nic_attached_oper_up(self, m_read_netlink_socket, m_socket):
- '''Test for a new nic attached'''
- ifname = "eth0"
- data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP)
- m_read_netlink_socket.side_effect = [data_op_up]
- ifread = wait_for_nic_attach_event(m_socket, [])
- self.assertEqual(m_read_netlink_socket.call_count, 1)
- self.assertEqual(ifname, ifread)
-
- def test_nic_attach_ignore_existing(self, m_read_netlink_socket, m_socket):
- '''Test that we read only the interfaces we are interested in.'''
- data_eth0 = self._media_switch_data("eth0", RTM_NEWLINK, OPER_DOWN)
- data_eth1 = self._media_switch_data("eth1", RTM_NEWLINK, OPER_DOWN)
- m_read_netlink_socket.side_effect = [data_eth0, data_eth1]
- ifread = wait_for_nic_attach_event(m_socket, ["eth0"])
- self.assertEqual(m_read_netlink_socket.call_count, 2)
- self.assertEqual("eth1", ifread)
-
- def test_nic_attach_read_first(self, m_read_netlink_socket, m_socket):
- '''Test that we read only the interfaces we are interested in.'''
- data_eth0 = self._media_switch_data("eth0", RTM_NEWLINK, OPER_DOWN)
- data_eth1 = self._media_switch_data("eth1", RTM_NEWLINK, OPER_DOWN)
- m_read_netlink_socket.side_effect = [data_eth0, data_eth1]
- ifread = wait_for_nic_attach_event(m_socket, ["eth1"])
- self.assertEqual(m_read_netlink_socket.call_count, 1)
- self.assertEqual("eth0", ifread)
-
- def test_nic_detached(self, m_read_netlink_socket, m_socket):
- '''Test for an existing nic detached'''
- ifname = "eth0"
- data_op_down = self._media_switch_data(ifname, RTM_DELLINK, OPER_DOWN)
- m_read_netlink_socket.side_effect = [data_op_down]
- ifread = wait_for_nic_detach_event(m_socket)
- self.assertEqual(m_read_netlink_socket.call_count, 1)
- self.assertEqual(ifname, ifread)
-
-
-@mock.patch('cloudinit.sources.helpers.netlink.socket.socket')
-@mock.patch('cloudinit.sources.helpers.netlink.read_netlink_socket')
-class TestWaitForMediaDisconnectConnect(CiTestCase):
- with_logs = True
-
- def _media_switch_data(self, ifname, msg_type, operstate):
- '''construct netlink data with specified fields'''
- if ifname and operstate is not None:
- data = bytearray(48)
- bytes = ifname.encode("utf-8")
- struct.pack_into("HH4sHHc", data, RTATTR_START_OFFSET, 8, 3,
- bytes, 5, 16, int_to_bytes(operstate))
- elif ifname:
- data = bytearray(40)
- bytes = ifname.encode("utf-8")
- struct.pack_into("HH4s", data, RTATTR_START_OFFSET, 8, 3, bytes)
- elif operstate:
- data = bytearray(40)
- struct.pack_into("HHc", data, RTATTR_START_OFFSET, 5, 16,
- int_to_bytes(operstate))
- struct.pack_into("=LHHLL", data, 0, len(data), msg_type, 0, 0, 0)
- return data
-
- def test_media_down_up_scenario(self, m_read_netlink_socket,
- m_socket):
- '''Test for media down up sequence for required interface name'''
- ifname = "eth0"
- # construct data for Oper State down
- data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN)
- # construct data for Oper State up
- data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP)
- m_read_netlink_socket.side_effect = [data_op_down, data_op_up]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 2)
-
- def test_wait_for_media_switch_diff_interface(self, m_read_netlink_socket,
- m_socket):
- '''wait_for_media_disconnect_connect ignores unexpected interfaces.
-
- The first two messages are for other interfaces and last two are for
- expected interface. So the function exit only after receiving last
- 2 messages and therefore the call count for m_read_netlink_socket
- has to be 4
- '''
- other_ifname = "eth1"
- expected_ifname = "eth0"
- data_op_down_eth1 = self._media_switch_data(
- other_ifname, RTM_NEWLINK, OPER_DOWN
- )
- data_op_up_eth1 = self._media_switch_data(
- other_ifname, RTM_NEWLINK, OPER_UP
- )
- data_op_down_eth0 = self._media_switch_data(
- expected_ifname, RTM_NEWLINK, OPER_DOWN
- )
- data_op_up_eth0 = self._media_switch_data(
- expected_ifname, RTM_NEWLINK, OPER_UP)
- m_read_netlink_socket.side_effect = [
- data_op_down_eth1,
- data_op_up_eth1,
- data_op_down_eth0,
- data_op_up_eth0
- ]
- wait_for_media_disconnect_connect(m_socket, expected_ifname)
- self.assertIn('Ignored netlink event on interface %s' % other_ifname,
- self.logs.getvalue())
- self.assertEqual(m_read_netlink_socket.call_count, 4)
-
- def test_invalid_msgtype_getlink(self, m_read_netlink_socket, m_socket):
- '''wait_for_media_disconnect_connect ignores GETLINK events.
-
- The first two messages are for oper down and up for RTM_GETLINK type
- which netlink module will ignore. The last 2 messages are RTM_NEWLINK
- with oper state down and up messages. Therefore the call count for
- m_read_netlink_socket has to be 4 ignoring first 2 messages
- of RTM_GETLINK
- '''
- ifname = "eth0"
- data_getlink_down = self._media_switch_data(
- ifname, RTM_GETLINK, OPER_DOWN
- )
- data_getlink_up = self._media_switch_data(
- ifname, RTM_GETLINK, OPER_UP
- )
- data_newlink_down = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_DOWN
- )
- data_newlink_up = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_UP
- )
- m_read_netlink_socket.side_effect = [
- data_getlink_down,
- data_getlink_up,
- data_newlink_down,
- data_newlink_up
- ]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 4)
-
- def test_invalid_msgtype_setlink(self, m_read_netlink_socket, m_socket):
- '''wait_for_media_disconnect_connect ignores SETLINK events.
-
- The first two messages are for oper down and up for RTM_GETLINK type
- which it will ignore. 3rd and 4th messages are RTM_NEWLINK with down
- and up messages. This function should exit after 4th messages since it
- sees down->up scenario. So the call count for m_read_netlink_socket
- has to be 4 ignoring first 2 messages of RTM_GETLINK and
- last 2 messages of RTM_NEWLINK
- '''
- ifname = "eth0"
- data_setlink_down = self._media_switch_data(
- ifname, RTM_SETLINK, OPER_DOWN
- )
- data_setlink_up = self._media_switch_data(
- ifname, RTM_SETLINK, OPER_UP
- )
- data_newlink_down = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_DOWN
- )
- data_newlink_up = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_UP
- )
- m_read_netlink_socket.side_effect = [
- data_setlink_down,
- data_setlink_up,
- data_newlink_down,
- data_newlink_up,
- data_newlink_down,
- data_newlink_up
- ]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 4)
-
- def test_netlink_invalid_switch_scenario(self, m_read_netlink_socket,
- m_socket):
- '''returns only if it receives UP event after a DOWN event'''
- ifname = "eth0"
- data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN)
- data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP)
- data_op_dormant = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_DORMANT
- )
- data_op_notpresent = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_NOTPRESENT
- )
- data_op_lowerdown = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_LOWERLAYERDOWN
- )
- data_op_testing = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_TESTING
- )
- data_op_unknown = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_UNKNOWN
- )
- m_read_netlink_socket.side_effect = [
- data_op_up, data_op_up,
- data_op_dormant, data_op_up,
- data_op_notpresent, data_op_up,
- data_op_lowerdown, data_op_up,
- data_op_testing, data_op_up,
- data_op_unknown, data_op_up,
- data_op_down, data_op_up
- ]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 14)
-
- def test_netlink_valid_inbetween_transitions(self, m_read_netlink_socket,
- m_socket):
- '''wait_for_media_disconnect_connect handles in between transitions'''
- ifname = "eth0"
- data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN)
- data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP)
- data_op_dormant = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_DORMANT)
- data_op_unknown = self._media_switch_data(
- ifname, RTM_NEWLINK, OPER_UNKNOWN)
- m_read_netlink_socket.side_effect = [
- data_op_down, data_op_dormant,
- data_op_unknown, data_op_up
- ]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 4)
-
- def test_netlink_invalid_operstate(self, m_read_netlink_socket, m_socket):
- '''wait_for_media_disconnect_connect should handle invalid operstates.
-
- The function should not fail and return even if it receives invalid
- operstates. It always should wait for down up sequence.
- '''
- ifname = "eth0"
- data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN)
- data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP)
- data_op_invalid = self._media_switch_data(ifname, RTM_NEWLINK, 7)
- m_read_netlink_socket.side_effect = [
- data_op_invalid, data_op_up,
- data_op_down, data_op_invalid,
- data_op_up
- ]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 5)
-
- def test_wait_invalid_socket(self, m_read_netlink_socket, m_socket):
- '''wait_for_media_disconnect_connect handle none netlink socket.'''
- socket = None
- ifname = "eth0"
- with self.assertRaises(AssertionError) as context:
- wait_for_media_disconnect_connect(socket, ifname)
- self.assertTrue('netlink socket is none' in str(context.exception))
-
- def test_wait_invalid_ifname(self, m_read_netlink_socket, m_socket):
- '''wait_for_media_disconnect_connect handle none interface name'''
- ifname = None
- with self.assertRaises(AssertionError) as context:
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertTrue('interface name is none' in str(context.exception))
- ifname = ""
- with self.assertRaises(AssertionError) as context:
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertTrue('interface name cannot be empty' in
- str(context.exception))
-
- def test_wait_invalid_rta_attr(self, m_read_netlink_socket, m_socket):
- ''' wait_for_media_disconnect_connect handles invalid rta data'''
- ifname = "eth0"
- data_invalid1 = self._media_switch_data(None, RTM_NEWLINK, OPER_DOWN)
- data_invalid2 = self._media_switch_data(ifname, RTM_NEWLINK, None)
- data_op_down = self._media_switch_data(ifname, RTM_NEWLINK, OPER_DOWN)
- data_op_up = self._media_switch_data(ifname, RTM_NEWLINK, OPER_UP)
- m_read_netlink_socket.side_effect = [
- data_invalid1, data_invalid2, data_op_down, data_op_up
- ]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 4)
-
- def test_read_multiple_netlink_msgs(self, m_read_netlink_socket, m_socket):
- '''Read multiple messages in single receive call'''
- ifname = "eth0"
- bytes = ifname.encode("utf-8")
- data = bytearray(96)
- struct.pack_into("=LHHLL", data, 0, 48, RTM_NEWLINK, 0, 0, 0)
- struct.pack_into(
- "HH4sHHc", data, RTATTR_START_OFFSET, 8, 3,
- bytes, 5, 16, int_to_bytes(OPER_DOWN)
- )
- struct.pack_into("=LHHLL", data, 48, 48, RTM_NEWLINK, 0, 0, 0)
- struct.pack_into(
- "HH4sHHc", data, 48 + RTATTR_START_OFFSET, 8,
- 3, bytes, 5, 16, int_to_bytes(OPER_UP)
- )
- m_read_netlink_socket.return_value = data
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 1)
-
- def test_read_partial_netlink_msgs(self, m_read_netlink_socket, m_socket):
- '''Read partial messages in receive call'''
- ifname = "eth0"
- bytes = ifname.encode("utf-8")
- data1 = bytearray(112)
- data2 = bytearray(32)
- struct.pack_into("=LHHLL", data1, 0, 48, RTM_NEWLINK, 0, 0, 0)
- struct.pack_into(
- "HH4sHHc", data1, RTATTR_START_OFFSET, 8, 3,
- bytes, 5, 16, int_to_bytes(OPER_DOWN)
- )
- struct.pack_into("=LHHLL", data1, 48, 48, RTM_NEWLINK, 0, 0, 0)
- struct.pack_into(
- "HH4sHHc", data1, 80, 8, 3, bytes, 5, 16, int_to_bytes(OPER_DOWN)
- )
- struct.pack_into("=LHHLL", data1, 96, 48, RTM_NEWLINK, 0, 0, 0)
- struct.pack_into(
- "HH4sHHc", data2, 16, 8, 3, bytes, 5, 16, int_to_bytes(OPER_UP)
- )
- m_read_netlink_socket.side_effect = [data1, data2]
- wait_for_media_disconnect_connect(m_socket, ifname)
- self.assertEqual(m_read_netlink_socket.call_count, 2)
diff --git a/cloudinit/sources/helpers/tests/test_openstack.py b/cloudinit/sources/helpers/tests/test_openstack.py
deleted file mode 100644
index 95fb9743..00000000
--- a/cloudinit/sources/helpers/tests/test_openstack.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-# ./cloudinit/sources/helpers/tests/test_openstack.py
-from unittest import mock
-
-from cloudinit.sources.helpers import openstack
-from cloudinit.tests import helpers as test_helpers
-
-
-@mock.patch(
- "cloudinit.net.is_openvswitch_internal_interface",
- mock.Mock(return_value=False)
-)
-class TestConvertNetJson(test_helpers.CiTestCase):
-
- def test_phy_types(self):
- """Verify the different known physical types are handled."""
- # network_data.json example from
- # https://docs.openstack.org/nova/latest/user/metadata.html
- mac0 = "fa:16:3e:9c:bf:3d"
- net_json = {
- "links": [
- {"ethernet_mac_address": mac0, "id": "tapcd9f6d46-4a",
- "mtu": None, "type": "bridge",
- "vif_id": "cd9f6d46-4a3a-43ab-a466-994af9db96fc"}
- ],
- "networks": [
- {"id": "network0", "link": "tapcd9f6d46-4a",
- "network_id": "99e88329-f20d-4741-9593-25bf07847b16",
- "type": "ipv4_dhcp"}
- ],
- "services": [{"address": "8.8.8.8", "type": "dns"}]
- }
- macs = {mac0: 'eth0'}
-
- expected = {
- 'version': 1,
- 'config': [
- {'mac_address': 'fa:16:3e:9c:bf:3d',
- 'mtu': None, 'name': 'eth0',
- 'subnets': [{'type': 'dhcp4'}],
- 'type': 'physical'},
- {'address': '8.8.8.8', 'type': 'nameserver'}]}
-
- for t in openstack.KNOWN_PHYSICAL_TYPES:
- net_json["links"][0]["type"] = t
- self.assertEqual(
- expected,
- openstack.convert_net_json(network_json=net_json,
- known_macs=macs))
diff --git a/cloudinit/sources/tests/__init__.py b/cloudinit/sources/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/cloudinit/sources/tests/__init__.py
+++ /dev/null
diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
deleted file mode 100644
index ae09cb17..00000000
--- a/cloudinit/sources/tests/test_init.py
+++ /dev/null
@@ -1,771 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import copy
-import inspect
-import os
-import stat
-
-from cloudinit.event import EventScope, EventType
-from cloudinit.helpers import Paths
-from cloudinit import importer
-from cloudinit.sources import (
- EXPERIMENTAL_TEXT, INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE,
- METADATA_UNKNOWN, REDACT_SENSITIVE_VALUE, UNSET, DataSource,
- canonical_cloud_id, redact_sensitive_keys)
-from cloudinit.tests.helpers import CiTestCase, mock
-from cloudinit.user_data import UserDataProcessor
-from cloudinit import util
-
-
-class DataSourceTestSubclassNet(DataSource):
-
- dsname = 'MyTestSubclass'
- url_max_wait = 55
-
- def __init__(self, sys_cfg, distro, paths, custom_metadata=None,
- custom_userdata=None, get_data_retval=True):
- super(DataSourceTestSubclassNet, self).__init__(
- sys_cfg, distro, paths)
- self._custom_userdata = custom_userdata
- self._custom_metadata = custom_metadata
- self._get_data_retval = get_data_retval
-
- def _get_cloud_name(self):
- return 'SubclassCloudName'
-
- def _get_data(self):
- if self._custom_metadata:
- self.metadata = self._custom_metadata
- else:
- self.metadata = {'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion'}
- if self._custom_userdata:
- self.userdata_raw = self._custom_userdata
- else:
- self.userdata_raw = 'userdata_raw'
- self.vendordata_raw = 'vendordata_raw'
- return self._get_data_retval
-
-
-class InvalidDataSourceTestSubclassNet(DataSource):
- pass
-
-
-class TestDataSource(CiTestCase):
-
- with_logs = True
- maxDiff = None
-
- def setUp(self):
- super(TestDataSource, self).setUp()
- self.sys_cfg = {'datasource': {'_undef': {'key1': False}}}
- self.distro = 'distrotest' # generally should be a Distro object
- self.paths = Paths({})
- self.datasource = DataSource(self.sys_cfg, self.distro, self.paths)
-
- def test_datasource_init(self):
- """DataSource initializes metadata attributes, ds_cfg and ud_proc."""
- self.assertEqual(self.paths, self.datasource.paths)
- self.assertEqual(self.sys_cfg, self.datasource.sys_cfg)
- self.assertEqual(self.distro, self.datasource.distro)
- self.assertIsNone(self.datasource.userdata)
- self.assertEqual({}, self.datasource.metadata)
- self.assertIsNone(self.datasource.userdata_raw)
- self.assertIsNone(self.datasource.vendordata)
- self.assertIsNone(self.datasource.vendordata_raw)
- self.assertEqual({'key1': False}, self.datasource.ds_cfg)
- self.assertIsInstance(self.datasource.ud_proc, UserDataProcessor)
-
- def test_datasource_init_gets_ds_cfg_using_dsname(self):
- """Init uses DataSource.dsname for sourcing ds_cfg."""
- sys_cfg = {'datasource': {'MyTestSubclass': {'key2': False}}}
- distro = 'distrotest' # generally should be a Distro object
- datasource = DataSourceTestSubclassNet(sys_cfg, distro, self.paths)
- self.assertEqual({'key2': False}, datasource.ds_cfg)
-
- def test_str_is_classname(self):
- """The string representation of the datasource is the classname."""
- self.assertEqual('DataSource', str(self.datasource))
- self.assertEqual(
- 'DataSourceTestSubclassNet',
- str(DataSourceTestSubclassNet('', '', self.paths)))
-
- def test_datasource_get_url_params_defaults(self):
- """get_url_params default url config settings for the datasource."""
- params = self.datasource.get_url_params()
- self.assertEqual(params.max_wait_seconds, self.datasource.url_max_wait)
- self.assertEqual(params.timeout_seconds, self.datasource.url_timeout)
- self.assertEqual(params.num_retries, self.datasource.url_retries)
- self.assertEqual(params.sec_between_retries,
- self.datasource.url_sec_between_retries)
-
- def test_datasource_get_url_params_subclassed(self):
- """Subclasses can override get_url_params defaults."""
- sys_cfg = {'datasource': {'MyTestSubclass': {'key2': False}}}
- distro = 'distrotest' # generally should be a Distro object
- datasource = DataSourceTestSubclassNet(sys_cfg, distro, self.paths)
- expected = (datasource.url_max_wait, datasource.url_timeout,
- datasource.url_retries, datasource.url_sec_between_retries)
- url_params = datasource.get_url_params()
- self.assertNotEqual(self.datasource.get_url_params(), url_params)
- self.assertEqual(expected, url_params)
-
- def test_datasource_get_url_params_ds_config_override(self):
- """Datasource configuration options can override url param defaults."""
- sys_cfg = {
- 'datasource': {
- 'MyTestSubclass': {
- 'max_wait': '1', 'timeout': '2',
- 'retries': '3', 'sec_between_retries': 4
- }}}
- datasource = DataSourceTestSubclassNet(
- sys_cfg, self.distro, self.paths)
- expected = (1, 2, 3, 4)
- url_params = datasource.get_url_params()
- self.assertNotEqual(
- (datasource.url_max_wait, datasource.url_timeout,
- datasource.url_retries, datasource.url_sec_between_retries),
- url_params)
- self.assertEqual(expected, url_params)
-
- def test_datasource_get_url_params_is_zero_or_greater(self):
- """get_url_params ignores timeouts with a value below 0."""
- # Set an override that is below 0 which gets ignored.
- sys_cfg = {'datasource': {'_undef': {'timeout': '-1'}}}
- datasource = DataSource(sys_cfg, self.distro, self.paths)
- (_max_wait, timeout, _retries,
- _sec_between_retries) = datasource.get_url_params()
- self.assertEqual(0, timeout)
-
- def test_datasource_get_url_uses_defaults_on_errors(self):
- """On invalid system config values for url_params defaults are used."""
- # All invalid values should be logged
- sys_cfg = {'datasource': {
- '_undef': {
- 'max_wait': 'nope', 'timeout': 'bug', 'retries': 'nonint'}}}
- datasource = DataSource(sys_cfg, self.distro, self.paths)
- url_params = datasource.get_url_params()
- expected = (datasource.url_max_wait, datasource.url_timeout,
- datasource.url_retries, datasource.url_sec_between_retries)
- self.assertEqual(expected, url_params)
- logs = self.logs.getvalue()
- expected_logs = [
- "Config max_wait 'nope' is not an int, using default '-1'",
- "Config timeout 'bug' is not an int, using default '10'",
- "Config retries 'nonint' is not an int, using default '5'",
- ]
- for log in expected_logs:
- self.assertIn(log, logs)
-
- @mock.patch('cloudinit.sources.net.find_fallback_nic')
- def test_fallback_interface_is_discovered(self, m_get_fallback_nic):
- """The fallback_interface is discovered via find_fallback_nic."""
- m_get_fallback_nic.return_value = 'nic9'
- self.assertEqual('nic9', self.datasource.fallback_interface)
-
- @mock.patch('cloudinit.sources.net.find_fallback_nic')
- def test_fallback_interface_logs_undiscovered(self, m_get_fallback_nic):
- """Log a warning when fallback_interface can not discover the nic."""
- self.datasource._cloud_name = 'MySupahCloud'
- m_get_fallback_nic.return_value = None # Couldn't discover nic
- self.assertIsNone(self.datasource.fallback_interface)
- self.assertEqual(
- 'WARNING: Did not find a fallback interface on MySupahCloud.\n',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.sources.net.find_fallback_nic')
- def test_wb_fallback_interface_is_cached(self, m_get_fallback_nic):
- """The fallback_interface is cached and won't be rediscovered."""
- self.datasource._fallback_interface = 'nic10'
- self.assertEqual('nic10', self.datasource.fallback_interface)
- m_get_fallback_nic.assert_not_called()
-
- def test__get_data_unimplemented(self):
- """Raise an error when _get_data is not implemented."""
- with self.assertRaises(NotImplementedError) as context_manager:
- self.datasource.get_data()
- self.assertIn(
- 'Subclasses of DataSource must implement _get_data',
- str(context_manager.exception))
- datasource2 = InvalidDataSourceTestSubclassNet(
- self.sys_cfg, self.distro, self.paths)
- with self.assertRaises(NotImplementedError) as context_manager:
- datasource2.get_data()
- self.assertIn(
- 'Subclasses of DataSource must implement _get_data',
- str(context_manager.exception))
-
- def test_get_data_calls_subclass__get_data(self):
- """Datasource.get_data uses the subclass' version of _get_data."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- self.assertTrue(datasource.get_data())
- self.assertEqual(
- {'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion'},
- datasource.metadata)
- self.assertEqual('userdata_raw', datasource.userdata_raw)
- self.assertEqual('vendordata_raw', datasource.vendordata_raw)
-
- def test_get_hostname_strips_local_hostname_without_domain(self):
- """Datasource.get_hostname strips metadata local-hostname of domain."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- self.assertTrue(datasource.get_data())
- self.assertEqual(
- 'test-subclass-hostname', datasource.metadata['local-hostname'])
- self.assertEqual('test-subclass-hostname', datasource.get_hostname())
- datasource.metadata['local-hostname'] = 'hostname.my.domain.com'
- self.assertEqual('hostname', datasource.get_hostname())
-
- def test_get_hostname_with_fqdn_returns_local_hostname_with_domain(self):
- """Datasource.get_hostname with fqdn set gets qualified hostname."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- self.assertTrue(datasource.get_data())
- datasource.metadata['local-hostname'] = 'hostname.my.domain.com'
- self.assertEqual(
- 'hostname.my.domain.com', datasource.get_hostname(fqdn=True))
-
- def test_get_hostname_without_metadata_uses_system_hostname(self):
- """Datasource.gethostname runs util.get_hostname when no metadata."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- self.assertEqual({}, datasource.metadata)
- mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts'
- with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost:
- with mock.patch(mock_fqdn) as m_fqdn:
- m_gethost.return_value = 'systemhostname.domain.com'
- m_fqdn.return_value = None # No maching fqdn in /etc/hosts
- self.assertEqual('systemhostname', datasource.get_hostname())
- self.assertEqual(
- 'systemhostname.domain.com',
- datasource.get_hostname(fqdn=True))
-
- def test_get_hostname_without_metadata_returns_none(self):
- """Datasource.gethostname returns None when metadata_only and no MD."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- self.assertEqual({}, datasource.metadata)
- mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts'
- with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost:
- with mock.patch(mock_fqdn) as m_fqdn:
- self.assertIsNone(datasource.get_hostname(metadata_only=True))
- self.assertIsNone(
- datasource.get_hostname(fqdn=True, metadata_only=True))
- self.assertEqual([], m_gethost.call_args_list)
- self.assertEqual([], m_fqdn.call_args_list)
-
- def test_get_hostname_without_metadata_prefers_etc_hosts(self):
- """Datasource.gethostname prefers /etc/hosts to util.get_hostname."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- self.assertEqual({}, datasource.metadata)
- mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts'
- with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost:
- with mock.patch(mock_fqdn) as m_fqdn:
- m_gethost.return_value = 'systemhostname.domain.com'
- m_fqdn.return_value = 'fqdnhostname.domain.com'
- self.assertEqual('fqdnhostname', datasource.get_hostname())
- self.assertEqual('fqdnhostname.domain.com',
- datasource.get_hostname(fqdn=True))
-
- def test_get_data_does_not_write_instance_data_on_failure(self):
- """get_data does not write INSTANCE_JSON_FILE on get_data False."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
- get_data_retval=False)
- self.assertFalse(datasource.get_data())
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- self.assertFalse(
- os.path.exists(json_file), 'Found unexpected file %s' % json_file)
-
- def test_get_data_writes_json_instance_data_on_success(self):
- """get_data writes INSTANCE_JSON_FILE to run_dir as world readable."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- sys_info = {
- "python": "3.7",
- "platform":
- "Linux-5.4.0-24-generic-x86_64-with-Ubuntu-20.04-focal",
- "uname": ["Linux", "myhost", "5.4.0-24-generic", "SMP blah",
- "x86_64"],
- "variant": "ubuntu", "dist": ["ubuntu", "20.04", "focal"]}
- with mock.patch("cloudinit.util.system_info", return_value=sys_info):
- datasource.get_data()
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- content = util.load_file(json_file)
- expected = {
- 'base64_encoded_keys': [],
- 'merged_cfg': REDACT_SENSITIVE_VALUE,
- 'sensitive_keys': ['merged_cfg'],
- 'sys_info': sys_info,
- 'v1': {
- '_beta_keys': ['subplatform'],
- 'availability-zone': 'myaz',
- 'availability_zone': 'myaz',
- 'cloud-name': 'subclasscloudname',
- 'cloud_name': 'subclasscloudname',
- 'distro': 'ubuntu',
- 'distro_release': 'focal',
- 'distro_version': '20.04',
- 'instance-id': 'iid-datasource',
- 'instance_id': 'iid-datasource',
- 'local-hostname': 'test-subclass-hostname',
- 'local_hostname': 'test-subclass-hostname',
- 'kernel_release': '5.4.0-24-generic',
- 'machine': 'x86_64',
- 'platform': 'mytestsubclass',
- 'public_ssh_keys': [],
- 'python_version': '3.7',
- 'region': 'myregion',
- 'system_platform':
- 'Linux-5.4.0-24-generic-x86_64-with-Ubuntu-20.04-focal',
- 'subplatform': 'unknown',
- 'variant': 'ubuntu'},
- 'ds': {
-
- '_doc': EXPERIMENTAL_TEXT,
- 'meta_data': {'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion'}}}
- self.assertEqual(expected, util.load_json(content))
- file_stat = os.stat(json_file)
- self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode))
- self.assertEqual(expected, util.load_json(content))
-
- def test_get_data_writes_redacted_public_json_instance_data(self):
- """get_data writes redacted content to public INSTANCE_JSON_FILE."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
- custom_metadata={
- 'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion',
- 'some': {'security-credentials': {
- 'cred1': 'sekret', 'cred2': 'othersekret'}}})
- self.assertCountEqual(
- ('merged_cfg', 'security-credentials',),
- datasource.sensitive_metadata_keys)
- sys_info = {
- "python": "3.7",
- "platform":
- "Linux-5.4.0-24-generic-x86_64-with-Ubuntu-20.04-focal",
- "uname": ["Linux", "myhost", "5.4.0-24-generic", "SMP blah",
- "x86_64"],
- "variant": "ubuntu", "dist": ["ubuntu", "20.04", "focal"]}
- with mock.patch("cloudinit.util.system_info", return_value=sys_info):
- datasource.get_data()
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- redacted = util.load_json(util.load_file(json_file))
- expected = {
- 'base64_encoded_keys': [],
- 'merged_cfg': REDACT_SENSITIVE_VALUE,
- 'sensitive_keys': [
- 'ds/meta_data/some/security-credentials', 'merged_cfg'],
- 'sys_info': sys_info,
- 'v1': {
- '_beta_keys': ['subplatform'],
- 'availability-zone': 'myaz',
- 'availability_zone': 'myaz',
- 'cloud-name': 'subclasscloudname',
- 'cloud_name': 'subclasscloudname',
- 'distro': 'ubuntu',
- 'distro_release': 'focal',
- 'distro_version': '20.04',
- 'instance-id': 'iid-datasource',
- 'instance_id': 'iid-datasource',
- 'local-hostname': 'test-subclass-hostname',
- 'local_hostname': 'test-subclass-hostname',
- 'kernel_release': '5.4.0-24-generic',
- 'machine': 'x86_64',
- 'platform': 'mytestsubclass',
- 'public_ssh_keys': [],
- 'python_version': '3.7',
- 'region': 'myregion',
- 'system_platform':
- 'Linux-5.4.0-24-generic-x86_64-with-Ubuntu-20.04-focal',
- 'subplatform': 'unknown',
- 'variant': 'ubuntu'},
- 'ds': {
- '_doc': EXPERIMENTAL_TEXT,
- 'meta_data': {
- 'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion',
- 'some': {'security-credentials': REDACT_SENSITIVE_VALUE}}}
- }
- self.assertCountEqual(expected, redacted)
- file_stat = os.stat(json_file)
- self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode))
-
- def test_get_data_writes_json_instance_data_sensitive(self):
- """
- get_data writes unmodified data to sensitive file as root-readonly.
- """
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
- custom_metadata={
- 'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion',
- 'some': {'security-credentials': {
- 'cred1': 'sekret', 'cred2': 'othersekret'}}})
- sys_info = {
- "python": "3.7",
- "platform":
- "Linux-5.4.0-24-generic-x86_64-with-Ubuntu-20.04-focal",
- "uname": ["Linux", "myhost", "5.4.0-24-generic", "SMP blah",
- "x86_64"],
- "variant": "ubuntu", "dist": ["ubuntu", "20.04", "focal"]}
-
- self.assertCountEqual(
- ('merged_cfg', 'security-credentials',),
- datasource.sensitive_metadata_keys)
- with mock.patch("cloudinit.util.system_info", return_value=sys_info):
- datasource.get_data()
- sensitive_json_file = self.tmp_path(INSTANCE_JSON_SENSITIVE_FILE, tmp)
- content = util.load_file(sensitive_json_file)
- expected = {
- 'base64_encoded_keys': [],
- 'merged_cfg': {
- '_doc': (
- 'Merged cloud-init system config from '
- '/etc/cloud/cloud.cfg and /etc/cloud/cloud.cfg.d/'
- ),
- 'datasource': {'_undef': {'key1': False}}},
- 'sensitive_keys': [
- 'ds/meta_data/some/security-credentials', 'merged_cfg'],
- 'sys_info': sys_info,
- 'v1': {
- '_beta_keys': ['subplatform'],
- 'availability-zone': 'myaz',
- 'availability_zone': 'myaz',
- 'cloud-name': 'subclasscloudname',
- 'cloud_name': 'subclasscloudname',
- 'distro': 'ubuntu',
- 'distro_release': 'focal',
- 'distro_version': '20.04',
- 'instance-id': 'iid-datasource',
- 'instance_id': 'iid-datasource',
- 'kernel_release': '5.4.0-24-generic',
- 'local-hostname': 'test-subclass-hostname',
- 'local_hostname': 'test-subclass-hostname',
- 'machine': 'x86_64',
- 'platform': 'mytestsubclass',
- 'public_ssh_keys': [],
- 'python_version': '3.7',
- 'region': 'myregion',
- 'subplatform': 'unknown',
- 'system_platform':
- 'Linux-5.4.0-24-generic-x86_64-with-Ubuntu-20.04-focal',
- 'variant': 'ubuntu'},
- 'ds': {
- '_doc': EXPERIMENTAL_TEXT,
- 'meta_data': {
- 'availability_zone': 'myaz',
- 'local-hostname': 'test-subclass-hostname',
- 'region': 'myregion',
- 'some': {
- 'security-credentials':
- {'cred1': 'sekret', 'cred2': 'othersekret'}}}}
- }
- self.assertCountEqual(expected, util.load_json(content))
- file_stat = os.stat(sensitive_json_file)
- self.assertEqual(0o600, stat.S_IMODE(file_stat.st_mode))
- self.assertEqual(expected, util.load_json(content))
-
- def test_get_data_handles_redacted_unserializable_content(self):
- """get_data warns unserializable content in INSTANCE_JSON_FILE."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
- custom_metadata={'key1': 'val1', 'key2': {'key2.1': self.paths}})
- datasource.get_data()
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- content = util.load_file(json_file)
- expected_metadata = {
- 'key1': 'val1',
- 'key2': {
- 'key2.1': "Warning: redacted unserializable type <class"
- " 'cloudinit.helpers.Paths'>"}}
- instance_json = util.load_json(content)
- self.assertEqual(
- expected_metadata, instance_json['ds']['meta_data'])
-
- def test_persist_instance_data_writes_ec2_metadata_when_set(self):
- """When ec2_metadata class attribute is set, persist to json."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- datasource.ec2_metadata = UNSET
- datasource.get_data()
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- instance_data = util.load_json(util.load_file(json_file))
- self.assertNotIn('ec2_metadata', instance_data['ds'])
- datasource.ec2_metadata = {'ec2stuff': 'is good'}
- datasource.persist_instance_data()
- instance_data = util.load_json(util.load_file(json_file))
- self.assertEqual(
- {'ec2stuff': 'is good'},
- instance_data['ds']['ec2_metadata'])
-
- def test_persist_instance_data_writes_network_json_when_set(self):
- """When network_data.json class attribute is set, persist to json."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
- datasource.get_data()
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- instance_data = util.load_json(util.load_file(json_file))
- self.assertNotIn('network_json', instance_data['ds'])
- datasource.network_json = {'network_json': 'is good'}
- datasource.persist_instance_data()
- instance_data = util.load_json(util.load_file(json_file))
- self.assertEqual(
- {'network_json': 'is good'},
- instance_data['ds']['network_json'])
-
- def test_get_data_base64encodes_unserializable_bytes(self):
- """On py3, get_data base64encodes any unserializable content."""
- tmp = self.tmp_dir()
- datasource = DataSourceTestSubclassNet(
- self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
- custom_metadata={'key1': 'val1', 'key2': {'key2.1': b'\x123'}})
- self.assertTrue(datasource.get_data())
- json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
- content = util.load_file(json_file)
- instance_json = util.load_json(content)
- self.assertCountEqual(
- ['ds/meta_data/key2/key2.1'],
- instance_json['base64_encoded_keys'])
- self.assertEqual(
- {'key1': 'val1', 'key2': {'key2.1': 'EjM='}},
- instance_json['ds']['meta_data'])
-
- def test_get_hostname_subclass_support(self):
- """Validate get_hostname signature on all subclasses of DataSource."""
- base_args = inspect.getfullargspec(DataSource.get_hostname)
- # Import all DataSource subclasses so we can inspect them.
- modules = util.find_modules(os.path.dirname(os.path.dirname(__file__)))
- for _loc, name in modules.items():
- mod_locs, _ = importer.find_module(name, ['cloudinit.sources'], [])
- if mod_locs:
- importer.import_module(mod_locs[0])
- for child in DataSource.__subclasses__():
- if 'Test' in child.dsname:
- continue
- self.assertEqual(
- base_args,
- inspect.getfullargspec(child.get_hostname),
- '%s does not implement DataSource.get_hostname params'
- % child)
- for grandchild in child.__subclasses__():
- self.assertEqual(
- base_args,
- inspect.getfullargspec(grandchild.get_hostname),
- '%s does not implement DataSource.get_hostname params'
- % grandchild)
-
- def test_clear_cached_attrs_resets_cached_attr_class_attributes(self):
- """Class attributes listed in cached_attr_defaults are reset."""
- count = 0
- # Setup values for all cached class attributes
- for attr, value in self.datasource.cached_attr_defaults:
- setattr(self.datasource, attr, count)
- count += 1
- self.datasource._dirty_cache = True
- self.datasource.clear_cached_attrs()
- for attr, value in self.datasource.cached_attr_defaults:
- self.assertEqual(value, getattr(self.datasource, attr))
-
- def test_clear_cached_attrs_noops_on_clean_cache(self):
- """Class attributes listed in cached_attr_defaults are reset."""
- count = 0
- # Setup values for all cached class attributes
- for attr, _ in self.datasource.cached_attr_defaults:
- setattr(self.datasource, attr, count)
- count += 1
- self.datasource._dirty_cache = False # Fake clean cache
- self.datasource.clear_cached_attrs()
- count = 0
- for attr, _ in self.datasource.cached_attr_defaults:
- self.assertEqual(count, getattr(self.datasource, attr))
- count += 1
-
- def test_clear_cached_attrs_skips_non_attr_class_attributes(self):
- """Skip any cached_attr_defaults which aren't class attributes."""
- self.datasource._dirty_cache = True
- self.datasource.clear_cached_attrs()
- for attr in ('ec2_metadata', 'network_json'):
- self.assertFalse(hasattr(self.datasource, attr))
-
- def test_clear_cached_attrs_of_custom_attrs(self):
- """Custom attr_values can be passed to clear_cached_attrs."""
- self.datasource._dirty_cache = True
- cached_attr_name = self.datasource.cached_attr_defaults[0][0]
- setattr(self.datasource, cached_attr_name, 'himom')
- self.datasource.myattr = 'orig'
- self.datasource.clear_cached_attrs(
- attr_defaults=(('myattr', 'updated'),))
- self.assertEqual('himom', getattr(self.datasource, cached_attr_name))
- self.assertEqual('updated', self.datasource.myattr)
-
- @mock.patch.dict(DataSource.default_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}})
- @mock.patch.dict(DataSource.supported_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}})
- def test_update_metadata_only_acts_on_supported_update_events(self):
- """update_metadata_if_supported wont get_data on unsupported events."""
- self.assertEqual(
- {EventScope.NETWORK: set([EventType.BOOT_NEW_INSTANCE])},
- self.datasource.default_update_events
- )
-
- def fake_get_data():
- raise Exception('get_data should not be called')
-
- self.datasource.get_data = fake_get_data
- self.assertFalse(
- self.datasource.update_metadata_if_supported(
- source_event_types=[EventType.BOOT]))
-
- @mock.patch.dict(DataSource.supported_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}})
- def test_update_metadata_returns_true_on_supported_update_event(self):
- """update_metadata_if_supported returns get_data on supported events"""
- def fake_get_data():
- return True
-
- self.datasource.get_data = fake_get_data
- self.datasource._network_config = 'something'
- self.datasource._dirty_cache = True
- self.assertTrue(
- self.datasource.update_metadata_if_supported(
- source_event_types=[
- EventType.BOOT, EventType.BOOT_NEW_INSTANCE]))
- self.assertEqual(UNSET, self.datasource._network_config)
-
- self.assertIn(
- "DEBUG: Update datasource metadata and network config due to"
- " events: boot-new-instance",
- self.logs.getvalue()
- )
-
-
-class TestRedactSensitiveData(CiTestCase):
-
- def test_redact_sensitive_data_noop_when_no_sensitive_keys_present(self):
- """When sensitive_keys is absent or empty from metadata do nothing."""
- md = {'my': 'data'}
- self.assertEqual(
- md, redact_sensitive_keys(md, redact_value='redacted'))
- md['sensitive_keys'] = []
- self.assertEqual(
- md, redact_sensitive_keys(md, redact_value='redacted'))
-
- def test_redact_sensitive_data_redacts_exact_match_name(self):
- """Only exact matched sensitive_keys are redacted from metadata."""
- md = {'sensitive_keys': ['md/secure'],
- 'md': {'secure': 's3kr1t', 'insecure': 'publik'}}
- secure_md = copy.deepcopy(md)
- secure_md['md']['secure'] = 'redacted'
- self.assertEqual(
- secure_md,
- redact_sensitive_keys(md, redact_value='redacted'))
-
- def test_redact_sensitive_data_does_redacts_with_default_string(self):
- """When redact_value is absent, REDACT_SENSITIVE_VALUE is used."""
- md = {'sensitive_keys': ['md/secure'],
- 'md': {'secure': 's3kr1t', 'insecure': 'publik'}}
- secure_md = copy.deepcopy(md)
- secure_md['md']['secure'] = 'redacted for non-root user'
- self.assertEqual(
- secure_md,
- redact_sensitive_keys(md))
-
-
-class TestCanonicalCloudID(CiTestCase):
-
- def test_cloud_id_returns_platform_on_unknowns(self):
- """When region and cloud_name are unknown, return platform."""
- self.assertEqual(
- 'platform',
- canonical_cloud_id(cloud_name=METADATA_UNKNOWN,
- region=METADATA_UNKNOWN,
- platform='platform'))
-
- def test_cloud_id_returns_platform_on_none(self):
- """When region and cloud_name are unknown, return platform."""
- self.assertEqual(
- 'platform',
- canonical_cloud_id(cloud_name=None,
- region=None,
- platform='platform'))
-
- def test_cloud_id_returns_cloud_name_on_unknown_region(self):
- """When region is unknown, return cloud_name."""
- for region in (None, METADATA_UNKNOWN):
- self.assertEqual(
- 'cloudname',
- canonical_cloud_id(cloud_name='cloudname',
- region=region,
- platform='platform'))
-
- def test_cloud_id_returns_platform_on_unknown_cloud_name(self):
- """When region is set but cloud_name is unknown return cloud_name."""
- self.assertEqual(
- 'platform',
- canonical_cloud_id(cloud_name=METADATA_UNKNOWN,
- region='region',
- platform='platform'))
-
- def test_cloud_id_aws_based_on_region_and_cloud_name(self):
- """When cloud_name is aws, return proper cloud-id based on region."""
- self.assertEqual(
- 'aws-china',
- canonical_cloud_id(cloud_name='aws',
- region='cn-north-1',
- platform='platform'))
- self.assertEqual(
- 'aws',
- canonical_cloud_id(cloud_name='aws',
- region='us-east-1',
- platform='platform'))
- self.assertEqual(
- 'aws-gov',
- canonical_cloud_id(cloud_name='aws',
- region='us-gov-1',
- platform='platform'))
- self.assertEqual( # Overrideen non-aws cloud_name is returned
- '!aws',
- canonical_cloud_id(cloud_name='!aws',
- region='us-gov-1',
- platform='platform'))
-
- def test_cloud_id_azure_based_on_region_and_cloud_name(self):
- """Report cloud-id when cloud_name is azure and region is in china."""
- self.assertEqual(
- 'azure-china',
- canonical_cloud_id(cloud_name='azure',
- region='chinaeast',
- platform='platform'))
- self.assertEqual(
- 'azure',
- canonical_cloud_id(cloud_name='azure',
- region='!chinaeast',
- platform='platform'))
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/tests/test_lxd.py b/cloudinit/sources/tests/test_lxd.py
deleted file mode 100644
index a6e51f3b..00000000
--- a/cloudinit/sources/tests/test_lxd.py
+++ /dev/null
@@ -1,376 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from collections import namedtuple
-from copy import deepcopy
-import json
-import re
-import stat
-from unittest import mock
-import yaml
-
-import pytest
-
-from cloudinit.sources import (
- DataSourceLXD as lxd, InvalidMetaDataException, UNSET
-)
-DS_PATH = "cloudinit.sources.DataSourceLXD."
-
-
-LStatResponse = namedtuple("lstatresponse", "st_mode")
-
-
-NETWORK_V1 = {
- "version": 1,
- "config": [
- {
- "type": "physical", "name": "eth0",
- "subnets": [{"type": "dhcp", "control": "auto"}]
- }
- ]
-}
-
-
-def _add_network_v1_device(devname) -> dict:
- """Helper to inject device name into default network v1 config."""
- network_cfg = deepcopy(NETWORK_V1)
- network_cfg["config"][0]["name"] = devname
- return network_cfg
-
-
-LXD_V1_METADATA = {
- "meta-data": "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n",
- "network-config": NETWORK_V1,
- "user-data": "#cloud-config\npackages: [sl]\n",
- "vendor-data": "#cloud-config\nruncmd: ['echo vendor-data']\n",
- "config": {
- "user.user-data":
- "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n",
- "user.vendor-data":
- "#cloud-config\nruncmd: ['echo vendor-data']\n",
- "user.network-config": yaml.safe_dump(NETWORK_V1),
- }
-}
-
-
-@pytest.fixture
-def lxd_metadata():
- return LXD_V1_METADATA
-
-
-@pytest.yield_fixture
-def lxd_ds(request, paths, lxd_metadata):
- """
- Return an instantiated DataSourceLXD.
-
- This also performs the mocking required for the default test case:
- * ``is_platform_viable`` returns True,
- * ``read_metadata`` returns ``LXD_V1_METADATA``
-
- (This uses the paths fixture for the required helpers.Paths object)
- """
- with mock.patch(DS_PATH + "is_platform_viable", return_value=True):
- with mock.patch(DS_PATH + "read_metadata", return_value=lxd_metadata):
- yield lxd.DataSourceLXD(
- sys_cfg={}, distro=mock.Mock(), paths=paths
- )
-
-
-class TestGenerateFallbackNetworkConfig:
-
- @pytest.mark.parametrize(
- "uname_machine,systemd_detect_virt,expected", (
- # None for systemd_detect_virt returns None from which
- ({}, None, NETWORK_V1),
- ({}, None, NETWORK_V1),
- ("anything", "lxc\n", NETWORK_V1),
- # `uname -m` on kvm determines devname
- ("x86_64", "kvm\n", _add_network_v1_device("enp5s0")),
- ("ppc64le", "kvm\n", _add_network_v1_device("enp0s5")),
- ("s390x", "kvm\n", _add_network_v1_device("enc9"))
- )
- )
- @mock.patch(DS_PATH + "util.system_info")
- @mock.patch(DS_PATH + "subp.subp")
- @mock.patch(DS_PATH + "subp.which")
- def test_net_v2_based_on_network_mode_virt_type_and_uname_machine(
- self,
- m_which,
- m_subp,
- m_system_info,
- uname_machine,
- systemd_detect_virt,
- expected,
- ):
- """Return network config v2 based on uname -m, systemd-detect-virt."""
- if systemd_detect_virt is None:
- m_which.return_value = None
- m_system_info.return_value = {"uname": ["", "", "", "", uname_machine]}
- m_subp.return_value = (systemd_detect_virt, "")
- assert expected == lxd.generate_fallback_network_config()
- if systemd_detect_virt is None:
- assert 0 == m_subp.call_count
- assert 0 == m_system_info.call_count
- else:
- assert [
- mock.call(["systemd-detect-virt"])
- ] == m_subp.call_args_list
- if systemd_detect_virt != "kvm\n":
- assert 0 == m_system_info.call_count
- else:
- assert 1 == m_system_info.call_count
-
-
-class TestDataSourceLXD:
- def test_platform_info(self, lxd_ds):
- assert "LXD" == lxd_ds.dsname
- assert "lxd" == lxd_ds.cloud_name
- assert "lxd" == lxd_ds.platform_type
-
- def test_subplatform(self, lxd_ds):
- assert "LXD socket API v. 1.0 (/dev/lxd/sock)" == lxd_ds.subplatform
-
- def test__get_data(self, lxd_ds):
- """get_data calls read_metadata, setting appropiate instance attrs."""
- assert UNSET == lxd_ds._crawled_metadata
- assert UNSET == lxd_ds._network_config
- assert None is lxd_ds.userdata_raw
- assert True is lxd_ds._get_data()
- assert LXD_V1_METADATA == lxd_ds._crawled_metadata
- # network-config is dumped from YAML
- assert NETWORK_V1 == lxd_ds._network_config
- # Any user-data and vendor-data are saved as raw
- assert LXD_V1_METADATA["user-data"] == lxd_ds.userdata_raw
- assert LXD_V1_METADATA["vendor-data"] == lxd_ds.vendordata_raw
-
-
-class TestIsPlatformViable:
- @pytest.mark.parametrize(
- "exists,lstat_mode,expected", (
- (False, None, False),
- (True, stat.S_IFREG, False),
- (True, stat.S_IFSOCK, True),
- )
- )
- @mock.patch(DS_PATH + "os.lstat")
- @mock.patch(DS_PATH + "os.path.exists")
- def test_expected_viable(
- self, m_exists, m_lstat, exists, lstat_mode, expected
- ):
- """Return True only when LXD_SOCKET_PATH exists and is a socket."""
- m_exists.return_value = exists
- m_lstat.return_value = LStatResponse(lstat_mode)
- assert expected is lxd.is_platform_viable()
- m_exists.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)])
- if exists:
- m_lstat.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)])
- else:
- assert 0 == m_lstat.call_count
-
-
-class TestReadMetadata:
- @pytest.mark.parametrize(
- "url_responses,expected,logs", (
- ( # Assert non-JSON format from config route
- {
- "http://lxd/1.0/meta-data": "local-hostname: md\n",
- "http://lxd/1.0/config": "[NOT_JSON",
- },
- InvalidMetaDataException(
- "Unable to determine cloud-init config from"
- " http://lxd/1.0/config. Expected JSON but found:"
- " [NOT_JSON"),
- ["[GET] [HTTP:200] http://lxd/1.0/meta-data",
- "[GET] [HTTP:200] http://lxd/1.0/config"],
- ),
- ( # Assert success on just meta-data
- {
- "http://lxd/1.0/meta-data": "local-hostname: md\n",
- "http://lxd/1.0/config": "[]",
- },
- {
- "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION,
- "config": {}, "meta-data": "local-hostname: md\n"
- },
- ["[GET] [HTTP:200] http://lxd/1.0/meta-data",
- "[GET] [HTTP:200] http://lxd/1.0/config"],
- ),
- ( # Assert 404s for config routes log skipping
- {
- "http://lxd/1.0/meta-data": "local-hostname: md\n",
- "http://lxd/1.0/config":
- '["/1.0/config/user.custom1",'
- ' "/1.0/config/user.meta-data",'
- ' "/1.0/config/user.network-config",'
- ' "/1.0/config/user.user-data",'
- ' "/1.0/config/user.vendor-data"]',
- "http://lxd/1.0/config/user.custom1": "custom1",
- "http://lxd/1.0/config/user.meta-data": "", # 404
- "http://lxd/1.0/config/user.network-config": "net-config",
- "http://lxd/1.0/config/user.user-data": "", # 404
- "http://lxd/1.0/config/user.vendor-data": "", # 404
- },
- {
- "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION,
- "config": {
- "user.custom1": "custom1", # Not promoted
- "user.network-config": "net-config",
- },
- "meta-data": "local-hostname: md\n",
- "network-config": "net-config",
- },
- [
- "Skipping http://lxd/1.0/config/user.vendor-data on"
- " [HTTP:404]",
- "Skipping http://lxd/1.0/config/user.meta-data on"
- " [HTTP:404]",
- "Skipping http://lxd/1.0/config/user.user-data on"
- " [HTTP:404]",
- "[GET] [HTTP:200] http://lxd/1.0/config",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.custom1",
- "[GET] [HTTP:200]"
- " http://lxd/1.0/config/user.network-config",
- ],
- ),
- ( # Assert all CONFIG_KEY_ALIASES promoted to top-level keys
- {
- "http://lxd/1.0/meta-data": "local-hostname: md\n",
- "http://lxd/1.0/config":
- '["/1.0/config/user.custom1",'
- ' "/1.0/config/user.meta-data",'
- ' "/1.0/config/user.network-config",'
- ' "/1.0/config/user.user-data",'
- ' "/1.0/config/user.vendor-data"]',
- "http://lxd/1.0/config/user.custom1": "custom1",
- "http://lxd/1.0/config/user.meta-data": "meta-data",
- "http://lxd/1.0/config/user.network-config": "net-config",
- "http://lxd/1.0/config/user.user-data": "user-data",
- "http://lxd/1.0/config/user.vendor-data": "vendor-data",
- },
- {
- "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION,
- "config": {
- "user.custom1": "custom1", # Not promoted
- "user.meta-data": "meta-data",
- "user.network-config": "net-config",
- "user.user-data": "user-data",
- "user.vendor-data": "vendor-data",
- },
- "meta-data": "local-hostname: md\n",
- "network-config": "net-config",
- "user-data": "user-data",
- "vendor-data": "vendor-data",
- },
- [
- "[GET] [HTTP:200] http://lxd/1.0/meta-data",
- "[GET] [HTTP:200] http://lxd/1.0/config",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.custom1",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.meta-data",
- "[GET] [HTTP:200]"
- " http://lxd/1.0/config/user.network-config",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.user-data",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.vendor-data",
- ],
- ),
- ( # Assert cloud-init.* config key values prefered over user.*
- {
- "http://lxd/1.0/meta-data": "local-hostname: md\n",
- "http://lxd/1.0/config":
- '["/1.0/config/user.meta-data",'
- ' "/1.0/config/user.network-config",'
- ' "/1.0/config/user.user-data",'
- ' "/1.0/config/user.vendor-data",'
- ' "/1.0/config/cloud-init.network-config",'
- ' "/1.0/config/cloud-init.user-data",'
- ' "/1.0/config/cloud-init.vendor-data"]',
- "http://lxd/1.0/config/user.meta-data": "user.meta-data",
- "http://lxd/1.0/config/user.network-config":
- "user.network-config",
- "http://lxd/1.0/config/user.user-data": "user.user-data",
- "http://lxd/1.0/config/user.vendor-data":
- "user.vendor-data",
- "http://lxd/1.0/config/cloud-init.meta-data":
- "cloud-init.meta-data",
- "http://lxd/1.0/config/cloud-init.network-config":
- "cloud-init.network-config",
- "http://lxd/1.0/config/cloud-init.user-data":
- "cloud-init.user-data",
- "http://lxd/1.0/config/cloud-init.vendor-data":
- "cloud-init.vendor-data",
- },
- {
- "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION,
- "config": {
- "user.meta-data": "user.meta-data",
- "user.network-config": "user.network-config",
- "user.user-data": "user.user-data",
- "user.vendor-data": "user.vendor-data",
- "cloud-init.network-config":
- "cloud-init.network-config",
- "cloud-init.user-data": "cloud-init.user-data",
- "cloud-init.vendor-data":
- "cloud-init.vendor-data",
- },
- "meta-data": "local-hostname: md\n",
- "network-config": "cloud-init.network-config",
- "user-data": "cloud-init.user-data",
- "vendor-data": "cloud-init.vendor-data",
- },
- [
- "[GET] [HTTP:200] http://lxd/1.0/meta-data",
- "[GET] [HTTP:200] http://lxd/1.0/config",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.meta-data",
- "[GET] [HTTP:200]"
- " http://lxd/1.0/config/user.network-config",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.user-data",
- "[GET] [HTTP:200] http://lxd/1.0/config/user.vendor-data",
- "[GET] [HTTP:200]"
- " http://lxd/1.0/config/cloud-init.network-config",
- "[GET] [HTTP:200]"
- " http://lxd/1.0/config/cloud-init.user-data",
- "[GET] [HTTP:200]"
- " http://lxd/1.0/config/cloud-init.vendor-data",
- "Ignoring LXD config user.user-data in favor of"
- " cloud-init.user-data value.",
- "Ignoring LXD config user.network-config in favor of"
- " cloud-init.network-config value.",
- "Ignoring LXD config user.vendor-data in favor of"
- " cloud-init.vendor-data value.",
- ],
- ),
- )
- )
- @mock.patch.object(lxd.requests.Session, 'get')
- def test_read_metadata_handles_unexpected_content_or_http_status(
- self, session_get, url_responses, expected, logs, caplog
- ):
- """read_metadata handles valid and invalid content and status codes."""
-
- def fake_get(url):
- """Mock Response json, ok, status_code, text from url_responses."""
- m_resp = mock.MagicMock()
- content = url_responses.get(url, '')
- m_resp.json.side_effect = lambda: json.loads(content)
- if content:
- mock_ok = mock.PropertyMock(return_value=True)
- mock_status_code = mock.PropertyMock(return_value=200)
- else:
- mock_ok = mock.PropertyMock(return_value=False)
- mock_status_code = mock.PropertyMock(return_value=404)
- type(m_resp).ok = mock_ok
- type(m_resp).status_code = mock_status_code
- mock_text = mock.PropertyMock(return_value=content)
- type(m_resp).text = mock_text
- return m_resp
-
- session_get.side_effect = fake_get
-
- if isinstance(expected, Exception):
- with pytest.raises(type(expected), match=re.escape(str(expected))):
- lxd.read_metadata()
- else:
- assert expected == lxd.read_metadata()
- caplogs = caplog.text
- for log in logs:
- assert log in caplogs
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
deleted file mode 100644
index 5f608cbb..00000000
--- a/cloudinit/sources/tests/test_oracle.py
+++ /dev/null
@@ -1,797 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import base64
-import copy
-import json
-from contextlib import ExitStack
-from unittest import mock
-
-import pytest
-
-from cloudinit.sources import DataSourceOracle as oracle
-from cloudinit.sources import NetworkConfigSource
-from cloudinit.sources.DataSourceOracle import OpcMetadata
-from cloudinit.tests import helpers as test_helpers
-from cloudinit.url_helper import UrlError
-
-DS_PATH = "cloudinit.sources.DataSourceOracle"
-
-# `curl -L http://169.254.169.254/opc/v1/vnics/` on a Oracle Bare Metal Machine
-# with a secondary VNIC attached (vnicId truncated for Python line length)
-OPC_BM_SECONDARY_VNIC_RESPONSE = """\
-[ {
- "vnicId" : "ocid1.vnic.oc1.phx.abyhqljtyvcucqkhdqmgjszebxe4hrb!!TRUNCATED||",
- "privateIp" : "10.0.0.8",
- "vlanTag" : 0,
- "macAddr" : "90:e2:ba:d4:f1:68",
- "virtualRouterIp" : "10.0.0.1",
- "subnetCidrBlock" : "10.0.0.0/24",
- "nicIndex" : 0
-}, {
- "vnicId" : "ocid1.vnic.oc1.phx.abyhqljtfmkxjdy2sqidndiwrsg63zf!!TRUNCATED||",
- "privateIp" : "10.0.4.5",
- "vlanTag" : 1,
- "macAddr" : "02:00:17:05:CF:51",
- "virtualRouterIp" : "10.0.4.1",
- "subnetCidrBlock" : "10.0.4.0/24",
- "nicIndex" : 0
-} ]"""
-
-# `curl -L http://169.254.169.254/opc/v1/vnics/` on a Oracle Virtual Machine
-# with a secondary VNIC attached
-OPC_VM_SECONDARY_VNIC_RESPONSE = """\
-[ {
- "vnicId" : "ocid1.vnic.oc1.phx.abyhqljtch72z5pd76cc2636qeqh7z_truncated",
- "privateIp" : "10.0.0.230",
- "vlanTag" : 1039,
- "macAddr" : "02:00:17:05:D1:DB",
- "virtualRouterIp" : "10.0.0.1",
- "subnetCidrBlock" : "10.0.0.0/24"
-}, {
- "vnicId" : "ocid1.vnic.oc1.phx.abyhqljt4iew3gwmvrwrhhf3bp5drj_truncated",
- "privateIp" : "10.0.0.231",
- "vlanTag" : 1041,
- "macAddr" : "00:00:17:02:2B:B1",
- "virtualRouterIp" : "10.0.0.1",
- "subnetCidrBlock" : "10.0.0.0/24"
-} ]"""
-
-
-# Fetched with `curl http://169.254.169.254/opc/v1/instance/` (and then
-# truncated for line length)
-OPC_V2_METADATA = """\
-{
- "availabilityDomain" : "qIZq:PHX-AD-1",
- "faultDomain" : "FAULT-DOMAIN-2",
- "compartmentId" : "ocid1.tenancy.oc1..aaaaaaaao7f7cccogqrg5emjxkxmTRUNCATED",
- "displayName" : "instance-20200320-1400",
- "hostname" : "instance-20200320-1400",
- "id" : "ocid1.instance.oc1.phx.anyhqljtniwq6syc3nex55sep5w34qbwmw6TRUNCATED",
- "image" : "ocid1.image.oc1.phx.aaaaaaaagmkn4gdhvvx24kiahh2b2qchsicTRUNCATED",
- "metadata" : {
- "ssh_authorized_keys" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ truncated",
- "user_data" : "IyEvYmluL3NoCnRvdWNoIC90bXAvZm9v"
- },
- "region" : "phx",
- "canonicalRegionName" : "us-phoenix-1",
- "ociAdName" : "phx-ad-3",
- "shape" : "VM.Standard2.1",
- "state" : "Running",
- "timeCreated" : 1584727285318,
- "agentConfig" : {
- "monitoringDisabled" : true,
- "managementDisabled" : true
- }
-}"""
-
-# Just a small meaningless change to differentiate the two metadatas
-OPC_V1_METADATA = OPC_V2_METADATA.replace("ocid1.instance", "ocid2.instance")
-
-
-@pytest.fixture
-def metadata_version():
- return 2
-
-
-@pytest.yield_fixture
-def oracle_ds(request, fixture_utils, paths, metadata_version):
- """
- Return an instantiated DataSourceOracle.
-
- This also performs the mocking required for the default test case:
- * ``_read_system_uuid`` returns something,
- * ``_is_platform_viable`` returns True,
- * ``_is_iscsi_root`` returns True (the simpler code path),
- * ``read_opc_metadata`` returns ``OPC_V1_METADATA``
-
- (This uses the paths fixture for the required helpers.Paths object, and the
- fixture_utils fixture for fetching markers.)
- """
- sys_cfg = fixture_utils.closest_marker_first_arg_or(
- request, "ds_sys_cfg", mock.MagicMock()
- )
- metadata = OpcMetadata(metadata_version, json.loads(OPC_V2_METADATA), None)
- with mock.patch(DS_PATH + "._read_system_uuid", return_value="someuuid"):
- with mock.patch(DS_PATH + "._is_platform_viable", return_value=True):
- with mock.patch(DS_PATH + "._is_iscsi_root", return_value=True):
- with mock.patch(
- DS_PATH + ".read_opc_metadata",
- return_value=metadata,
- ):
- yield oracle.DataSourceOracle(
- sys_cfg=sys_cfg, distro=mock.Mock(), paths=paths,
- )
-
-
-class TestDataSourceOracle:
- def test_platform_info(self, oracle_ds):
- assert "oracle" == oracle_ds.cloud_name
- assert "oracle" == oracle_ds.platform_type
-
- def test_subplatform_before_fetch(self, oracle_ds):
- assert 'unknown' == oracle_ds.subplatform
-
- def test_platform_info_after_fetch(self, oracle_ds):
- oracle_ds._get_data()
- assert 'metadata (http://169.254.169.254/opc/v2/)' == \
- oracle_ds.subplatform
-
- @pytest.mark.parametrize('metadata_version', [1])
- def test_v1_platform_info_after_fetch(self, oracle_ds):
- oracle_ds._get_data()
- assert 'metadata (http://169.254.169.254/opc/v1/)' == \
- oracle_ds.subplatform
-
- def test_secondary_nics_disabled_by_default(self, oracle_ds):
- assert not oracle_ds.ds_cfg["configure_secondary_nics"]
-
- @pytest.mark.ds_sys_cfg(
- {"datasource": {"Oracle": {"configure_secondary_nics": True}}}
- )
- def test_sys_cfg_can_enable_configure_secondary_nics(self, oracle_ds):
- assert oracle_ds.ds_cfg["configure_secondary_nics"]
-
-
-class TestIsPlatformViable(test_helpers.CiTestCase):
- @mock.patch(DS_PATH + ".dmi.read_dmi_data",
- return_value=oracle.CHASSIS_ASSET_TAG)
- def test_expected_viable(self, m_read_dmi_data):
- """System with known chassis tag is viable."""
- self.assertTrue(oracle._is_platform_viable())
- m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
-
- @mock.patch(DS_PATH + ".dmi.read_dmi_data", return_value=None)
- def test_expected_not_viable_dmi_data_none(self, m_read_dmi_data):
- """System without known chassis tag is not viable."""
- self.assertFalse(oracle._is_platform_viable())
- m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
-
- @mock.patch(DS_PATH + ".dmi.read_dmi_data", return_value="LetsGoCubs")
- def test_expected_not_viable_other(self, m_read_dmi_data):
- """System with unnown chassis tag is not viable."""
- self.assertFalse(oracle._is_platform_viable())
- m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
-
-
-@mock.patch(
- "cloudinit.net.is_openvswitch_internal_interface",
- mock.Mock(return_value=False)
-)
-class TestNetworkConfigFromOpcImds:
- def test_no_secondary_nics_does_not_mutate_input(self, oracle_ds):
- oracle_ds._vnics_data = [{}]
- # We test this by using in a non-dict to ensure that no dict
- # operations are used; failure would be seen as exceptions
- oracle_ds._network_config = object()
- oracle_ds._add_network_config_from_opc_imds()
-
- def test_bare_metal_machine_skipped(self, oracle_ds, caplog):
- # nicIndex in the first entry indicates a bare metal machine
- oracle_ds._vnics_data = json.loads(OPC_BM_SECONDARY_VNIC_RESPONSE)
- # We test this by using a non-dict to ensure that no dict
- # operations are used
- oracle_ds._network_config = object()
- oracle_ds._add_network_config_from_opc_imds()
- assert 'bare metal machine' in caplog.text
-
- def test_missing_mac_skipped(self, oracle_ds, caplog):
- oracle_ds._vnics_data = json.loads(OPC_VM_SECONDARY_VNIC_RESPONSE)
-
- oracle_ds._network_config = {
- 'version': 1, 'config': [{'primary': 'nic'}]
- }
- with mock.patch(DS_PATH + ".get_interfaces_by_mac", return_value={}):
- oracle_ds._add_network_config_from_opc_imds()
-
- assert 1 == len(oracle_ds.network_config['config'])
- assert 'Interface with MAC 00:00:17:02:2b:b1 not found; skipping' in \
- caplog.text
-
- def test_missing_mac_skipped_v2(self, oracle_ds, caplog):
- oracle_ds._vnics_data = json.loads(OPC_VM_SECONDARY_VNIC_RESPONSE)
-
- oracle_ds._network_config = {
- 'version': 2, 'ethernets': {'primary': {'nic': {}}}
- }
- with mock.patch(DS_PATH + ".get_interfaces_by_mac", return_value={}):
- oracle_ds._add_network_config_from_opc_imds()
-
- assert 1 == len(oracle_ds.network_config['ethernets'])
- assert 'Interface with MAC 00:00:17:02:2b:b1 not found; skipping' in \
- caplog.text
-
- def test_secondary_nic(self, oracle_ds):
- oracle_ds._vnics_data = json.loads(OPC_VM_SECONDARY_VNIC_RESPONSE)
- oracle_ds._network_config = {
- 'version': 1, 'config': [{'primary': 'nic'}]
- }
- mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
- with mock.patch(DS_PATH + ".get_interfaces_by_mac",
- return_value={mac_addr: nic_name}):
- oracle_ds._add_network_config_from_opc_imds()
-
- # The input is mutated
- assert 2 == len(oracle_ds.network_config['config'])
-
- secondary_nic_cfg = oracle_ds.network_config['config'][1]
- assert nic_name == secondary_nic_cfg['name']
- assert 'physical' == secondary_nic_cfg['type']
- assert mac_addr == secondary_nic_cfg['mac_address']
- assert 9000 == secondary_nic_cfg['mtu']
-
- assert 1 == len(secondary_nic_cfg['subnets'])
- subnet_cfg = secondary_nic_cfg['subnets'][0]
- # These values are hard-coded in OPC_VM_SECONDARY_VNIC_RESPONSE
- assert '10.0.0.231' == subnet_cfg['address']
-
- def test_secondary_nic_v2(self, oracle_ds):
- oracle_ds._vnics_data = json.loads(OPC_VM_SECONDARY_VNIC_RESPONSE)
- oracle_ds._network_config = {
- 'version': 2, 'ethernets': {'primary': {'nic': {}}}
- }
- mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
- with mock.patch(DS_PATH + ".get_interfaces_by_mac",
- return_value={mac_addr: nic_name}):
- oracle_ds._add_network_config_from_opc_imds()
-
- # The input is mutated
- assert 2 == len(oracle_ds.network_config['ethernets'])
-
- secondary_nic_cfg = oracle_ds.network_config['ethernets']['ens3']
- assert secondary_nic_cfg['dhcp4'] is False
- assert secondary_nic_cfg['dhcp6'] is False
- assert mac_addr == secondary_nic_cfg['match']['macaddress']
- assert 9000 == secondary_nic_cfg['mtu']
-
- assert 1 == len(secondary_nic_cfg['addresses'])
- # These values are hard-coded in OPC_VM_SECONDARY_VNIC_RESPONSE
- assert '10.0.0.231' == secondary_nic_cfg['addresses'][0]
-
-
-class TestNetworkConfigFiltersNetFailover(test_helpers.CiTestCase):
-
- def setUp(self):
- super(TestNetworkConfigFiltersNetFailover, self).setUp()
- self.add_patch(DS_PATH + '.get_interfaces_by_mac',
- 'm_get_interfaces_by_mac')
- self.add_patch(DS_PATH + '.is_netfail_master', 'm_netfail_master')
-
- def test_ignore_bogus_network_config(self):
- netcfg = {'something': 'here'}
- passed_netcfg = copy.copy(netcfg)
- oracle._ensure_netfailover_safe(passed_netcfg)
- self.assertEqual(netcfg, passed_netcfg)
-
- def test_ignore_network_config_unknown_versions(self):
- netcfg = {'something': 'here', 'version': 3}
- passed_netcfg = copy.copy(netcfg)
- oracle._ensure_netfailover_safe(passed_netcfg)
- self.assertEqual(netcfg, passed_netcfg)
-
- def test_checks_v1_type_physical_interfaces(self):
- mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
- self.m_get_interfaces_by_mac.return_value = {
- mac_addr: nic_name,
- }
- netcfg = {'version': 1, 'config': [
- {'type': 'physical', 'name': nic_name, 'mac_address': mac_addr,
- 'subnets': [{'type': 'dhcp4'}]}]}
- passed_netcfg = copy.copy(netcfg)
- self.m_netfail_master.return_value = False
- oracle._ensure_netfailover_safe(passed_netcfg)
- self.assertEqual(netcfg, passed_netcfg)
- self.assertEqual([mock.call(nic_name)],
- self.m_netfail_master.call_args_list)
-
- def test_checks_v1_skips_non_phys_interfaces(self):
- mac_addr, nic_name = '00:00:17:02:2b:b1', 'bond0'
- self.m_get_interfaces_by_mac.return_value = {
- mac_addr: nic_name,
- }
- netcfg = {'version': 1, 'config': [
- {'type': 'bond', 'name': nic_name, 'mac_address': mac_addr,
- 'subnets': [{'type': 'dhcp4'}]}]}
- passed_netcfg = copy.copy(netcfg)
- oracle._ensure_netfailover_safe(passed_netcfg)
- self.assertEqual(netcfg, passed_netcfg)
- self.assertEqual(0, self.m_netfail_master.call_count)
-
- def test_removes_master_mac_property_v1(self):
- nic_master, mac_master = 'ens3', self.random_string()
- nic_other, mac_other = 'ens7', self.random_string()
- nic_extra, mac_extra = 'enp0s1f2', self.random_string()
- self.m_get_interfaces_by_mac.return_value = {
- mac_master: nic_master,
- mac_other: nic_other,
- mac_extra: nic_extra,
- }
- netcfg = {'version': 1, 'config': [
- {'type': 'physical', 'name': nic_master,
- 'mac_address': mac_master},
- {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
- {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
- ]}
-
- def _is_netfail_master(iface):
- if iface == 'ens3':
- return True
- return False
- self.m_netfail_master.side_effect = _is_netfail_master
- expected_cfg = {'version': 1, 'config': [
- {'type': 'physical', 'name': nic_master},
- {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
- {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
- ]}
- oracle._ensure_netfailover_safe(netcfg)
- self.assertEqual(expected_cfg, netcfg)
-
- def test_checks_v2_type_ethernet_interfaces(self):
- mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
- self.m_get_interfaces_by_mac.return_value = {
- mac_addr: nic_name,
- }
- netcfg = {'version': 2, 'ethernets': {
- nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
- 'match': {'macaddress': mac_addr}}}}
- passed_netcfg = copy.copy(netcfg)
- self.m_netfail_master.return_value = False
- oracle._ensure_netfailover_safe(passed_netcfg)
- self.assertEqual(netcfg, passed_netcfg)
- self.assertEqual([mock.call(nic_name)],
- self.m_netfail_master.call_args_list)
-
- def test_skips_v2_non_ethernet_interfaces(self):
- mac_addr, nic_name = '00:00:17:02:2b:b1', 'wlps0'
- self.m_get_interfaces_by_mac.return_value = {
- mac_addr: nic_name,
- }
- netcfg = {'version': 2, 'wifis': {
- nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
- 'match': {'macaddress': mac_addr}}}}
- passed_netcfg = copy.copy(netcfg)
- oracle._ensure_netfailover_safe(passed_netcfg)
- self.assertEqual(netcfg, passed_netcfg)
- self.assertEqual(0, self.m_netfail_master.call_count)
-
- def test_removes_master_mac_property_v2(self):
- nic_master, mac_master = 'ens3', self.random_string()
- nic_other, mac_other = 'ens7', self.random_string()
- nic_extra, mac_extra = 'enp0s1f2', self.random_string()
- self.m_get_interfaces_by_mac.return_value = {
- mac_master: nic_master,
- mac_other: nic_other,
- mac_extra: nic_extra,
- }
- netcfg = {'version': 2, 'ethernets': {
- nic_extra: {'dhcp4': True, 'set-name': nic_extra,
- 'match': {'macaddress': mac_extra}},
- nic_other: {'dhcp4': True, 'set-name': nic_other,
- 'match': {'macaddress': mac_other}},
- nic_master: {'dhcp4': True, 'set-name': nic_master,
- 'match': {'macaddress': mac_master}},
- }}
-
- def _is_netfail_master(iface):
- if iface == 'ens3':
- return True
- return False
- self.m_netfail_master.side_effect = _is_netfail_master
-
- expected_cfg = {'version': 2, 'ethernets': {
- nic_master: {'dhcp4': True, 'match': {'name': nic_master}},
- nic_extra: {'dhcp4': True, 'set-name': nic_extra,
- 'match': {'macaddress': mac_extra}},
- nic_other: {'dhcp4': True, 'set-name': nic_other,
- 'match': {'macaddress': mac_other}},
- }}
- oracle._ensure_netfailover_safe(netcfg)
- import pprint
- pprint.pprint(netcfg)
- print('---- ^^ modified ^^ ---- vv original vv ----')
- pprint.pprint(expected_cfg)
- self.assertEqual(expected_cfg, netcfg)
-
-
-def _mock_v2_urls(httpretty):
- def instance_callback(request, uri, response_headers):
- print(response_headers)
- assert request.headers.get("Authorization") == "Bearer Oracle"
- return [200, response_headers, OPC_V2_METADATA]
-
- def vnics_callback(request, uri, response_headers):
- assert request.headers.get("Authorization") == "Bearer Oracle"
- return [200, response_headers, OPC_BM_SECONDARY_VNIC_RESPONSE]
-
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v2/instance/",
- body=instance_callback
- )
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v2/vnics/",
- body=vnics_callback
- )
-
-
-def _mock_no_v2_urls(httpretty):
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v2/instance/",
- status=404,
- )
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v1/instance/",
- body=OPC_V1_METADATA
- )
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v1/vnics/",
- body=OPC_BM_SECONDARY_VNIC_RESPONSE
- )
-
-
-class TestReadOpcMetadata:
- # See https://docs.pytest.org/en/stable/example
- # /parametrize.html#parametrizing-conditional-raising
- does_not_raise = ExitStack
-
- @mock.patch("cloudinit.url_helper.time.sleep", lambda _: None)
- @pytest.mark.parametrize(
- 'version,setup_urls,instance_data,fetch_vnics,vnics_data', [
- (2, _mock_v2_urls, json.loads(OPC_V2_METADATA), True,
- json.loads(OPC_BM_SECONDARY_VNIC_RESPONSE)),
- (2, _mock_v2_urls, json.loads(OPC_V2_METADATA), False, None),
- (1, _mock_no_v2_urls, json.loads(OPC_V1_METADATA), True,
- json.loads(OPC_BM_SECONDARY_VNIC_RESPONSE)),
- (1, _mock_no_v2_urls, json.loads(OPC_V1_METADATA), False, None),
- ]
- )
- def test_metadata_returned(
- self, version, setup_urls, instance_data,
- fetch_vnics, vnics_data, httpretty
- ):
- setup_urls(httpretty)
- metadata = oracle.read_opc_metadata(fetch_vnics_data=fetch_vnics)
-
- assert version == metadata.version
- assert instance_data == metadata.instance_data
- assert vnics_data == metadata.vnics_data
-
- # No need to actually wait between retries in the tests
- @mock.patch("cloudinit.url_helper.time.sleep", lambda _: None)
- @pytest.mark.parametrize(
- "v2_failure_count,v1_failure_count,expected_body,expectation",
- [
- (1, 0, json.loads(OPC_V2_METADATA), does_not_raise()),
- (2, 0, json.loads(OPC_V2_METADATA), does_not_raise()),
- (3, 0, json.loads(OPC_V1_METADATA), does_not_raise()),
- (3, 1, json.loads(OPC_V1_METADATA), does_not_raise()),
- (3, 2, json.loads(OPC_V1_METADATA), does_not_raise()),
- (3, 3, None, pytest.raises(UrlError)),
- ]
- )
- def test_retries(self, v2_failure_count, v1_failure_count,
- expected_body, expectation, httpretty):
- v2_responses = [httpretty.Response("", status=404)] * v2_failure_count
- v2_responses.append(httpretty.Response(OPC_V2_METADATA))
- v1_responses = [httpretty.Response("", status=404)] * v1_failure_count
- v1_responses.append(httpretty.Response(OPC_V1_METADATA))
-
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v1/instance/",
- responses=v1_responses,
- )
- httpretty.register_uri(
- httpretty.GET,
- "http://169.254.169.254/opc/v2/instance/",
- responses=v2_responses,
- )
- with expectation:
- assert expected_body == oracle.read_opc_metadata().instance_data
-
-
-class TestCommon_GetDataBehaviour:
- """This test class tests behaviour common to iSCSI and non-iSCSI root.
-
- It defines a fixture, parameterized_oracle_ds, which is used in all the
- tests herein to test that the commonly expected behaviour is the same with
- iSCSI root and without.
-
- (As non-iSCSI root behaviour is a superset of iSCSI root behaviour this
- class is implicitly also testing all iSCSI root behaviour so there is no
- separate class for that case.)
- """
-
- @pytest.yield_fixture(params=[True, False])
- def parameterized_oracle_ds(self, request, oracle_ds):
- """oracle_ds parameterized for iSCSI and non-iSCSI root respectively"""
- is_iscsi_root = request.param
- with ExitStack() as stack:
- stack.enter_context(
- mock.patch(
- DS_PATH + "._is_iscsi_root", return_value=is_iscsi_root
- )
- )
- if not is_iscsi_root:
- stack.enter_context(
- mock.patch(DS_PATH + ".net.find_fallback_nic")
- )
- stack.enter_context(
- mock.patch(DS_PATH + ".dhcp.EphemeralDHCPv4")
- )
- yield oracle_ds
-
- @mock.patch(
- DS_PATH + "._is_platform_viable", mock.Mock(return_value=False)
- )
- def test_false_if_platform_not_viable(
- self, parameterized_oracle_ds,
- ):
- assert not parameterized_oracle_ds._get_data()
-
- @pytest.mark.parametrize(
- "keyname,expected_value",
- (
- ("availability-zone", "phx-ad-3"),
- ("launch-index", 0),
- ("local-hostname", "instance-20200320-1400"),
- (
- "instance-id",
- "ocid1.instance.oc1.phx"
- ".anyhqljtniwq6syc3nex55sep5w34qbwmw6TRUNCATED",
- ),
- ("name", "instance-20200320-1400"),
- (
- "public_keys",
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ truncated",
- ),
- ),
- )
- def test_metadata_keys_set_correctly(
- self, keyname, expected_value, parameterized_oracle_ds,
- ):
- assert parameterized_oracle_ds._get_data()
- assert expected_value == parameterized_oracle_ds.metadata[keyname]
-
- @pytest.mark.parametrize(
- "attribute_name,expected_value",
- [
- ("_crawled_metadata", json.loads(OPC_V2_METADATA)),
- (
- "userdata_raw",
- base64.b64decode(b"IyEvYmluL3NoCnRvdWNoIC90bXAvZm9v"),
- ),
- ("system_uuid", "my-test-uuid"),
- ],
- )
- @mock.patch(
- DS_PATH + "._read_system_uuid", mock.Mock(return_value="my-test-uuid")
- )
- def test_attributes_set_correctly(
- self, attribute_name, expected_value, parameterized_oracle_ds,
- ):
- assert parameterized_oracle_ds._get_data()
- assert expected_value == getattr(
- parameterized_oracle_ds, attribute_name
- )
-
- @pytest.mark.parametrize(
- "ssh_keys,expected_value",
- [
- # No SSH keys in metadata => no keys detected
- (None, []),
- # Empty SSH keys in metadata => no keys detected
- ("", []),
- # Single SSH key in metadata => single key detected
- ("ssh-rsa ... test@test", ["ssh-rsa ... test@test"]),
- # Multiple SSH keys in metadata => multiple keys detected
- (
- "ssh-rsa ... test@test\nssh-rsa ... test2@test2",
- ["ssh-rsa ... test@test", "ssh-rsa ... test2@test2"],
- ),
- ],
- )
- def test_public_keys_handled_correctly(
- self, ssh_keys, expected_value, parameterized_oracle_ds
- ):
- instance_data = json.loads(OPC_V1_METADATA)
- if ssh_keys is None:
- del instance_data["metadata"]["ssh_authorized_keys"]
- else:
- instance_data["metadata"]["ssh_authorized_keys"] = ssh_keys
- metadata = OpcMetadata(None, instance_data, None)
- with mock.patch(
- DS_PATH + ".read_opc_metadata", mock.Mock(return_value=metadata),
- ):
- assert parameterized_oracle_ds._get_data()
- assert (
- expected_value == parameterized_oracle_ds.get_public_ssh_keys()
- )
-
- def test_missing_user_data_handled_gracefully(
- self, parameterized_oracle_ds
- ):
- instance_data = json.loads(OPC_V1_METADATA)
- del instance_data["metadata"]["user_data"]
- metadata = OpcMetadata(None, instance_data, None)
- with mock.patch(
- DS_PATH + ".read_opc_metadata", mock.Mock(return_value=metadata),
- ):
- assert parameterized_oracle_ds._get_data()
-
- assert parameterized_oracle_ds.userdata_raw is None
-
- def test_missing_metadata_handled_gracefully(
- self, parameterized_oracle_ds
- ):
- instance_data = json.loads(OPC_V1_METADATA)
- del instance_data["metadata"]
- metadata = OpcMetadata(None, instance_data, None)
- with mock.patch(
- DS_PATH + ".read_opc_metadata", mock.Mock(return_value=metadata),
- ):
- assert parameterized_oracle_ds._get_data()
-
- assert parameterized_oracle_ds.userdata_raw is None
- assert [] == parameterized_oracle_ds.get_public_ssh_keys()
-
-
-@mock.patch(DS_PATH + "._is_iscsi_root", lambda: False)
-class TestNonIscsiRoot_GetDataBehaviour:
- @mock.patch(DS_PATH + ".dhcp.EphemeralDHCPv4")
- @mock.patch(DS_PATH + ".net.find_fallback_nic")
- def test_read_opc_metadata_called_with_ephemeral_dhcp(
- self, m_find_fallback_nic, m_EphemeralDHCPv4, oracle_ds
- ):
- in_context_manager = False
-
- def enter_context_manager():
- nonlocal in_context_manager
- in_context_manager = True
-
- def exit_context_manager(*args):
- nonlocal in_context_manager
- in_context_manager = False
-
- m_EphemeralDHCPv4.return_value.__enter__.side_effect = (
- enter_context_manager
- )
- m_EphemeralDHCPv4.return_value.__exit__.side_effect = (
- exit_context_manager
- )
-
- def assert_in_context_manager(**kwargs):
- assert in_context_manager
- return mock.MagicMock()
-
- with mock.patch(
- DS_PATH + ".read_opc_metadata",
- mock.Mock(side_effect=assert_in_context_manager),
- ):
- assert oracle_ds._get_data()
-
- assert [
- mock.call(
- iface=m_find_fallback_nic.return_value,
- connectivity_url_data={
- 'headers': {
- 'Authorization': 'Bearer Oracle'
- },
- 'url': 'http://169.254.169.254/opc/v2/instance/'
- }
- )
- ] == m_EphemeralDHCPv4.call_args_list
-
-
-@mock.patch(DS_PATH + ".get_interfaces_by_mac", lambda: {})
-@mock.patch(DS_PATH + ".cmdline.read_initramfs_config")
-class TestNetworkConfig:
- def test_network_config_cached(self, m_read_initramfs_config, oracle_ds):
- """.network_config should be cached"""
- assert 0 == m_read_initramfs_config.call_count
- oracle_ds.network_config # pylint: disable=pointless-statement
- assert 1 == m_read_initramfs_config.call_count
- oracle_ds.network_config # pylint: disable=pointless-statement
- assert 1 == m_read_initramfs_config.call_count
-
- def test_network_cmdline(self, m_read_initramfs_config, oracle_ds):
- """network_config should prefer initramfs config over fallback"""
- ncfg = {"version": 1, "config": [{"a": "b"}]}
- m_read_initramfs_config.return_value = copy.deepcopy(ncfg)
-
- assert ncfg == oracle_ds.network_config
- assert 0 == oracle_ds.distro.generate_fallback_config.call_count
-
- def test_network_fallback(self, m_read_initramfs_config, oracle_ds):
- """network_config should prefer initramfs config over fallback"""
- ncfg = {"version": 1, "config": [{"a": "b"}]}
-
- m_read_initramfs_config.return_value = None
- oracle_ds.distro.generate_fallback_config.return_value = copy.deepcopy(
- ncfg
- )
-
- assert ncfg == oracle_ds.network_config
-
- @pytest.mark.parametrize(
- "configure_secondary_nics,expect_secondary_nics",
- [(True, True), (False, False), (None, False)],
- )
- def test_secondary_nic_addition(
- self,
- m_read_initramfs_config,
- configure_secondary_nics,
- expect_secondary_nics,
- oracle_ds,
- ):
- """Test that _add_network_config_from_opc_imds is called as expected
-
- (configure_secondary_nics=None is used to test the default behaviour.)
- """
- m_read_initramfs_config.return_value = {"version": 1, "config": []}
-
- if configure_secondary_nics is not None:
- oracle_ds.ds_cfg[
- "configure_secondary_nics"
- ] = configure_secondary_nics
-
- def side_effect(self):
- self._network_config["secondary_added"] = mock.sentinel.needle
-
- oracle_ds._vnics_data = 'DummyData'
- with mock.patch.object(
- oracle.DataSourceOracle, "_add_network_config_from_opc_imds",
- new=side_effect,
- ):
- was_secondary_added = "secondary_added" in oracle_ds.network_config
- assert expect_secondary_nics == was_secondary_added
-
- def test_secondary_nic_failure_isnt_blocking(
- self,
- m_read_initramfs_config,
- caplog,
- oracle_ds,
- ):
- oracle_ds.ds_cfg["configure_secondary_nics"] = True
- oracle_ds._vnics_data = "DummyData"
-
- with mock.patch.object(
- oracle.DataSourceOracle, "_add_network_config_from_opc_imds",
- side_effect=Exception()
- ):
- network_config = oracle_ds.network_config
- assert network_config == m_read_initramfs_config.return_value
- assert "Failed to parse secondary network configuration" in caplog.text
-
- def test_ds_network_cfg_preferred_over_initramfs(self, _m):
- """Ensure that DS net config is preferred over initramfs config"""
- config_sources = oracle.DataSourceOracle.network_config_sources
- ds_idx = config_sources.index(NetworkConfigSource.ds)
- initramfs_idx = config_sources.index(NetworkConfigSource.initramfs)
- assert ds_idx < initramfs_idx
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/__init__.py b/cloudinit/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/cloudinit/tests/__init__.py
+++ /dev/null
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
deleted file mode 100644
index ccd56793..00000000
--- a/cloudinit/tests/helpers.py
+++ /dev/null
@@ -1,507 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-import functools
-import httpretty
-import io
-import logging
-import os
-import random
-import shutil
-import string
-import sys
-import tempfile
-import time
-import unittest
-from contextlib import ExitStack, contextmanager
-from unittest import mock
-from unittest.util import strclass
-
-from cloudinit.config.schema import (
- SchemaValidationError, validate_cloudconfig_schema)
-from cloudinit import cloud
-from cloudinit import distros
-from cloudinit import helpers as ch
-from cloudinit.sources import DataSourceNone
-from cloudinit.templater import JINJA_AVAILABLE
-from cloudinit import subp
-from cloudinit import util
-
-_real_subp = subp.subp
-
-# Used for skipping tests
-SkipTest = unittest.SkipTest
-skipIf = unittest.skipIf
-
-
-# Makes the old path start
-# with new base instead of whatever
-# it previously had
-def rebase_path(old_path, new_base):
- if old_path.startswith(new_base):
- # Already handled...
- return old_path
- # Retarget the base of that path
- # to the new base instead of the
- # old one...
- path = os.path.join(new_base, old_path.lstrip("/"))
- path = os.path.abspath(path)
- return path
-
-
-# Can work on anything that takes a path as arguments
-def retarget_many_wrapper(new_base, am, old_func):
- def wrapper(*args, **kwds):
- n_args = list(args)
- nam = am
- if am == -1:
- nam = len(n_args)
- for i in range(0, nam):
- path = args[i]
- # patchOS() wraps various os and os.path functions, however in
- # Python 3 some of these now accept file-descriptors (integers).
- # That breaks rebase_path() so in lieu of a better solution, just
- # don't rebase if we get a fd.
- if isinstance(path, str):
- n_args[i] = rebase_path(path, new_base)
- return old_func(*n_args, **kwds)
- return wrapper
-
-
-class TestCase(unittest.TestCase):
-
- def reset_global_state(self):
- """Reset any global state to its original settings.
-
- cloudinit caches some values in cloudinit.util. Unit tests that
- involved those cached paths were then subject to failure if the order
- of invocation changed (LP: #1703697).
-
- This function resets any of these global state variables to their
- initial state.
-
- In the future this should really be done with some registry that
- can then be cleaned in a more obvious way.
- """
- util.PROC_CMDLINE = None
- util._DNS_REDIRECT_IP = None
- util._LSB_RELEASE = {}
-
- def setUp(self):
- super(TestCase, self).setUp()
- self.reset_global_state()
-
- def shortDescription(self):
- return strclass(self.__class__) + '.' + self._testMethodName
-
- def add_patch(self, target, attr, *args, **kwargs):
- """Patches specified target object and sets it as attr on test
- instance also schedules cleanup"""
- if 'autospec' not in kwargs:
- kwargs['autospec'] = True
- m = mock.patch(target, *args, **kwargs)
- p = m.start()
- self.addCleanup(m.stop)
- setattr(self, attr, p)
-
-
-class CiTestCase(TestCase):
- """This is the preferred test case base class unless user
- needs other test case classes below."""
-
- # Subclass overrides for specific test behavior
- # Whether or not a unit test needs logfile setup
- with_logs = False
- allowed_subp = False
- SUBP_SHELL_TRUE = "shell=true"
-
- @contextmanager
- def allow_subp(self, allowed_subp):
- orig = self.allowed_subp
- try:
- self.allowed_subp = allowed_subp
- yield
- finally:
- self.allowed_subp = orig
-
- def setUp(self):
- super(CiTestCase, self).setUp()
- if self.with_logs:
- # Create a log handler so unit tests can search expected logs.
- self.logger = logging.getLogger()
- self.logs = io.StringIO()
- formatter = logging.Formatter('%(levelname)s: %(message)s')
- handler = logging.StreamHandler(self.logs)
- handler.setFormatter(formatter)
- self.old_handlers = self.logger.handlers
- self.logger.handlers = [handler]
- if self.allowed_subp is True:
- subp.subp = _real_subp
- else:
- subp.subp = self._fake_subp
-
- def _fake_subp(self, *args, **kwargs):
- if 'args' in kwargs:
- cmd = kwargs['args']
- else:
- if not args:
- raise TypeError(
- "subp() missing 1 required positional argument: 'args'")
- cmd = args[0]
-
- if not isinstance(cmd, str):
- cmd = cmd[0]
- pass_through = False
- if not isinstance(self.allowed_subp, (list, bool)):
- raise TypeError("self.allowed_subp supports list or bool.")
- if isinstance(self.allowed_subp, bool):
- pass_through = self.allowed_subp
- else:
- pass_through = (
- (cmd in self.allowed_subp) or
- (self.SUBP_SHELL_TRUE in self.allowed_subp and
- kwargs.get('shell')))
- if pass_through:
- return _real_subp(*args, **kwargs)
- raise Exception(
- "called subp. set self.allowed_subp=True to allow\n subp(%s)" %
- ', '.join([str(repr(a)) for a in args] +
- ["%s=%s" % (k, repr(v)) for k, v in kwargs.items()]))
-
- def tearDown(self):
- if self.with_logs:
- # Remove the handler we setup
- logging.getLogger().handlers = self.old_handlers
- logging.getLogger().setLevel(logging.NOTSET)
- subp.subp = _real_subp
- super(CiTestCase, self).tearDown()
-
- def tmp_dir(self, dir=None, cleanup=True):
- # return a full path to a temporary directory that will be cleaned up.
- if dir is None:
- tmpd = tempfile.mkdtemp(
- prefix="ci-%s." % self.__class__.__name__)
- else:
- tmpd = tempfile.mkdtemp(dir=dir)
- self.addCleanup(
- functools.partial(shutil.rmtree, tmpd, ignore_errors=True))
- return tmpd
-
- def tmp_path(self, path, dir=None):
- # return an absolute path to 'path' under dir.
- # if dir is None, one will be created with tmp_dir()
- # the file is not created or modified.
- if dir is None:
- dir = self.tmp_dir()
- return os.path.normpath(os.path.abspath(os.path.join(dir, path)))
-
- def tmp_cloud(self, distro, sys_cfg=None, metadata=None):
- """Create a cloud with tmp working directory paths.
-
- @param distro: Name of the distro to attach to the cloud.
- @param metadata: Optional metadata to set on the datasource.
-
- @return: The built cloud instance.
- """
- self.new_root = self.tmp_dir()
- if not sys_cfg:
- sys_cfg = {}
- tmp_paths = {}
- for var in ['templates_dir', 'run_dir', 'cloud_dir']:
- tmp_paths[var] = self.tmp_path(var, dir=self.new_root)
- util.ensure_dir(tmp_paths[var])
- self.paths = ch.Paths(tmp_paths)
- cls = distros.fetch(distro)
- mydist = cls(distro, sys_cfg, self.paths)
- myds = DataSourceNone.DataSourceNone(sys_cfg, mydist, self.paths)
- if metadata:
- myds.metadata.update(metadata)
- return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None)
-
- @classmethod
- def random_string(cls, length=8):
- """ return a random lowercase string with default length of 8"""
- return ''.join(
- random.choice(string.ascii_lowercase) for _ in range(length))
-
-
-class ResourceUsingTestCase(CiTestCase):
-
- def setUp(self):
- super(ResourceUsingTestCase, self).setUp()
- self.resource_path = None
-
- def getCloudPaths(self, ds=None):
- tmpdir = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, tmpdir)
- cp = ch.Paths({'cloud_dir': tmpdir,
- 'templates_dir': resourceLocation()},
- ds=ds)
- return cp
-
-
-class FilesystemMockingTestCase(ResourceUsingTestCase):
-
- def setUp(self):
- super(FilesystemMockingTestCase, self).setUp()
- self.patched_funcs = ExitStack()
-
- def tearDown(self):
- self.patched_funcs.close()
- ResourceUsingTestCase.tearDown(self)
-
- def replicateTestRoot(self, example_root, target_root):
- real_root = resourceLocation()
- real_root = os.path.join(real_root, 'roots', example_root)
- for (dir_path, _dirnames, filenames) in os.walk(real_root):
- real_path = dir_path
- make_path = rebase_path(real_path[len(real_root):], target_root)
- util.ensure_dir(make_path)
- for f in filenames:
- real_path = util.abs_join(real_path, f)
- make_path = util.abs_join(make_path, f)
- shutil.copy(real_path, make_path)
-
- def patchUtils(self, new_root):
- patch_funcs = {
- util: [('write_file', 1),
- ('append_file', 1),
- ('load_file', 1),
- ('ensure_dir', 1),
- ('chmod', 1),
- ('delete_dir_contents', 1),
- ('del_file', 1),
- ('sym_link', -1),
- ('copy', -1)],
- }
- for (mod, funcs) in patch_funcs.items():
- for (f, am) in funcs:
- func = getattr(mod, f)
- trap_func = retarget_many_wrapper(new_root, am, func)
- self.patched_funcs.enter_context(
- mock.patch.object(mod, f, trap_func))
-
- # Handle subprocess calls
- func = getattr(subp, 'subp')
-
- def nsubp(*_args, **_kwargs):
- return ('', '')
-
- self.patched_funcs.enter_context(
- mock.patch.object(subp, 'subp', nsubp))
-
- def null_func(*_args, **_kwargs):
- return None
-
- for f in ['chownbyid', 'chownbyname']:
- self.patched_funcs.enter_context(
- mock.patch.object(util, f, null_func))
-
- def patchOS(self, new_root):
- patch_funcs = {
- os.path: [('isfile', 1), ('exists', 1),
- ('islink', 1), ('isdir', 1), ('lexists', 1)],
- os: [('listdir', 1), ('mkdir', 1),
- ('lstat', 1), ('symlink', 2),
- ('stat', 1)]
- }
-
- if hasattr(os, 'scandir'):
- # py27 does not have scandir
- patch_funcs[os].append(('scandir', 1))
-
- for (mod, funcs) in patch_funcs.items():
- for f, nargs in funcs:
- func = getattr(mod, f)
- trap_func = retarget_many_wrapper(new_root, nargs, func)
- self.patched_funcs.enter_context(
- mock.patch.object(mod, f, trap_func))
-
- def patchOpen(self, new_root):
- trap_func = retarget_many_wrapper(new_root, 1, open)
- self.patched_funcs.enter_context(
- mock.patch('builtins.open', trap_func)
- )
-
- def patchStdoutAndStderr(self, stdout=None, stderr=None):
- if stdout is not None:
- self.patched_funcs.enter_context(
- mock.patch.object(sys, 'stdout', stdout))
- if stderr is not None:
- self.patched_funcs.enter_context(
- mock.patch.object(sys, 'stderr', stderr))
-
- def reRoot(self, root=None):
- if root is None:
- root = self.tmp_dir()
- self.patchUtils(root)
- self.patchOS(root)
- self.patchOpen(root)
- return root
-
- @contextmanager
- def reRooted(self, root=None):
- try:
- yield self.reRoot(root)
- finally:
- self.patched_funcs.close()
-
-
-class HttprettyTestCase(CiTestCase):
- # necessary as http_proxy gets in the way of httpretty
- # https://github.com/gabrielfalcao/HTTPretty/issues/122
- # Also make sure that allow_net_connect is set to False.
- # And make sure reset and enable/disable are done.
-
- def setUp(self):
- self.restore_proxy = os.environ.get('http_proxy')
- if self.restore_proxy is not None:
- del os.environ['http_proxy']
- super(HttprettyTestCase, self).setUp()
- httpretty.HTTPretty.allow_net_connect = False
- httpretty.reset()
- httpretty.enable()
- # Stop the logging from HttpPretty so our logs don't get mixed
- # up with its logs
- logging.getLogger('httpretty.core').setLevel(logging.CRITICAL)
-
- def tearDown(self):
- httpretty.disable()
- httpretty.reset()
- if self.restore_proxy:
- os.environ['http_proxy'] = self.restore_proxy
- super(HttprettyTestCase, self).tearDown()
-
-
-class SchemaTestCaseMixin(unittest.TestCase):
-
- def assertSchemaValid(self, cfg, msg="Valid Schema failed validation."):
- """Assert the config is valid per self.schema.
-
- If there is only one top level key in the schema properties, then
- the cfg will be put under that key."""
- props = list(self.schema.get('properties'))
- # put cfg under top level key if there is only one in the schema
- if len(props) == 1:
- cfg = {props[0]: cfg}
- try:
- validate_cloudconfig_schema(cfg, self.schema, strict=True)
- except SchemaValidationError:
- self.fail(msg)
-
-
-def populate_dir(path, files):
- if not os.path.exists(path):
- os.makedirs(path)
- ret = []
- for (name, content) in files.items():
- p = os.path.sep.join([path, name])
- util.ensure_dir(os.path.dirname(p))
- with open(p, "wb") as fp:
- if isinstance(content, bytes):
- fp.write(content)
- else:
- fp.write(content.encode('utf-8'))
- fp.close()
- ret.append(p)
-
- return ret
-
-
-def populate_dir_with_ts(path, data):
- """data is {'file': ('contents', mtime)}. mtime relative to now."""
- populate_dir(path, dict((k, v[0]) for k, v in data.items()))
- btime = time.time()
- for fpath, (_contents, mtime) in data.items():
- ts = btime + mtime if mtime else btime
- os.utime(os.path.sep.join((path, fpath)), (ts, ts))
-
-
-def dir2dict(startdir, prefix=None):
- flist = {}
- if prefix is None:
- prefix = startdir
- for root, _dirs, files in os.walk(startdir):
- for fname in files:
- fpath = os.path.join(root, fname)
- key = fpath[len(prefix):]
- flist[key] = util.load_file(fpath)
- return flist
-
-
-def wrap_and_call(prefix, mocks, func, *args, **kwargs):
- """
- call func(args, **kwargs) with mocks applied, then unapplies mocks
- nicer to read than repeating dectorators on each function
-
- prefix: prefix for mock names (e.g. 'cloudinit.stages.util') or None
- mocks: dictionary of names (under 'prefix') to mock and either
- a return value or a dictionary to pass to the mock.patch call
- func: function to call with mocks applied
- *args,**kwargs: arguments for 'func'
-
- return_value: return from 'func'
- """
- delim = '.'
- if prefix is None:
- prefix = ''
- prefix = prefix.rstrip(delim)
- unwraps = []
- for fname, kw in mocks.items():
- if prefix:
- fname = delim.join((prefix, fname))
- if not isinstance(kw, dict):
- kw = {'return_value': kw}
- p = mock.patch(fname, **kw)
- p.start()
- unwraps.append(p)
- try:
- return func(*args, **kwargs)
- finally:
- for p in unwraps:
- p.stop()
-
-
-def resourceLocation(subname=None):
- path = os.path.join('tests', 'data')
- if not subname:
- return path
- return os.path.join(path, subname)
-
-
-def readResource(name, mode='r'):
- with open(resourceLocation(name), mode) as fh:
- return fh.read()
-
-
-try:
- import jsonschema
- assert jsonschema # avoid pyflakes error F401: import unused
- _missing_jsonschema_dep = False
-except ImportError:
- _missing_jsonschema_dep = True
-
-
-def skipUnlessJsonSchema():
- return skipIf(
- _missing_jsonschema_dep, "No python-jsonschema dependency present.")
-
-
-def skipUnlessJinja():
- return skipIf(not JINJA_AVAILABLE, "No jinja dependency present.")
-
-
-def skipIfJinja():
- return skipIf(JINJA_AVAILABLE, "Jinja dependency present.")
-
-
-# older versions of mock do not have the useful 'assert_not_called'
-if not hasattr(mock.Mock, 'assert_not_called'):
- def __mock_assert_not_called(mmock):
- if mmock.call_count != 0:
- msg = ("[citest] Expected '%s' to not have been called. "
- "Called %s times." %
- (mmock._mock_name or 'mock', mmock.call_count))
- raise AssertionError(msg)
- mock.Mock.assert_not_called = __mock_assert_not_called
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_conftest.py b/cloudinit/tests/test_conftest.py
deleted file mode 100644
index 6f1263a5..00000000
--- a/cloudinit/tests/test_conftest.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import pytest
-
-from cloudinit import subp
-from cloudinit.tests.helpers import CiTestCase
-
-
-class TestDisableSubpUsage:
- """Test that the disable_subp_usage fixture behaves as expected."""
-
- def test_using_subp_raises_assertion_error(self):
- with pytest.raises(AssertionError):
- subp.subp(["some", "args"])
-
- def test_typeerrors_on_incorrect_usage(self):
- with pytest.raises(TypeError):
- # We are intentionally passing no value for a parameter, so:
- # pylint: disable=no-value-for-parameter
- subp.subp()
-
- @pytest.mark.allow_all_subp
- def test_subp_usage_can_be_reenabled(self):
- subp.subp(['whoami'])
-
- @pytest.mark.allow_subp_for("whoami")
- def test_subp_usage_can_be_conditionally_reenabled(self):
- # The two parameters test each potential invocation with a single
- # argument
- with pytest.raises(AssertionError) as excinfo:
- subp.subp(["some", "args"])
- assert "allowed: whoami" in str(excinfo.value)
- subp.subp(['whoami'])
-
- @pytest.mark.allow_subp_for("whoami", "bash")
- def test_subp_usage_can_be_conditionally_reenabled_for_multiple_cmds(self):
- with pytest.raises(AssertionError) as excinfo:
- subp.subp(["some", "args"])
- assert "allowed: whoami,bash" in str(excinfo.value)
- subp.subp(['bash', '-c', 'true'])
- subp.subp(['whoami'])
-
- @pytest.mark.allow_all_subp
- @pytest.mark.allow_subp_for("bash")
- def test_both_marks_raise_an_error(self):
- with pytest.raises(AssertionError, match="marked both"):
- subp.subp(["bash"])
-
-
-class TestDisableSubpUsageInTestSubclass(CiTestCase):
- """Test that disable_subp_usage doesn't impact CiTestCase's subp logic."""
-
- def test_using_subp_raises_exception(self):
- with pytest.raises(Exception):
- subp.subp(["some", "args"])
-
- def test_typeerrors_on_incorrect_usage(self):
- with pytest.raises(TypeError):
- subp.subp()
-
- def test_subp_usage_can_be_reenabled(self):
- _old_allowed_subp = self.allow_subp
- self.allowed_subp = True
- try:
- subp.subp(['bash', '-c', 'true'])
- finally:
- self.allowed_subp = _old_allowed_subp
diff --git a/cloudinit/tests/test_dhclient_hook.py b/cloudinit/tests/test_dhclient_hook.py
deleted file mode 100644
index eadae81c..00000000
--- a/cloudinit/tests/test_dhclient_hook.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests for cloudinit.dhclient_hook."""
-
-from cloudinit import dhclient_hook as dhc
-from cloudinit.tests.helpers import CiTestCase, dir2dict, populate_dir
-
-import argparse
-import json
-import os
-from unittest import mock
-
-
-class TestDhclientHook(CiTestCase):
-
- ex_env = {
- 'interface': 'eth0',
- 'new_dhcp_lease_time': '3600',
- 'new_host_name': 'x1',
- 'new_ip_address': '10.145.210.163',
- 'new_subnet_mask': '255.255.255.0',
- 'old_host_name': 'x1',
- 'PATH': '/usr/sbin:/usr/bin:/sbin:/bin',
- 'pid': '614',
- 'reason': 'BOUND',
- }
-
- # some older versions of dhclient put the same content,
- # but in upper case with DHCP4_ instead of new_
- ex_env_dhcp4 = {
- 'REASON': 'BOUND',
- 'DHCP4_dhcp_lease_time': '3600',
- 'DHCP4_host_name': 'x1',
- 'DHCP4_ip_address': '10.145.210.163',
- 'DHCP4_subnet_mask': '255.255.255.0',
- 'INTERFACE': 'eth0',
- 'PATH': '/usr/sbin:/usr/bin:/sbin:/bin',
- 'pid': '614',
- }
-
- expected = {
- 'dhcp_lease_time': '3600',
- 'host_name': 'x1',
- 'ip_address': '10.145.210.163',
- 'subnet_mask': '255.255.255.0'}
-
- def setUp(self):
- super(TestDhclientHook, self).setUp()
- self.tmp = self.tmp_dir()
-
- def test_handle_args(self):
- """quick test of call to handle_args."""
- nic = 'eth0'
- args = argparse.Namespace(event=dhc.UP, interface=nic)
- with mock.patch.dict("os.environ", clear=True, values=self.ex_env):
- dhc.handle_args(dhc.NAME, args, data_d=self.tmp)
- found = dir2dict(self.tmp + os.path.sep)
- self.assertEqual([nic + ".json"], list(found.keys()))
- self.assertEqual(self.expected, json.loads(found[nic + ".json"]))
-
- def test_run_hook_up_creates_dir(self):
- """If dir does not exist, run_hook should create it."""
- subd = self.tmp_path("subdir", self.tmp)
- nic = 'eth1'
- dhc.run_hook(nic, 'up', data_d=subd, env=self.ex_env)
- self.assertEqual(
- set([nic + ".json"]), set(dir2dict(subd + os.path.sep)))
-
- def test_run_hook_up(self):
- """Test expected use of run_hook_up."""
- nic = 'eth0'
- dhc.run_hook(nic, 'up', data_d=self.tmp, env=self.ex_env)
- found = dir2dict(self.tmp + os.path.sep)
- self.assertEqual([nic + ".json"], list(found.keys()))
- self.assertEqual(self.expected, json.loads(found[nic + ".json"]))
-
- def test_run_hook_up_dhcp4_prefix(self):
- """Test run_hook filters correctly with older DHCP4_ data."""
- nic = 'eth0'
- dhc.run_hook(nic, 'up', data_d=self.tmp, env=self.ex_env_dhcp4)
- found = dir2dict(self.tmp + os.path.sep)
- self.assertEqual([nic + ".json"], list(found.keys()))
- self.assertEqual(self.expected, json.loads(found[nic + ".json"]))
-
- def test_run_hook_down_deletes(self):
- """down should delete the created json file."""
- nic = 'eth1'
- populate_dir(
- self.tmp, {nic + ".json": "{'abcd'}", 'myfile.txt': 'text'})
- dhc.run_hook(nic, 'down', data_d=self.tmp, env={'old_host_name': 'x1'})
- self.assertEqual(
- set(['myfile.txt']),
- set(dir2dict(self.tmp + os.path.sep)))
-
- def test_get_parser(self):
- """Smoke test creation of get_parser."""
- # cloud-init main uses 'action'.
- event, interface = (dhc.UP, 'mynic0')
- self.assertEqual(
- argparse.Namespace(event=event, interface=interface,
- action=(dhc.NAME, dhc.handle_args)),
- dhc.get_parser().parse_args([event, interface]))
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_dmi.py b/cloudinit/tests/test_dmi.py
deleted file mode 100644
index 78a72122..00000000
--- a/cloudinit/tests/test_dmi.py
+++ /dev/null
@@ -1,154 +0,0 @@
-from cloudinit.tests import helpers
-from cloudinit import dmi
-from cloudinit import util
-from cloudinit import subp
-
-import os
-import tempfile
-import shutil
-from unittest import mock
-
-
-class TestReadDMIData(helpers.FilesystemMockingTestCase):
-
- def setUp(self):
- super(TestReadDMIData, self).setUp()
- self.new_root = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, self.new_root)
- self.reRoot(self.new_root)
- p = mock.patch("cloudinit.dmi.is_container", return_value=False)
- self.addCleanup(p.stop)
- self._m_is_container = p.start()
- p = mock.patch("cloudinit.dmi.is_FreeBSD", return_value=False)
- self.addCleanup(p.stop)
- self._m_is_FreeBSD = p.start()
-
- def _create_sysfs_parent_directory(self):
- util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id'))
-
- def _create_sysfs_file(self, key, content):
- """Mocks the sys path found on Linux systems."""
- self._create_sysfs_parent_directory()
- dmi_key = "/sys/class/dmi/id/{0}".format(key)
- util.write_file(dmi_key, content)
-
- def _configure_dmidecode_return(self, key, content, error=None):
- """
- In order to test a missing sys path and call outs to dmidecode, this
- function fakes the results of dmidecode to test the results.
- """
- def _dmidecode_subp(cmd):
- if cmd[-1] != key:
- raise subp.ProcessExecutionError()
- return (content, error)
-
- self.patched_funcs.enter_context(
- mock.patch("cloudinit.dmi.subp.which", side_effect=lambda _: True))
- self.patched_funcs.enter_context(
- mock.patch("cloudinit.dmi.subp.subp", side_effect=_dmidecode_subp))
-
- def _configure_kenv_return(self, key, content, error=None):
- """
- In order to test a FreeBSD system call outs to kenv, this
- function fakes the results of kenv to test the results.
- """
- def _kenv_subp(cmd):
- if cmd[-1] != dmi.DMIDECODE_TO_KERNEL[key].freebsd:
- raise subp.ProcessExecutionError()
- return (content, error)
-
- self.patched_funcs.enter_context(
- mock.patch("cloudinit.dmi.subp.subp", side_effect=_kenv_subp))
-
- def patch_mapping(self, new_mapping):
- self.patched_funcs.enter_context(
- mock.patch('cloudinit.dmi.DMIDECODE_TO_KERNEL',
- new_mapping))
-
- def test_sysfs_used_with_key_in_mapping_and_file_on_disk(self):
- self.patch_mapping({'mapped-key': dmi.kdmi('mapped-value', None)})
- expected_dmi_value = 'sys-used-correctly'
- self._create_sysfs_file('mapped-value', expected_dmi_value)
- self._configure_dmidecode_return('mapped-key', 'wrong-wrong-wrong')
- self.assertEqual(expected_dmi_value, dmi.read_dmi_data('mapped-key'))
-
- def test_dmidecode_used_if_no_sysfs_file_on_disk(self):
- self.patch_mapping({})
- self._create_sysfs_parent_directory()
- expected_dmi_value = 'dmidecode-used'
- self._configure_dmidecode_return('use-dmidecode', expected_dmi_value)
- with mock.patch("cloudinit.util.os.uname") as m_uname:
- m_uname.return_value = ('x-sysname', 'x-nodename',
- 'x-release', 'x-version', 'x86_64')
- self.assertEqual(expected_dmi_value,
- dmi.read_dmi_data('use-dmidecode'))
-
- def test_dmidecode_not_used_on_arm(self):
- self.patch_mapping({})
- print("current =%s", subp)
- self._create_sysfs_parent_directory()
- dmi_val = 'from-dmidecode'
- dmi_name = 'use-dmidecode'
- self._configure_dmidecode_return(dmi_name, dmi_val)
- print("now =%s", subp)
-
- expected = {'armel': None, 'aarch64': dmi_val, 'x86_64': dmi_val}
- found = {}
- # we do not run the 'dmi-decode' binary on some arches
- # verify that anything requested that is not in the sysfs dir
- # will return None on those arches.
- with mock.patch("cloudinit.util.os.uname") as m_uname:
- for arch in expected:
- m_uname.return_value = ('x-sysname', 'x-nodename',
- 'x-release', 'x-version', arch)
- print("now2 =%s", subp)
- found[arch] = dmi.read_dmi_data(dmi_name)
- self.assertEqual(expected, found)
-
- def test_none_returned_if_neither_source_has_data(self):
- self.patch_mapping({})
- self._configure_dmidecode_return('key', 'value')
- self.assertIsNone(dmi.read_dmi_data('expect-fail'))
-
- def test_none_returned_if_dmidecode_not_in_path(self):
- self.patched_funcs.enter_context(
- mock.patch.object(subp, 'which', lambda _: False))
- self.patch_mapping({})
- self.assertIsNone(dmi.read_dmi_data('expect-fail'))
-
- def test_empty_string_returned_instead_of_foxfox(self):
- # uninitialized dmi values show as \xff, return empty string
- my_len = 32
- dmi_value = b'\xff' * my_len + b'\n'
- expected = ""
- dmi_key = 'system-product-name'
- sysfs_key = 'product_name'
- self._create_sysfs_file(sysfs_key, dmi_value)
- self.assertEqual(expected, dmi.read_dmi_data(dmi_key))
-
- def test_container_returns_none(self):
- """In a container read_dmi_data should always return None."""
-
- # first verify we get the value if not in container
- self._m_is_container.return_value = False
- key, val = ("system-product-name", "my_product")
- self._create_sysfs_file('product_name', val)
- self.assertEqual(val, dmi.read_dmi_data(key))
-
- # then verify in container returns None
- self._m_is_container.return_value = True
- self.assertIsNone(dmi.read_dmi_data(key))
-
- def test_container_returns_none_on_unknown(self):
- """In a container even bogus keys return None."""
- self._m_is_container.return_value = True
- self._create_sysfs_file('product_name', "should-be-ignored")
- self.assertIsNone(dmi.read_dmi_data("bogus"))
- self.assertIsNone(dmi.read_dmi_data("system-product-name"))
-
- def test_freebsd_uses_kenv(self):
- """On a FreeBSD system, kenv is called."""
- self._m_is_FreeBSD.return_value = True
- key, val = ("system-product-name", "my_product")
- self._configure_kenv_return(key, val)
- self.assertEqual(dmi.read_dmi_data(key), val)
diff --git a/cloudinit/tests/test_event.py b/cloudinit/tests/test_event.py
deleted file mode 100644
index 3da4c70c..00000000
--- a/cloudinit/tests/test_event.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-"""Tests related to cloudinit.event module."""
-from cloudinit.event import EventType, EventScope, userdata_to_events
-
-
-class TestEvent:
- def test_userdata_to_events(self):
- userdata = {'network': {'when': ['boot']}}
- expected = {EventScope.NETWORK: {EventType.BOOT}}
- assert expected == userdata_to_events(userdata)
-
- def test_invalid_scope(self, caplog):
- userdata = {'networkasdfasdf': {'when': ['boot']}}
- userdata_to_events(userdata)
- assert (
- "'networkasdfasdf' is not a valid EventScope! Update data "
- "will be ignored for 'networkasdfasdf' scope"
- ) in caplog.text
-
- def test_invalid_event(self, caplog):
- userdata = {'network': {'when': ['bootasdfasdf']}}
- userdata_to_events(userdata)
- assert (
- "'bootasdfasdf' is not a valid EventType! Update data "
- "will be ignored for 'network' scope"
- ) in caplog.text
diff --git a/cloudinit/tests/test_features.py b/cloudinit/tests/test_features.py
deleted file mode 100644
index d7a7226d..00000000
--- a/cloudinit/tests/test_features.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-# pylint: disable=no-member,no-name-in-module
-"""
-This file is for testing the feature flag functionality itself,
-NOT for testing any individual feature flag
-"""
-import pytest
-import sys
-from pathlib import Path
-
-import cloudinit
-
-
-@pytest.yield_fixture()
-def create_override(request):
- """
- Create a feature overrides file and do some module wizardry to make
- it seem like we're importing the features file for the first time.
-
- After creating the override file with the values passed by the test,
- we need to reload cloudinit.features
- to get all of the current features (including the overridden ones).
- Once the test is complete, we remove the file we created and set
- features and feature_overrides modules to how they were before
- the test started
- """
- override_path = Path(cloudinit.__file__).parent / 'feature_overrides.py'
- if override_path.exists():
- raise Exception("feature_overrides.py unexpectedly exists! "
- "Remove it to run this test.")
- with override_path.open('w') as f:
- for key, value in request.param.items():
- f.write('{} = {}\n'.format(key, value))
-
- sys.modules.pop('cloudinit.features', None)
-
- yield
-
- override_path.unlink()
- sys.modules.pop('cloudinit.feature_overrides', None)
-
-
-class TestFeatures:
- def test_feature_without_override(self):
- from cloudinit.features import ERROR_ON_USER_DATA_FAILURE
- assert ERROR_ON_USER_DATA_FAILURE is True
-
- @pytest.mark.parametrize('create_override',
- [{'ERROR_ON_USER_DATA_FAILURE': False}],
- indirect=True)
- def test_feature_with_override(self, create_override):
- from cloudinit.features import ERROR_ON_USER_DATA_FAILURE
- assert ERROR_ON_USER_DATA_FAILURE is False
-
- @pytest.mark.parametrize('create_override',
- [{'SPAM': True}],
- indirect=True)
- def test_feature_only_in_override(self, create_override):
- from cloudinit.features import SPAM
- assert SPAM is True
diff --git a/cloudinit/tests/test_gpg.py b/cloudinit/tests/test_gpg.py
deleted file mode 100644
index 311dfad6..00000000
--- a/cloudinit/tests/test_gpg.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-"""Test gpg module."""
-
-from unittest import mock
-
-from cloudinit import gpg
-from cloudinit import subp
-from cloudinit.tests.helpers import CiTestCase
-
-
-@mock.patch("cloudinit.gpg.time.sleep")
-@mock.patch("cloudinit.gpg.subp.subp")
-class TestReceiveKeys(CiTestCase):
- """Test the recv_key method."""
-
- def test_retries_on_subp_exc(self, m_subp, m_sleep):
- """retry should be done on gpg receive keys failure."""
- retries = (1, 2, 4)
- my_exc = subp.ProcessExecutionError(
- stdout='', stderr='', exit_code=2, cmd=['mycmd'])
- m_subp.side_effect = (my_exc, my_exc, ('', ''))
- gpg.recv_key("ABCD", "keyserver.example.com", retries=retries)
- self.assertEqual([mock.call(1), mock.call(2)], m_sleep.call_args_list)
-
- def test_raises_error_after_retries(self, m_subp, m_sleep):
- """If the final run fails, error should be raised."""
- naplen = 1
- keyid, keyserver = ("ABCD", "keyserver.example.com")
- m_subp.side_effect = subp.ProcessExecutionError(
- stdout='', stderr='', exit_code=2, cmd=['mycmd'])
- with self.assertRaises(ValueError) as rcm:
- gpg.recv_key(keyid, keyserver, retries=(naplen,))
- self.assertIn(keyid, str(rcm.exception))
- self.assertIn(keyserver, str(rcm.exception))
- m_sleep.assert_called_with(naplen)
-
- def test_no_retries_on_none(self, m_subp, m_sleep):
- """retry should not be done if retries is None."""
- m_subp.side_effect = subp.ProcessExecutionError(
- stdout='', stderr='', exit_code=2, cmd=['mycmd'])
- with self.assertRaises(ValueError):
- gpg.recv_key("ABCD", "keyserver.example.com", retries=None)
- m_sleep.assert_not_called()
-
- def test_expected_gpg_command(self, m_subp, m_sleep):
- """Verify gpg is called with expected args."""
- key, keyserver = ("DEADBEEF", "keyserver.example.com")
- retries = (1, 2, 4)
- m_subp.return_value = ('', '')
- gpg.recv_key(key, keyserver, retries=retries)
- m_subp.assert_called_once_with(
- ['gpg', '--no-tty',
- '--keyserver=%s' % keyserver, '--recv-keys', key],
- capture=True)
- m_sleep.assert_not_called()
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
deleted file mode 100644
index e44b16d8..00000000
--- a/cloudinit/tests/test_netinfo.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests netinfo module functions and classes."""
-
-from copy import copy
-
-from cloudinit.netinfo import netdev_info, netdev_pformat, route_pformat
-from cloudinit.tests.helpers import CiTestCase, mock, readResource
-
-
-# Example ifconfig and route output
-SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")
-SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")
-SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output")
-SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
-SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
-SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
-SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
-SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
-NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
-ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
-FREEBSD_NETDEV_OUT = readResource("netinfo/freebsd-netdev-formatted-output")
-
-
-class TestNetInfo(CiTestCase):
-
- maxDiff = None
- with_logs = True
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_old_nettools_pformat(self, m_subp, m_which):
- """netdev_pformat properly rendering old nettools info."""
- m_subp.return_value = (SAMPLE_OLD_IFCONFIG_OUT, '')
- m_which.side_effect = lambda x: x if x == 'ifconfig' else None
- content = netdev_pformat()
- self.assertEqual(NETDEV_FORMATTED_OUT, content)
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_new_nettools_pformat(self, m_subp, m_which):
- """netdev_pformat properly rendering netdev new nettools info."""
- m_subp.return_value = (SAMPLE_NEW_IFCONFIG_OUT, '')
- m_which.side_effect = lambda x: x if x == 'ifconfig' else None
- content = netdev_pformat()
- self.assertEqual(NETDEV_FORMATTED_OUT, content)
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_freebsd_nettools_pformat(self, m_subp, m_which):
- """netdev_pformat properly rendering netdev new nettools info."""
- m_subp.return_value = (SAMPLE_FREEBSD_IFCONFIG_OUT, '')
- m_which.side_effect = lambda x: x if x == 'ifconfig' else None
- content = netdev_pformat()
- print()
- print(content)
- print()
- self.assertEqual(FREEBSD_NETDEV_OUT, content)
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_iproute_pformat(self, m_subp, m_which):
- """netdev_pformat properly rendering ip route info."""
- m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')
- m_which.side_effect = lambda x: x if x == 'ip' else None
- content = netdev_pformat()
- new_output = copy(NETDEV_FORMATTED_OUT)
- # ip route show describes global scopes on ipv4 addresses
- # whereas ifconfig does not. Add proper global/host scope to output.
- new_output = new_output.replace('| . | 50:7b', '| global | 50:7b')
- new_output = new_output.replace(
- '255.0.0.0 | . |', '255.0.0.0 | host |')
- self.assertEqual(new_output, content)
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_warn_on_missing_commands(self, m_subp, m_which):
- """netdev_pformat warns when missing both ip and 'netstat'."""
- m_which.return_value = None # Niether ip nor netstat found
- content = netdev_pformat()
- self.assertEqual('\n', content)
- self.assertEqual(
- "WARNING: Could not print networks: missing 'ip' and 'ifconfig'"
- " commands\n",
- self.logs.getvalue())
- m_subp.assert_not_called()
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_info_nettools_down(self, m_subp, m_which):
- """test netdev_info using nettools and down interfaces."""
- m_subp.return_value = (
- readResource("netinfo/new-ifconfig-output-down"), "")
- m_which.side_effect = lambda x: x if x == 'ifconfig' else None
- self.assertEqual(
- {'eth0': {'ipv4': [], 'ipv6': [],
- 'hwaddr': '00:16:3e:de:51:a6', 'up': False},
- 'lo': {'ipv4': [{'ip': '127.0.0.1', 'mask': '255.0.0.0'}],
- 'ipv6': [{'ip': '::1/128', 'scope6': 'host'}],
- 'hwaddr': '.', 'up': True}},
- netdev_info("."))
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_netdev_info_iproute_down(self, m_subp, m_which):
- """Test netdev_info with ip and down interfaces."""
- m_subp.return_value = (
- readResource("netinfo/sample-ipaddrshow-output-down"), "")
- m_which.side_effect = lambda x: x if x == 'ip' else None
- self.assertEqual(
- {'lo': {'ipv4': [{'ip': '127.0.0.1', 'bcast': '.',
- 'mask': '255.0.0.0', 'scope': 'host'}],
- 'ipv6': [{'ip': '::1/128', 'scope6': 'host'}],
- 'hwaddr': '.', 'up': True},
- 'eth0': {'ipv4': [], 'ipv6': [],
- 'hwaddr': '00:16:3e:de:51:a6', 'up': False}},
- netdev_info("."))
-
- @mock.patch('cloudinit.netinfo.netdev_info')
- def test_netdev_pformat_with_down(self, m_netdev_info):
- """test netdev_pformat when netdev_info returns 'down' interfaces."""
- m_netdev_info.return_value = (
- {'lo': {'ipv4': [{'ip': '127.0.0.1', 'mask': '255.0.0.0',
- 'scope': 'host'}],
- 'ipv6': [{'ip': '::1/128', 'scope6': 'host'}],
- 'hwaddr': '.', 'up': True},
- 'eth0': {'ipv4': [], 'ipv6': [],
- 'hwaddr': '00:16:3e:de:51:a6', 'up': False}})
- self.assertEqual(
- readResource("netinfo/netdev-formatted-output-down"),
- netdev_pformat())
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_route_nettools_pformat(self, m_subp, m_which):
- """route_pformat properly rendering nettools route info."""
-
- def subp_netstat_route_selector(*args, **kwargs):
- if args[0] == ['netstat', '--route', '--numeric', '--extend']:
- return (SAMPLE_ROUTE_OUT_V4, '')
- if args[0] == ['netstat', '-A', 'inet6', '--route', '--numeric']:
- return (SAMPLE_ROUTE_OUT_V6, '')
- raise Exception('Unexpected subp call %s' % args[0])
-
- m_subp.side_effect = subp_netstat_route_selector
- m_which.side_effect = lambda x: x if x == 'netstat' else None
- content = route_pformat()
- self.assertEqual(ROUTE_FORMATTED_OUT, content)
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_route_iproute_pformat(self, m_subp, m_which):
- """route_pformat properly rendering ip route info."""
-
- def subp_iproute_selector(*args, **kwargs):
- if ['ip', '-o', 'route', 'list'] == args[0]:
- return (SAMPLE_IPROUTE_OUT_V4, '')
- v6cmd = ['ip', '--oneline', '-6', 'route', 'list', 'table', 'all']
- if v6cmd == args[0]:
- return (SAMPLE_IPROUTE_OUT_V6, '')
- raise Exception('Unexpected subp call %s' % args[0])
-
- m_subp.side_effect = subp_iproute_selector
- m_which.side_effect = lambda x: x if x == 'ip' else None
- content = route_pformat()
- self.assertEqual(ROUTE_FORMATTED_OUT, content)
-
- @mock.patch('cloudinit.netinfo.subp.which')
- @mock.patch('cloudinit.netinfo.subp.subp')
- def test_route_warn_on_missing_commands(self, m_subp, m_which):
- """route_pformat warns when missing both ip and 'netstat'."""
- m_which.return_value = None # Niether ip nor netstat found
- content = route_pformat()
- self.assertEqual('\n', content)
- self.assertEqual(
- "WARNING: Could not print routes: missing 'ip' and 'netstat'"
- " commands\n",
- self.logs.getvalue())
- m_subp.assert_not_called()
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_persistence.py b/cloudinit/tests/test_persistence.py
deleted file mode 100644
index ec1152a9..00000000
--- a/cloudinit/tests/test_persistence.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright (C) 2020 Canonical Ltd.
-#
-# Author: Daniel Watkins <oddbloke@ubuntu.com>
-#
-# This file is part of cloud-init. See LICENSE file for license information.
-"""
-Tests for cloudinit.persistence.
-
-Per https://docs.python.org/3/library/pickle.html, only "classes that are
-defined at the top level of a module" can be pickled. This means that all of
-our ``CloudInitPickleMixin`` subclasses for testing must be defined at
-module-level (rather than being defined inline or dynamically in the body of
-test methods, as we would do without this constraint).
-
-``TestPickleMixin.test_subclasses`` iterates over a list of all of these
-classes, and tests that they round-trip through a pickle dump/load. As the
-interface we're testing is that ``_unpickle`` is called appropriately on
-subclasses, our subclasses define their assertions in their ``_unpickle``
-implementation. (This means that the assertions will not be executed if
-``_unpickle`` is not called at all; we have
-``TestPickleMixin.test_unpickle_called`` to ensure it is called.)
-
-To avoid manually maintaining a list of classes for parametrization we use a
-simple metaclass, ``_Collector``, to gather them up.
-"""
-
-import pickle
-from unittest import mock
-
-import pytest
-
-from cloudinit.persistence import CloudInitPickleMixin
-
-
-class _Collector(type):
- """Any class using this as a metaclass will be stored in test_classes."""
-
- test_classes = []
-
- def __new__(cls, *args):
- new_cls = super().__new__(cls, *args)
- _Collector.test_classes.append(new_cls)
- return new_cls
-
-
-class InstanceVersionNotUsed(CloudInitPickleMixin, metaclass=_Collector):
- """Test that the class version is used over one set in instance state."""
-
- _ci_pkl_version = 1
-
- def __init__(self):
- self._ci_pkl_version = 2
-
- def _unpickle(self, ci_pkl_version: int) -> None:
- assert 1 == ci_pkl_version
-
-
-class MissingVersionHandled(CloudInitPickleMixin, metaclass=_Collector):
- """Test that pickles without ``_ci_pkl_version`` are handled gracefully.
-
- This is tested by overriding ``__getstate__`` so the dumped pickle of this
- class will not have ``_ci_pkl_version`` included.
- """
-
- def __getstate__(self):
- return self.__dict__
-
- def _unpickle(self, ci_pkl_version: int) -> None:
- assert 0 == ci_pkl_version
-
-
-class OverridenVersionHonored(CloudInitPickleMixin, metaclass=_Collector):
- """Test that the subclass's version is used."""
-
- _ci_pkl_version = 1
-
- def _unpickle(self, ci_pkl_version: int) -> None:
- assert 1 == ci_pkl_version
-
-
-class StateIsRestored(CloudInitPickleMixin, metaclass=_Collector):
- """Instance state should be restored before ``_unpickle`` is called."""
-
- def __init__(self):
- self.some_state = "some state"
-
- def _unpickle(self, ci_pkl_version: int) -> None:
- assert "some state" == self.some_state
-
-
-class UnpickleCanBeUnoverriden(CloudInitPickleMixin, metaclass=_Collector):
- """Subclasses should not need to override ``_unpickle``."""
-
-
-class VersionDefaultsToZero(CloudInitPickleMixin, metaclass=_Collector):
- """Test that the default version is 0."""
-
- def _unpickle(self, ci_pkl_version: int) -> None:
- assert 0 == ci_pkl_version
-
-
-class VersionIsPoppedFromState(CloudInitPickleMixin, metaclass=_Collector):
- """Test _ci_pkl_version is popped from state before being restored."""
-
- def _unpickle(self, ci_pkl_version: int) -> None:
- # `self._ci_pkl_version` returns the type's _ci_pkl_version if it isn't
- # in instance state, so we need to explicitly check self.__dict__.
- assert "_ci_pkl_version" not in self.__dict__
-
-
-class TestPickleMixin:
- def test_unpickle_called(self):
- """Test that self._unpickle is called on unpickle."""
- with mock.patch.object(
- CloudInitPickleMixin, "_unpickle"
- ) as m_unpickle:
- pickle.loads(pickle.dumps(CloudInitPickleMixin()))
- assert 1 == m_unpickle.call_count
-
- @pytest.mark.parametrize("cls", _Collector.test_classes)
- def test_subclasses(self, cls):
- """For each collected class, round-trip through pickle dump/load.
-
- Assertions are implemented in ``cls._unpickle``, and so are evoked as
- part of the pickle load.
- """
- pickle.loads(pickle.dumps(cls()))
diff --git a/cloudinit/tests/test_simpletable.py b/cloudinit/tests/test_simpletable.py
deleted file mode 100644
index a12a62a0..00000000
--- a/cloudinit/tests/test_simpletable.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright (C) 2017 Amazon.com, Inc. or its affiliates
-#
-# Author: Andrew Jorgensen <ajorgens@amazon.com>
-#
-# This file is part of cloud-init. See LICENSE file for license information.
-"""Tests that SimpleTable works just like PrettyTable for cloud-init.
-
-Not all possible PrettyTable cases are tested because we're not trying to
-reimplement the entire library, only the minimal parts we actually use.
-"""
-
-from cloudinit.simpletable import SimpleTable
-from cloudinit.tests.helpers import CiTestCase
-
-# Examples rendered by cloud-init using PrettyTable
-NET_DEVICE_FIELDS = (
- 'Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address')
-NET_DEVICE_ROWS = (
- ('ens3', True, '172.31.4.203', '255.255.240.0', '.', '0a:1f:07:15:98:70'),
- ('ens3', True, 'fe80::81f:7ff:fe15:9870/64', '.', 'link',
- '0a:1f:07:15:98:70'),
- ('lo', True, '127.0.0.1', '255.0.0.0', '.', '.'),
- ('lo', True, '::1/128', '.', 'host', '.'),
-)
-NET_DEVICE_TABLE = """\
-+--------+------+----------------------------+---------------+-------+-------------------+
-| Device | Up | Address | Mask | Scope | Hw-Address |
-+--------+------+----------------------------+---------------+-------+-------------------+
-| ens3 | True | 172.31.4.203 | 255.255.240.0 | . | 0a:1f:07:15:98:70 |
-| ens3 | True | fe80::81f:7ff:fe15:9870/64 | . | link | 0a:1f:07:15:98:70 |
-| lo | True | 127.0.0.1 | 255.0.0.0 | . | . |
-| lo | True | ::1/128 | . | host | . |
-+--------+------+----------------------------+---------------+-------+-------------------+""" # noqa: E501
-ROUTE_IPV4_FIELDS = (
- 'Route', 'Destination', 'Gateway', 'Genmask', 'Interface', 'Flags')
-ROUTE_IPV4_ROWS = (
- ('0', '0.0.0.0', '172.31.0.1', '0.0.0.0', 'ens3', 'UG'),
- ('1', '169.254.0.0', '0.0.0.0', '255.255.0.0', 'ens3', 'U'),
- ('2', '172.31.0.0', '0.0.0.0', '255.255.240.0', 'ens3', 'U'),
-)
-ROUTE_IPV4_TABLE = """\
-+-------+-------------+------------+---------------+-----------+-------+
-| Route | Destination | Gateway | Genmask | Interface | Flags |
-+-------+-------------+------------+---------------+-----------+-------+
-| 0 | 0.0.0.0 | 172.31.0.1 | 0.0.0.0 | ens3 | UG |
-| 1 | 169.254.0.0 | 0.0.0.0 | 255.255.0.0 | ens3 | U |
-| 2 | 172.31.0.0 | 0.0.0.0 | 255.255.240.0 | ens3 | U |
-+-------+-------------+------------+---------------+-----------+-------+"""
-
-AUTHORIZED_KEYS_FIELDS = (
- 'Keytype', 'Fingerprint (md5)', 'Options', 'Comment')
-AUTHORIZED_KEYS_ROWS = (
- ('ssh-rsa', '24:c7:41:49:47:12:31:a0:de:6f:62:79:9b:13:06:36', '-',
- 'ajorgens'),
-)
-AUTHORIZED_KEYS_TABLE = """\
-+---------+-------------------------------------------------+---------+----------+
-| Keytype | Fingerprint (md5) | Options | Comment |
-+---------+-------------------------------------------------+---------+----------+
-| ssh-rsa | 24:c7:41:49:47:12:31:a0:de:6f:62:79:9b:13:06:36 | - | ajorgens |
-+---------+-------------------------------------------------+---------+----------+""" # noqa: E501
-
-# from prettytable import PrettyTable
-# pt = PrettyTable(('HEADER',))
-# print(pt)
-NO_ROWS_FIELDS = ('HEADER',)
-NO_ROWS_TABLE = """\
-+--------+
-| HEADER |
-+--------+
-+--------+"""
-
-
-class TestSimpleTable(CiTestCase):
-
- def test_no_rows(self):
- """An empty table is rendered as PrettyTable would have done it."""
- table = SimpleTable(NO_ROWS_FIELDS)
- self.assertEqual(str(table), NO_ROWS_TABLE)
-
- def test_net_dev(self):
- """Net device info is rendered as it was with PrettyTable."""
- table = SimpleTable(NET_DEVICE_FIELDS)
- for row in NET_DEVICE_ROWS:
- table.add_row(row)
- self.assertEqual(str(table), NET_DEVICE_TABLE)
-
- def test_route_ipv4(self):
- """Route IPv4 info is rendered as it was with PrettyTable."""
- table = SimpleTable(ROUTE_IPV4_FIELDS)
- for row in ROUTE_IPV4_ROWS:
- table.add_row(row)
- self.assertEqual(str(table), ROUTE_IPV4_TABLE)
-
- def test_authorized_keys(self):
- """SSH authorized keys are rendered as they were with PrettyTable."""
- table = SimpleTable(AUTHORIZED_KEYS_FIELDS)
- for row in AUTHORIZED_KEYS_ROWS:
- table.add_row(row)
-
- def test_get_string(self):
- """get_string() method returns the same content as str()."""
- table = SimpleTable(AUTHORIZED_KEYS_FIELDS)
- for row in AUTHORIZED_KEYS_ROWS:
- table.add_row(row)
- self.assertEqual(table.get_string(), str(table))
diff --git a/cloudinit/tests/test_stages.py b/cloudinit/tests/test_stages.py
deleted file mode 100644
index a50836a4..00000000
--- a/cloudinit/tests/test_stages.py
+++ /dev/null
@@ -1,478 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests related to cloudinit.stages module."""
-import os
-import stat
-
-import pytest
-
-from cloudinit import stages
-from cloudinit import sources
-from cloudinit.sources import NetworkConfigSource
-
-from cloudinit.event import EventScope, EventType
-from cloudinit.util import write_file
-
-from cloudinit.tests.helpers import CiTestCase, mock
-
-TEST_INSTANCE_ID = 'i-testing'
-
-
-class FakeDataSource(sources.DataSource):
-
- def __init__(self, paths=None, userdata=None, vendordata=None,
- network_config=''):
- super(FakeDataSource, self).__init__({}, None, paths=paths)
- self.metadata = {'instance-id': TEST_INSTANCE_ID}
- self.userdata_raw = userdata
- self.vendordata_raw = vendordata
- self._network_config = None
- if network_config: # Permit for None value to setup attribute
- self._network_config = network_config
-
- @property
- def network_config(self):
- return self._network_config
-
- def _get_data(self):
- return True
-
-
-class TestInit(CiTestCase):
- with_logs = True
- allowed_subp = False
-
- def setUp(self):
- super(TestInit, self).setUp()
- self.tmpdir = self.tmp_dir()
- self.init = stages.Init()
- # Setup fake Paths for Init to reference
- self.init._cfg = {'system_info': {
- 'distro': 'ubuntu', 'paths': {'cloud_dir': self.tmpdir,
- 'run_dir': self.tmpdir}}}
- self.init.datasource = FakeDataSource(paths=self.init.paths)
- self._real_is_new_instance = self.init.is_new_instance
- self.init.is_new_instance = mock.Mock(return_value=True)
-
- def test_wb__find_networking_config_disabled(self):
- """find_networking_config returns no config when disabled."""
- disable_file = os.path.join(
- self.init.paths.get_cpath('data'), 'upgraded-network')
- write_file(disable_file, '')
- self.assertEqual(
- (None, disable_file),
- self.init._find_networking_config())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_disabled_by_kernel(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns when disabled by kernel cmdline."""
- m_cmdline.return_value = {'config': 'disabled'}
- m_initramfs.return_value = {'config': ['fake_initrd']}
- self.assertEqual(
- (None, NetworkConfigSource.cmdline),
- self.init._find_networking_config())
- self.assertEqual('DEBUG: network config disabled by cmdline\n',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_disabled_by_initrd(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns when disabled by kernel cmdline."""
- m_cmdline.return_value = {}
- m_initramfs.return_value = {'config': 'disabled'}
- self.assertEqual(
- (None, NetworkConfigSource.initramfs),
- self.init._find_networking_config())
- self.assertEqual('DEBUG: network config disabled by initramfs\n',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_disabled_by_datasrc(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns when disabled by datasource cfg."""
- m_cmdline.return_value = {} # Kernel doesn't disable networking
- m_initramfs.return_value = {} # initramfs doesn't disable networking
- self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
- 'network': {}} # system config doesn't disable
-
- self.init.datasource = FakeDataSource(
- network_config={'config': 'disabled'})
- self.assertEqual(
- (None, NetworkConfigSource.ds),
- self.init._find_networking_config())
- self.assertEqual('DEBUG: network config disabled by ds\n',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_disabled_by_sysconfig(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns when disabled by system config."""
- m_cmdline.return_value = {} # Kernel doesn't disable networking
- m_initramfs.return_value = {} # initramfs doesn't disable networking
- self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
- 'network': {'config': 'disabled'}}
- self.assertEqual(
- (None, NetworkConfigSource.system_cfg),
- self.init._find_networking_config())
- self.assertEqual('DEBUG: network config disabled by system_cfg\n',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test__find_networking_config_uses_datasrc_order(
- self, m_cmdline, m_initramfs):
- """find_networking_config should check sources in DS defined order"""
- # cmdline and initramfs, which would normally be preferred over other
- # sources, disable networking; in this case, though, the DS moves them
- # later so its own config is preferred
- m_cmdline.return_value = {'config': 'disabled'}
- m_initramfs.return_value = {'config': 'disabled'}
-
- ds_net_cfg = {'config': {'needle': True}}
- self.init.datasource = FakeDataSource(network_config=ds_net_cfg)
- self.init.datasource.network_config_sources = [
- NetworkConfigSource.ds, NetworkConfigSource.system_cfg,
- NetworkConfigSource.cmdline, NetworkConfigSource.initramfs]
-
- self.assertEqual(
- (ds_net_cfg, NetworkConfigSource.ds),
- self.init._find_networking_config())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test__find_networking_config_warns_if_datasrc_uses_invalid_src(
- self, m_cmdline, m_initramfs):
- """find_networking_config should check sources in DS defined order"""
- ds_net_cfg = {'config': {'needle': True}}
- self.init.datasource = FakeDataSource(network_config=ds_net_cfg)
- self.init.datasource.network_config_sources = [
- 'invalid_src', NetworkConfigSource.ds]
-
- self.assertEqual(
- (ds_net_cfg, NetworkConfigSource.ds),
- self.init._find_networking_config())
- self.assertIn('WARNING: data source specifies an invalid network'
- ' cfg_source: invalid_src',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test__find_networking_config_warns_if_datasrc_uses_unavailable_src(
- self, m_cmdline, m_initramfs):
- """find_networking_config should check sources in DS defined order"""
- ds_net_cfg = {'config': {'needle': True}}
- self.init.datasource = FakeDataSource(network_config=ds_net_cfg)
- self.init.datasource.network_config_sources = [
- NetworkConfigSource.fallback, NetworkConfigSource.ds]
-
- self.assertEqual(
- (ds_net_cfg, NetworkConfigSource.ds),
- self.init._find_networking_config())
- self.assertIn('WARNING: data source specifies an unavailable network'
- ' cfg_source: fallback',
- self.logs.getvalue())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_returns_kernel(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns kernel cmdline config if present."""
- expected_cfg = {'config': ['fakekernel']}
- m_cmdline.return_value = expected_cfg
- m_initramfs.return_value = {'config': ['fake_initrd']}
- self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
- 'network': {'config': ['fakesys_config']}}
- self.init.datasource = FakeDataSource(
- network_config={'config': ['fakedatasource']})
- self.assertEqual(
- (expected_cfg, NetworkConfigSource.cmdline),
- self.init._find_networking_config())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_returns_initramfs(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns kernel cmdline config if present."""
- expected_cfg = {'config': ['fake_initrd']}
- m_cmdline.return_value = {}
- m_initramfs.return_value = expected_cfg
- self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
- 'network': {'config': ['fakesys_config']}}
- self.init.datasource = FakeDataSource(
- network_config={'config': ['fakedatasource']})
- self.assertEqual(
- (expected_cfg, NetworkConfigSource.initramfs),
- self.init._find_networking_config())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_returns_system_cfg(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns system config when present."""
- m_cmdline.return_value = {} # No kernel network config
- m_initramfs.return_value = {} # no initramfs network config
- expected_cfg = {'config': ['fakesys_config']}
- self.init._cfg = {'system_info': {'paths': {'cloud_dir': self.tmpdir}},
- 'network': expected_cfg}
- self.init.datasource = FakeDataSource(
- network_config={'config': ['fakedatasource']})
- self.assertEqual(
- (expected_cfg, NetworkConfigSource.system_cfg),
- self.init._find_networking_config())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_returns_datasrc_cfg(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns datasource net config if present."""
- m_cmdline.return_value = {} # No kernel network config
- m_initramfs.return_value = {} # no initramfs network config
- # No system config for network in setUp
- expected_cfg = {'config': ['fakedatasource']}
- self.init.datasource = FakeDataSource(network_config=expected_cfg)
- self.assertEqual(
- (expected_cfg, NetworkConfigSource.ds),
- self.init._find_networking_config())
-
- @mock.patch('cloudinit.stages.cmdline.read_initramfs_config')
- @mock.patch('cloudinit.stages.cmdline.read_kernel_cmdline_config')
- def test_wb__find_networking_config_returns_fallback(
- self, m_cmdline, m_initramfs):
- """find_networking_config returns fallback config if not defined."""
- m_cmdline.return_value = {} # Kernel doesn't disable networking
- m_initramfs.return_value = {} # no initramfs network config
- # Neither datasource nor system_info disable or provide network
-
- fake_cfg = {'config': [{'type': 'physical', 'name': 'eth9'}],
- 'version': 1}
-
- def fake_generate_fallback():
- return fake_cfg
-
- # Monkey patch distro which gets cached on self.init
- distro = self.init.distro
- distro.generate_fallback_config = fake_generate_fallback
- self.assertEqual(
- (fake_cfg, NetworkConfigSource.fallback),
- self.init._find_networking_config())
- self.assertNotIn('network config disabled', self.logs.getvalue())
-
- def test_apply_network_config_disabled(self):
- """Log when network is disabled by upgraded-network."""
- disable_file = os.path.join(
- self.init.paths.get_cpath('data'), 'upgraded-network')
-
- def fake_network_config():
- return (None, disable_file)
-
- self.init._find_networking_config = fake_network_config
-
- self.init.apply_network_config(True)
- self.assertIn(
- 'INFO: network config is disabled by %s' % disable_file,
- self.logs.getvalue())
-
- @mock.patch('cloudinit.net.get_interfaces_by_mac')
- @mock.patch('cloudinit.distros.ubuntu.Distro')
- def test_apply_network_on_new_instance(self, m_ubuntu, m_macs):
- """Call distro apply_network_config methods on is_new_instance."""
- net_cfg = {
- 'version': 1, 'config': [
- {'subnets': [{'type': 'dhcp'}], 'type': 'physical',
- 'name': 'eth9', 'mac_address': '42:42:42:42:42:42'}]}
-
- def fake_network_config():
- return net_cfg, NetworkConfigSource.fallback
-
- m_macs.return_value = {'42:42:42:42:42:42': 'eth9'}
-
- self.init._find_networking_config = fake_network_config
-
- self.init.apply_network_config(True)
- self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
- self.init.distro.apply_network_config.assert_called_with(
- net_cfg, bring_up=True)
-
- @mock.patch('cloudinit.distros.ubuntu.Distro')
- def test_apply_network_on_same_instance_id(self, m_ubuntu):
- """Only call distro.apply_network_config_names on same instance id."""
- self.init.is_new_instance = self._real_is_new_instance
- old_instance_id = os.path.join(
- self.init.paths.get_cpath('data'), 'instance-id')
- write_file(old_instance_id, TEST_INSTANCE_ID)
- net_cfg = {
- 'version': 1, 'config': [
- {'subnets': [{'type': 'dhcp'}], 'type': 'physical',
- 'name': 'eth9', 'mac_address': '42:42:42:42:42:42'}]}
-
- def fake_network_config():
- return net_cfg, NetworkConfigSource.fallback
-
- self.init._find_networking_config = fake_network_config
-
- self.init.apply_network_config(True)
- self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
- self.init.distro.apply_network_config.assert_not_called()
- assert (
- "No network config applied. Neither a new instance nor datasource "
- "network update allowed"
- ) in self.logs.getvalue()
-
- # CiTestCase doesn't work with pytest.mark.parametrize, and moving this
- # functionality to a separate class is more cumbersome than it'd be worth
- # at the moment, so use this as a simple setup
- def _apply_network_setup(self, m_macs):
- old_instance_id = os.path.join(
- self.init.paths.get_cpath('data'), 'instance-id')
- write_file(old_instance_id, TEST_INSTANCE_ID)
- net_cfg = {
- 'version': 1, 'config': [
- {'subnets': [{'type': 'dhcp'}], 'type': 'physical',
- 'name': 'eth9', 'mac_address': '42:42:42:42:42:42'}]}
-
- def fake_network_config():
- return net_cfg, NetworkConfigSource.fallback
-
- m_macs.return_value = {'42:42:42:42:42:42': 'eth9'}
-
- self.init._find_networking_config = fake_network_config
- self.init.datasource = FakeDataSource(paths=self.init.paths)
- self.init.is_new_instance = mock.Mock(return_value=False)
- return net_cfg
-
- @mock.patch('cloudinit.net.get_interfaces_by_mac')
- @mock.patch('cloudinit.distros.ubuntu.Distro')
- @mock.patch.dict(sources.DataSource.default_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE, EventType.BOOT}})
- def test_apply_network_allowed_when_default_boot(
- self, m_ubuntu, m_macs
- ):
- """Apply network if datasource permits BOOT event."""
- net_cfg = self._apply_network_setup(m_macs)
-
- self.init.apply_network_config(True)
- assert mock.call(
- net_cfg
- ) == self.init.distro.apply_network_config_names.call_args_list[-1]
- assert mock.call(
- net_cfg, bring_up=True
- ) == self.init.distro.apply_network_config.call_args_list[-1]
-
- @mock.patch('cloudinit.net.get_interfaces_by_mac')
- @mock.patch('cloudinit.distros.ubuntu.Distro')
- @mock.patch.dict(sources.DataSource.default_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}})
- def test_apply_network_disabled_when_no_default_boot(
- self, m_ubuntu, m_macs
- ):
- """Don't apply network if datasource has no BOOT event."""
- self._apply_network_setup(m_macs)
- self.init.apply_network_config(True)
- self.init.distro.apply_network_config.assert_not_called()
- assert (
- "No network config applied. Neither a new instance nor datasource "
- "network update allowed"
- ) in self.logs.getvalue()
-
- @mock.patch('cloudinit.net.get_interfaces_by_mac')
- @mock.patch('cloudinit.distros.ubuntu.Distro')
- @mock.patch.dict(sources.DataSource.default_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}})
- def test_apply_network_allowed_with_userdata_overrides(
- self, m_ubuntu, m_macs
- ):
- """Apply network if userdata overrides default config"""
- net_cfg = self._apply_network_setup(m_macs)
- self.init._cfg = {'updates': {'network': {'when': ['boot']}}}
- self.init.apply_network_config(True)
- self.init.distro.apply_network_config_names.assert_called_with(
- net_cfg)
- self.init.distro.apply_network_config.assert_called_with(
- net_cfg, bring_up=True)
-
- @mock.patch('cloudinit.net.get_interfaces_by_mac')
- @mock.patch('cloudinit.distros.ubuntu.Distro')
- @mock.patch.dict(sources.DataSource.supported_update_events, {
- EventScope.NETWORK: {EventType.BOOT_NEW_INSTANCE}})
- def test_apply_network_disabled_when_unsupported(
- self, m_ubuntu, m_macs
- ):
- """Don't apply network config if unsupported.
-
- Shouldn't work even when specified as userdata
- """
- self._apply_network_setup(m_macs)
-
- self.init._cfg = {'updates': {'network': {'when': ['boot']}}}
- self.init.apply_network_config(True)
- self.init.distro.apply_network_config.assert_not_called()
- assert (
- "No network config applied. Neither a new instance nor datasource "
- "network update allowed"
- ) in self.logs.getvalue()
-
-
-class TestInit_InitializeFilesystem:
- """Tests for cloudinit.stages.Init._initialize_filesystem.
-
- TODO: Expand these tests to cover all of _initialize_filesystem's behavior.
- """
-
- @pytest.yield_fixture
- def init(self, paths):
- """A fixture which yields a stages.Init instance with paths and cfg set
-
- As it is replaced with a mock, consumers of this fixture can set
- `init._cfg` if the default empty dict configuration is not appropriate.
- """
- with mock.patch("cloudinit.stages.util.ensure_dirs"):
- init = stages.Init()
- init._cfg = {}
- init._paths = paths
- yield init
-
- @mock.patch("cloudinit.stages.util.ensure_file")
- def test_ensure_file_not_called_if_no_log_file_configured(
- self, m_ensure_file, init
- ):
- """If no log file is configured, we should not ensure its existence."""
- init._cfg = {}
-
- init._initialize_filesystem()
-
- assert 0 == m_ensure_file.call_count
-
- def test_log_files_existence_is_ensured_if_configured(self, init, tmpdir):
- """If a log file is configured, we should ensure its existence."""
- log_file = tmpdir.join("cloud-init.log")
- init._cfg = {"def_log_file": str(log_file)}
-
- init._initialize_filesystem()
-
- assert log_file.exists()
- # Assert we create it 0o640 by default if it doesn't already exist
- assert 0o640 == stat.S_IMODE(log_file.stat().mode)
-
- def test_existing_file_permissions_are_not_modified(self, init, tmpdir):
- """If the log file already exists, we should not modify its permissions
-
- See https://bugs.launchpad.net/cloud-init/+bug/1900837.
- """
- # Use a mode that will never be made the default so this test will
- # always be valid
- mode = 0o606
- log_file = tmpdir.join("cloud-init.log")
- log_file.ensure()
- log_file.chmod(mode)
- init._cfg = {"def_log_file": str(log_file)}
-
- init._initialize_filesystem()
-
- assert mode == stat.S_IMODE(log_file.stat().mode)
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_subp.py b/cloudinit/tests/test_subp.py
deleted file mode 100644
index 515d5d64..00000000
--- a/cloudinit/tests/test_subp.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests for cloudinit.subp utility functions"""
-
-import json
-import os
-import sys
-import stat
-
-from unittest import mock
-
-from cloudinit import subp, util
-from cloudinit.tests.helpers import CiTestCase
-
-
-BASH = subp.which('bash')
-BOGUS_COMMAND = 'this-is-not-expected-to-be-a-program-name'
-
-
-class TestPrependBaseCommands(CiTestCase):
-
- with_logs = True
-
- def test_prepend_base_command_errors_on_neither_string_nor_list(self):
- """Raise an error for each command which is not a string or list."""
- orig_commands = ['ls', 1, {'not': 'gonna work'}, ['basecmd', 'list']]
- with self.assertRaises(TypeError) as context_manager:
- subp.prepend_base_command(
- base_command='basecmd', commands=orig_commands)
- self.assertEqual(
- "Invalid basecmd config. These commands are not a string or"
- " list:\n1\n{'not': 'gonna work'}",
- str(context_manager.exception))
-
- def test_prepend_base_command_warns_on_non_base_string_commands(self):
- """Warn on each non-base for commands of type string."""
- orig_commands = [
- 'ls', 'basecmd list', 'touch /blah', 'basecmd install x']
- fixed_commands = subp.prepend_base_command(
- base_command='basecmd', commands=orig_commands)
- self.assertEqual(
- 'WARNING: Non-basecmd commands in basecmd config:\n'
- 'ls\ntouch /blah\n',
- self.logs.getvalue())
- self.assertEqual(orig_commands, fixed_commands)
-
- def test_prepend_base_command_prepends_on_non_base_list_commands(self):
- """Prepend 'basecmd' for each non-basecmd command of type list."""
- orig_commands = [['ls'], ['basecmd', 'list'], ['basecmda', '/blah'],
- ['basecmd', 'install', 'x']]
- expected = [['basecmd', 'ls'], ['basecmd', 'list'],
- ['basecmd', 'basecmda', '/blah'],
- ['basecmd', 'install', 'x']]
- fixed_commands = subp.prepend_base_command(
- base_command='basecmd', commands=orig_commands)
- self.assertEqual('', self.logs.getvalue())
- self.assertEqual(expected, fixed_commands)
-
- def test_prepend_base_command_removes_first_item_when_none(self):
- """Remove the first element of a non-basecmd when it is None."""
- orig_commands = [[None, 'ls'], ['basecmd', 'list'],
- [None, 'touch', '/blah'],
- ['basecmd', 'install', 'x']]
- expected = [['ls'], ['basecmd', 'list'],
- ['touch', '/blah'],
- ['basecmd', 'install', 'x']]
- fixed_commands = subp.prepend_base_command(
- base_command='basecmd', commands=orig_commands)
- self.assertEqual('', self.logs.getvalue())
- self.assertEqual(expected, fixed_commands)
-
-
-class TestSubp(CiTestCase):
- allowed_subp = [BASH, 'cat', CiTestCase.SUBP_SHELL_TRUE,
- BOGUS_COMMAND, sys.executable]
-
- stdin2err = [BASH, '-c', 'cat >&2']
- stdin2out = ['cat']
- utf8_invalid = b'ab\xaadef'
- utf8_valid = b'start \xc3\xa9 end'
- utf8_valid_2 = b'd\xc3\xa9j\xc8\xa7'
- printenv = [BASH, '-c', 'for n in "$@"; do echo "$n=${!n}"; done', '--']
-
- def printf_cmd(self, *args):
- # bash's printf supports \xaa. So does /usr/bin/printf
- # but by using bash, we remove dependency on another program.
- return([BASH, '-c', 'printf "$@"', 'printf'] + list(args))
-
- def test_subp_handles_bytestrings(self):
- """subp can run a bytestring command if shell is True."""
- tmp_file = self.tmp_path('test.out')
- cmd = 'echo HI MOM >> {tmp_file}'.format(tmp_file=tmp_file)
- (out, _err) = subp.subp(cmd.encode('utf-8'), shell=True)
- self.assertEqual('', out)
- self.assertEqual('', _err)
- self.assertEqual('HI MOM\n', util.load_file(tmp_file))
-
- def test_subp_handles_strings(self):
- """subp can run a string command if shell is True."""
- tmp_file = self.tmp_path('test.out')
- cmd = 'echo HI MOM >> {tmp_file}'.format(tmp_file=tmp_file)
- (out, _err) = subp.subp(cmd, shell=True)
- self.assertEqual('', out)
- self.assertEqual('', _err)
- self.assertEqual('HI MOM\n', util.load_file(tmp_file))
-
- def test_subp_handles_utf8(self):
- # The given bytes contain utf-8 accented characters as seen in e.g.
- # the "deja dup" package in Ubuntu.
- cmd = self.printf_cmd(self.utf8_valid_2)
- (out, _err) = subp.subp(cmd, capture=True)
- self.assertEqual(out, self.utf8_valid_2.decode('utf-8'))
-
- def test_subp_respects_decode_false(self):
- (out, err) = subp.subp(self.stdin2out, capture=True, decode=False,
- data=self.utf8_valid)
- self.assertTrue(isinstance(out, bytes))
- self.assertTrue(isinstance(err, bytes))
- self.assertEqual(out, self.utf8_valid)
-
- def test_subp_decode_ignore(self):
- # this executes a string that writes invalid utf-8 to stdout
- (out, _err) = subp.subp(self.printf_cmd('abc\\xaadef'),
- capture=True, decode='ignore')
- self.assertEqual(out, 'abcdef')
-
- def test_subp_decode_strict_valid_utf8(self):
- (out, _err) = subp.subp(self.stdin2out, capture=True,
- decode='strict', data=self.utf8_valid)
- self.assertEqual(out, self.utf8_valid.decode('utf-8'))
-
- def test_subp_decode_invalid_utf8_replaces(self):
- (out, _err) = subp.subp(self.stdin2out, capture=True,
- data=self.utf8_invalid)
- expected = self.utf8_invalid.decode('utf-8', 'replace')
- self.assertEqual(out, expected)
-
- def test_subp_decode_strict_raises(self):
- args = []
- kwargs = {'args': self.stdin2out, 'capture': True,
- 'decode': 'strict', 'data': self.utf8_invalid}
- self.assertRaises(UnicodeDecodeError, subp.subp, *args, **kwargs)
-
- def test_subp_capture_stderr(self):
- data = b'hello world'
- (out, err) = subp.subp(self.stdin2err, capture=True,
- decode=False, data=data,
- update_env={'LC_ALL': 'C'})
- self.assertEqual(err, data)
- self.assertEqual(out, b'')
-
- def test_subp_reads_env(self):
- with mock.patch.dict("os.environ", values={'FOO': 'BAR'}):
- out, _err = subp.subp(self.printenv + ['FOO'], capture=True)
- self.assertEqual('FOO=BAR', out.splitlines()[0])
-
- def test_subp_env_and_update_env(self):
- out, _err = subp.subp(
- self.printenv + ['FOO', 'HOME', 'K1', 'K2'], capture=True,
- env={'FOO': 'BAR'},
- update_env={'HOME': '/myhome', 'K2': 'V2'})
- self.assertEqual(
- ['FOO=BAR', 'HOME=/myhome', 'K1=', 'K2=V2'], out.splitlines())
-
- def test_subp_update_env(self):
- extra = {'FOO': 'BAR', 'HOME': '/root', 'K1': 'V1'}
- with mock.patch.dict("os.environ", values=extra):
- out, _err = subp.subp(
- self.printenv + ['FOO', 'HOME', 'K1', 'K2'], capture=True,
- update_env={'HOME': '/myhome', 'K2': 'V2'})
-
- self.assertEqual(
- ['FOO=BAR', 'HOME=/myhome', 'K1=V1', 'K2=V2'], out.splitlines())
-
- def test_subp_warn_missing_shebang(self):
- """Warn on no #! in script"""
- noshebang = self.tmp_path('noshebang')
- util.write_file(noshebang, 'true\n')
-
- print("os is %s" % os)
- os.chmod(noshebang, os.stat(noshebang).st_mode | stat.S_IEXEC)
- with self.allow_subp([noshebang]):
- self.assertRaisesRegex(subp.ProcessExecutionError,
- r'Missing #! in script\?',
- subp.subp, (noshebang,))
-
- def test_subp_combined_stderr_stdout(self):
- """Providing combine_capture as True redirects stderr to stdout."""
- data = b'hello world'
- (out, err) = subp.subp(self.stdin2err, capture=True,
- combine_capture=True, decode=False, data=data)
- self.assertEqual(b'', err)
- self.assertEqual(data, out)
-
- def test_returns_none_if_no_capture(self):
- (out, err) = subp.subp(self.stdin2out, data=b'', capture=False)
- self.assertIsNone(err)
- self.assertIsNone(out)
-
- def test_exception_has_out_err_are_bytes_if_decode_false(self):
- """Raised exc should have stderr, stdout as bytes if no decode."""
- with self.assertRaises(subp.ProcessExecutionError) as cm:
- subp.subp([BOGUS_COMMAND], decode=False)
- self.assertTrue(isinstance(cm.exception.stdout, bytes))
- self.assertTrue(isinstance(cm.exception.stderr, bytes))
-
- def test_exception_has_out_err_are_bytes_if_decode_true(self):
- """Raised exc should have stderr, stdout as string if no decode."""
- with self.assertRaises(subp.ProcessExecutionError) as cm:
- subp.subp([BOGUS_COMMAND], decode=True)
- self.assertTrue(isinstance(cm.exception.stdout, str))
- self.assertTrue(isinstance(cm.exception.stderr, str))
-
- def test_bunch_of_slashes_in_path(self):
- self.assertEqual("/target/my/path/",
- subp.target_path("/target/", "//my/path/"))
- self.assertEqual("/target/my/path/",
- subp.target_path("/target/", "///my/path/"))
-
- def test_c_lang_can_take_utf8_args(self):
- """Independent of system LC_CTYPE, args can contain utf-8 strings.
-
- When python starts up, its default encoding gets set based on
- the value of LC_CTYPE. If no system locale is set, the default
- encoding for both python2 and python3 in some paths will end up
- being ascii.
-
- Attempts to use setlocale or patching (or changing) os.environ
- in the current environment seem to not be effective.
-
- This test starts up a python with LC_CTYPE set to C so that
- the default encoding will be set to ascii. In such an environment
- Popen(['command', 'non-ascii-arg']) would cause a UnicodeDecodeError.
- """
- python_prog = '\n'.join([
- 'import json, sys',
- 'from cloudinit.subp import subp',
- 'data = sys.stdin.read()',
- 'cmd = json.loads(data)',
- 'subp(cmd, capture=False)',
- ''])
- cmd = [BASH, '-c', 'echo -n "$@"', '--',
- self.utf8_valid.decode("utf-8")]
- python_subp = [sys.executable, '-c', python_prog]
-
- out, _err = subp.subp(
- python_subp, update_env={'LC_CTYPE': 'C'},
- data=json.dumps(cmd).encode("utf-8"),
- decode=False)
- self.assertEqual(self.utf8_valid, out)
-
- def test_bogus_command_logs_status_messages(self):
- """status_cb gets status messages logs on bogus commands provided."""
- logs = []
-
- def status_cb(log):
- logs.append(log)
-
- with self.assertRaises(subp.ProcessExecutionError):
- subp.subp([BOGUS_COMMAND], status_cb=status_cb)
-
- expected = [
- 'Begin run command: {cmd}\n'.format(cmd=BOGUS_COMMAND),
- 'ERROR: End run command: invalid command provided\n']
- self.assertEqual(expected, logs)
-
- def test_command_logs_exit_codes_to_status_cb(self):
- """status_cb gets status messages containing command exit code."""
- logs = []
-
- def status_cb(log):
- logs.append(log)
-
- with self.assertRaises(subp.ProcessExecutionError):
- subp.subp([BASH, '-c', 'exit 2'], status_cb=status_cb)
- subp.subp([BASH, '-c', 'exit 0'], status_cb=status_cb)
-
- expected = [
- 'Begin run command: %s -c exit 2\n' % BASH,
- 'ERROR: End run command: exit(2)\n',
- 'Begin run command: %s -c exit 0\n' % BASH,
- 'End run command: exit(0)\n']
- self.assertEqual(expected, logs)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_temp_utils.py b/cloudinit/tests/test_temp_utils.py
deleted file mode 100644
index 4a52ef89..00000000
--- a/cloudinit/tests/test_temp_utils.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests for cloudinit.temp_utils"""
-
-from cloudinit.temp_utils import mkdtemp, mkstemp, tempdir
-from cloudinit.tests.helpers import CiTestCase, wrap_and_call
-import os
-
-
-class TestTempUtils(CiTestCase):
-
- def test_mkdtemp_default_non_root(self):
- """mkdtemp creates a dir under /tmp for the unprivileged."""
- calls = []
-
- def fake_mkdtemp(*args, **kwargs):
- calls.append(kwargs)
- return '/fake/return/path'
-
- retval = wrap_and_call(
- 'cloudinit.temp_utils',
- {'os.getuid': 1000,
- 'tempfile.mkdtemp': {'side_effect': fake_mkdtemp},
- '_TMPDIR': {'new': None},
- 'os.path.isdir': True},
- mkdtemp)
- self.assertEqual('/fake/return/path', retval)
- self.assertEqual([{'dir': '/tmp'}], calls)
-
- def test_mkdtemp_default_non_root_needs_exe(self):
- """mkdtemp creates a dir under /var/tmp/cloud-init when needs_exe."""
- calls = []
-
- def fake_mkdtemp(*args, **kwargs):
- calls.append(kwargs)
- return '/fake/return/path'
-
- retval = wrap_and_call(
- 'cloudinit.temp_utils',
- {'os.getuid': 1000,
- 'tempfile.mkdtemp': {'side_effect': fake_mkdtemp},
- '_TMPDIR': {'new': None},
- 'os.path.isdir': True},
- mkdtemp, needs_exe=True)
- self.assertEqual('/fake/return/path', retval)
- self.assertEqual([{'dir': '/var/tmp/cloud-init'}], calls)
-
- def test_mkdtemp_default_root(self):
- """mkdtemp creates a dir under /run/cloud-init for the privileged."""
- calls = []
-
- def fake_mkdtemp(*args, **kwargs):
- calls.append(kwargs)
- return '/fake/return/path'
-
- retval = wrap_and_call(
- 'cloudinit.temp_utils',
- {'os.getuid': 0,
- 'tempfile.mkdtemp': {'side_effect': fake_mkdtemp},
- '_TMPDIR': {'new': None},
- 'os.path.isdir': True},
- mkdtemp)
- self.assertEqual('/fake/return/path', retval)
- self.assertEqual([{'dir': '/run/cloud-init/tmp'}], calls)
-
- def test_mkstemp_default_non_root(self):
- """mkstemp creates secure tempfile under /tmp for the unprivileged."""
- calls = []
-
- def fake_mkstemp(*args, **kwargs):
- calls.append(kwargs)
- return '/fake/return/path'
-
- retval = wrap_and_call(
- 'cloudinit.temp_utils',
- {'os.getuid': 1000,
- 'tempfile.mkstemp': {'side_effect': fake_mkstemp},
- '_TMPDIR': {'new': None},
- 'os.path.isdir': True},
- mkstemp)
- self.assertEqual('/fake/return/path', retval)
- self.assertEqual([{'dir': '/tmp'}], calls)
-
- def test_mkstemp_default_root(self):
- """mkstemp creates a secure tempfile in /run/cloud-init for root."""
- calls = []
-
- def fake_mkstemp(*args, **kwargs):
- calls.append(kwargs)
- return '/fake/return/path'
-
- retval = wrap_and_call(
- 'cloudinit.temp_utils',
- {'os.getuid': 0,
- 'tempfile.mkstemp': {'side_effect': fake_mkstemp},
- '_TMPDIR': {'new': None},
- 'os.path.isdir': True},
- mkstemp)
- self.assertEqual('/fake/return/path', retval)
- self.assertEqual([{'dir': '/run/cloud-init/tmp'}], calls)
-
- def test_tempdir_error_suppression(self):
- """test tempdir suppresses errors during directory removal."""
-
- with self.assertRaises(OSError):
- with tempdir(prefix='cloud-init-dhcp-') as tdir:
- os.rmdir(tdir)
- # As a result, the directory is already gone,
- # so shutil.rmtree should raise OSError
-
- with tempdir(rmtree_ignore_errors=True,
- prefix='cloud-init-dhcp-') as tdir:
- os.rmdir(tdir)
- # Since the directory is already gone, shutil.rmtree would raise
- # OSError, but we suppress that
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_upgrade.py b/cloudinit/tests/test_upgrade.py
deleted file mode 100644
index da3ab23b..00000000
--- a/cloudinit/tests/test_upgrade.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copyright (C) 2020 Canonical Ltd.
-#
-# Author: Daniel Watkins <oddbloke@ubuntu.com>
-#
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Upgrade testing for cloud-init.
-
-This module tests cloud-init's behaviour across upgrades. Specifically, it
-specifies a set of invariants that the current codebase expects to be true (as
-tests in ``TestUpgrade``) and then checks that these hold true after unpickling
-``obj.pkl``s from previous versions of cloud-init; those pickles are stored in
-``tests/data/old_pickles/``.
-"""
-
-import operator
-import pathlib
-
-import pytest
-
-from cloudinit.stages import _pkl_load
-from cloudinit.tests.helpers import resourceLocation
-
-
-class TestUpgrade:
- @pytest.fixture(
- params=pathlib.Path(resourceLocation("old_pickles")).glob("*.pkl"),
- scope="class",
- ids=operator.attrgetter("name"),
- )
- def previous_obj_pkl(self, request):
- """Load each pickle to memory once, then run all tests against it.
-
- Test implementations _must not_ modify the ``previous_obj_pkl`` which
- they are passed, as that will affect tests that run after them.
- """
- return _pkl_load(str(request.param))
-
- def test_networking_set_on_distro(self, previous_obj_pkl):
- """We always expect to have ``.networking`` on ``Distro`` objects."""
- assert previous_obj_pkl.distro.networking is not None
-
- def test_blacklist_drivers_set_on_networking(self, previous_obj_pkl):
- """We always expect Networking.blacklist_drivers to be initialised."""
- assert previous_obj_pkl.distro.networking.blacklist_drivers is None
-
- def test_paths_has_run_dir_attribute(self, previous_obj_pkl):
- assert previous_obj_pkl.paths.run_dir is not None
-
- def test_vendordata_exists(self, previous_obj_pkl):
- assert previous_obj_pkl.vendordata2 is None
- assert previous_obj_pkl.vendordata2_raw is None
diff --git a/cloudinit/tests/test_url_helper.py b/cloudinit/tests/test_url_helper.py
deleted file mode 100644
index c3918f80..00000000
--- a/cloudinit/tests/test_url_helper.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from cloudinit.url_helper import (
- NOT_FOUND, UrlError, REDACTED, oauth_headers, read_file_or_url,
- retry_on_url_exc)
-from cloudinit.tests.helpers import CiTestCase, mock, skipIf
-from cloudinit import util
-from cloudinit import version
-
-import httpretty
-import logging
-import requests
-
-
-try:
- import oauthlib
- assert oauthlib # avoid pyflakes error F401: import unused
- _missing_oauthlib_dep = False
-except ImportError:
- _missing_oauthlib_dep = True
-
-
-M_PATH = 'cloudinit.url_helper.'
-
-
-class TestOAuthHeaders(CiTestCase):
-
- def test_oauth_headers_raises_not_implemented_when_oathlib_missing(self):
- """oauth_headers raises a NotImplemented error when oauth absent."""
- with mock.patch.dict('sys.modules', {'oauthlib': None}):
- with self.assertRaises(NotImplementedError) as context_manager:
- oauth_headers(1, 2, 3, 4, 5)
- self.assertEqual(
- 'oauth support is not available',
- str(context_manager.exception))
-
- @skipIf(_missing_oauthlib_dep, "No python-oauthlib dependency")
- @mock.patch('oauthlib.oauth1.Client')
- def test_oauth_headers_calls_oathlibclient_when_available(self, m_client):
- """oauth_headers calls oaut1.hClient.sign with the provided url."""
- class fakeclient(object):
- def sign(self, url):
- # The first and 3rd item of the client.sign tuple are ignored
- return ('junk', url, 'junk2')
-
- m_client.return_value = fakeclient()
-
- return_value = oauth_headers(
- 'url', 'consumer_key', 'token_key', 'token_secret',
- 'consumer_secret')
- self.assertEqual('url', return_value)
-
-
-class TestReadFileOrUrl(CiTestCase):
-
- with_logs = True
-
- def test_read_file_or_url_str_from_file(self):
- """Test that str(result.contents) on file is text version of contents.
- It should not be "b'data'", but just "'data'" """
- tmpf = self.tmp_path("myfile1")
- data = b'This is my file content\n'
- util.write_file(tmpf, data, omode="wb")
- result = read_file_or_url("file://%s" % tmpf)
- self.assertEqual(result.contents, data)
- self.assertEqual(str(result), data.decode('utf-8'))
-
- @httpretty.activate
- def test_read_file_or_url_str_from_url(self):
- """Test that str(result.contents) on url is text version of contents.
- It should not be "b'data'", but just "'data'" """
- url = 'http://hostname/path'
- data = b'This is my url content\n'
- httpretty.register_uri(httpretty.GET, url, data)
- result = read_file_or_url(url)
- self.assertEqual(result.contents, data)
- self.assertEqual(str(result), data.decode('utf-8'))
-
- @httpretty.activate
- def test_read_file_or_url_str_from_url_redacting_headers_from_logs(self):
- """Headers are redacted from logs but unredacted in requests."""
- url = 'http://hostname/path'
- headers = {'sensitive': 'sekret', 'server': 'blah'}
- httpretty.register_uri(httpretty.GET, url)
- # By default, httpretty will log our request along with the header,
- # so if we don't change this the secret will show up in the logs
- logging.getLogger('httpretty.core').setLevel(logging.CRITICAL)
-
- read_file_or_url(url, headers=headers, headers_redact=['sensitive'])
- logs = self.logs.getvalue()
- for k in headers.keys():
- self.assertEqual(headers[k], httpretty.last_request().headers[k])
- self.assertIn(REDACTED, logs)
- self.assertNotIn('sekret', logs)
-
- @httpretty.activate
- def test_read_file_or_url_str_from_url_redacts_noheaders(self):
- """When no headers_redact, header values are in logs and requests."""
- url = 'http://hostname/path'
- headers = {'sensitive': 'sekret', 'server': 'blah'}
- httpretty.register_uri(httpretty.GET, url)
-
- read_file_or_url(url, headers=headers)
- for k in headers.keys():
- self.assertEqual(headers[k], httpretty.last_request().headers[k])
- logs = self.logs.getvalue()
- self.assertNotIn(REDACTED, logs)
- self.assertIn('sekret', logs)
-
- @mock.patch(M_PATH + 'readurl')
- def test_read_file_or_url_passes_params_to_readurl(self, m_readurl):
- """read_file_or_url passes all params through to readurl."""
- url = 'http://hostname/path'
- response = 'This is my url content\n'
- m_readurl.return_value = response
- params = {'url': url, 'timeout': 1, 'retries': 2,
- 'headers': {'somehdr': 'val'},
- 'data': 'data', 'sec_between': 1,
- 'ssl_details': {'cert_file': '/path/cert.pem'},
- 'headers_cb': 'headers_cb', 'exception_cb': 'exception_cb'}
- self.assertEqual(response, read_file_or_url(**params))
- params.pop('url') # url is passed in as a positional arg
- self.assertEqual([mock.call(url, **params)], m_readurl.call_args_list)
-
- def test_wb_read_url_defaults_honored_by_read_file_or_url_callers(self):
- """Readurl param defaults used when unspecified by read_file_or_url
-
- Param defaults tested are as follows:
- retries: 0, additional headers None beyond default, method: GET,
- data: None, check_status: True and allow_redirects: True
- """
- url = 'http://hostname/path'
-
- m_response = mock.MagicMock()
-
- class FakeSession(requests.Session):
- @classmethod
- def request(cls, **kwargs):
- self.assertEqual(
- {'url': url, 'allow_redirects': True, 'method': 'GET',
- 'headers': {
- 'User-Agent': 'Cloud-Init/%s' % (
- version.version_string())}},
- kwargs)
- return m_response
-
- with mock.patch(M_PATH + 'requests.Session') as m_session:
- error = requests.exceptions.HTTPError('broke')
- m_session.side_effect = [error, FakeSession()]
- # assert no retries and check_status == True
- with self.assertRaises(UrlError) as context_manager:
- response = read_file_or_url(url)
- self.assertEqual('broke', str(context_manager.exception))
- # assert default headers, method, url and allow_redirects True
- # Success on 2nd call with FakeSession
- response = read_file_or_url(url)
- self.assertEqual(m_response, response._response)
-
-
-class TestRetryOnUrlExc(CiTestCase):
-
- def test_do_not_retry_non_urlerror(self):
- """When exception is not UrlError return False."""
- myerror = IOError('something unexcpected')
- self.assertFalse(retry_on_url_exc(msg='', exc=myerror))
-
- def test_perform_retries_on_not_found(self):
- """When exception is UrlError with a 404 status code return True."""
- myerror = UrlError(cause=RuntimeError(
- 'something was not found'), code=NOT_FOUND)
- self.assertTrue(retry_on_url_exc(msg='', exc=myerror))
-
- def test_perform_retries_on_timeout(self):
- """When exception is a requests.Timout return True."""
- myerror = UrlError(cause=requests.Timeout('something timed out'))
- self.assertTrue(retry_on_url_exc(msg='', exc=myerror))
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
deleted file mode 100644
index 7a3175f3..00000000
--- a/cloudinit/tests/test_util.py
+++ /dev/null
@@ -1,1187 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""Tests for cloudinit.util"""
-
-import base64
-import logging
-import json
-import platform
-import pytest
-
-import cloudinit.util as util
-from cloudinit import subp
-
-from cloudinit.tests.helpers import CiTestCase, mock
-from textwrap import dedent
-
-LOG = logging.getLogger(__name__)
-
-MOUNT_INFO = [
- '68 0 8:3 / / ro,relatime shared:1 - btrfs /dev/sda1 ro,attr2,inode64',
- '153 68 254:0 / /home rw,relatime shared:101 - xfs /dev/sda2 rw,attr2'
-]
-
-OS_RELEASE_SLES = dedent("""\
- NAME="SLES"
- VERSION="12-SP3"
- VERSION_ID="12.3"
- PRETTY_NAME="SUSE Linux Enterprise Server 12 SP3"
- ID="sles"
- ANSI_COLOR="0;32"
- CPE_NAME="cpe:/o:suse:sles:12:sp3"
-""")
-
-OS_RELEASE_OPENSUSE = dedent("""\
- NAME="openSUSE Leap"
- VERSION="42.3"
- ID=opensuse
- ID_LIKE="suse"
- VERSION_ID="42.3"
- PRETTY_NAME="openSUSE Leap 42.3"
- ANSI_COLOR="0;32"
- CPE_NAME="cpe:/o:opensuse:leap:42.3"
- BUG_REPORT_URL="https://bugs.opensuse.org"
- HOME_URL="https://www.opensuse.org/"
-""")
-
-OS_RELEASE_OPENSUSE_L15 = dedent("""\
- NAME="openSUSE Leap"
- VERSION="15.0"
- ID="opensuse-leap"
- ID_LIKE="suse opensuse"
- VERSION_ID="15.0"
- PRETTY_NAME="openSUSE Leap 15.0"
- ANSI_COLOR="0;32"
- CPE_NAME="cpe:/o:opensuse:leap:15.0"
- BUG_REPORT_URL="https://bugs.opensuse.org"
- HOME_URL="https://www.opensuse.org/"
-""")
-
-OS_RELEASE_OPENSUSE_TW = dedent("""\
- NAME="openSUSE Tumbleweed"
- ID="opensuse-tumbleweed"
- ID_LIKE="opensuse suse"
- VERSION_ID="20180920"
- PRETTY_NAME="openSUSE Tumbleweed"
- ANSI_COLOR="0;32"
- CPE_NAME="cpe:/o:opensuse:tumbleweed:20180920"
- BUG_REPORT_URL="https://bugs.opensuse.org"
- HOME_URL="https://www.opensuse.org/"
-""")
-
-OS_RELEASE_CENTOS = dedent("""\
- NAME="CentOS Linux"
- VERSION="7 (Core)"
- ID="centos"
- ID_LIKE="rhel fedora"
- VERSION_ID="7"
- PRETTY_NAME="CentOS Linux 7 (Core)"
- ANSI_COLOR="0;31"
- CPE_NAME="cpe:/o:centos:centos:7"
- HOME_URL="https://www.centos.org/"
- BUG_REPORT_URL="https://bugs.centos.org/"
-
- CENTOS_MANTISBT_PROJECT="CentOS-7"
- CENTOS_MANTISBT_PROJECT_VERSION="7"
- REDHAT_SUPPORT_PRODUCT="centos"
- REDHAT_SUPPORT_PRODUCT_VERSION="7"
-""")
-
-OS_RELEASE_REDHAT_7 = dedent("""\
- NAME="Red Hat Enterprise Linux Server"
- VERSION="7.5 (Maipo)"
- ID="rhel"
- ID_LIKE="fedora"
- VARIANT="Server"
- VARIANT_ID="server"
- VERSION_ID="7.5"
- PRETTY_NAME="Red Hat"
- ANSI_COLOR="0;31"
- CPE_NAME="cpe:/o:redhat:enterprise_linux:7.5:GA:server"
- HOME_URL="https://www.redhat.com/"
- BUG_REPORT_URL="https://bugzilla.redhat.com/"
-
- REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7"
- REDHAT_BUGZILLA_PRODUCT_VERSION=7.5
- REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
- REDHAT_SUPPORT_PRODUCT_VERSION="7.5"
-""")
-
-OS_RELEASE_ALMALINUX_8 = dedent("""\
- NAME="AlmaLinux"
- VERSION="8.3 (Purple Manul)"
- ID="almalinux"
- ID_LIKE="rhel centos fedora"
- VERSION_ID="8.3"
- PLATFORM_ID="platform:el8"
- PRETTY_NAME="AlmaLinux 8.3 (Purple Manul)"
- ANSI_COLOR="0;34"
- CPE_NAME="cpe:/o:almalinux:almalinux:8.3:GA"
- HOME_URL="https://almalinux.org/"
- BUG_REPORT_URL="https://bugs.almalinux.org/"
-
- ALMALINUX_MANTISBT_PROJECT="AlmaLinux-8"
- ALMALINUX_MANTISBT_PROJECT_VERSION="8.3"
-""")
-
-OS_RELEASE_EUROLINUX_7 = dedent("""\
- VERSION="7.9 (Minsk)"
- ID="eurolinux"
- ID_LIKE="rhel scientific centos fedora"
- VERSION_ID="7.9"
- PRETTY_NAME="EuroLinux 7.9 (Minsk)"
- ANSI_COLOR="0;31"
- CPE_NAME="cpe:/o:eurolinux:eurolinux:7.9:GA"
- HOME_URL="http://www.euro-linux.com/"
- BUG_REPORT_URL="mailto:support@euro-linux.com"
- REDHAT_BUGZILLA_PRODUCT="EuroLinux 7"
- REDHAT_BUGZILLA_PRODUCT_VERSION=7.9
- REDHAT_SUPPORT_PRODUCT="EuroLinux"
- REDHAT_SUPPORT_PRODUCT_VERSION="7.9"
-""")
-
-OS_RELEASE_EUROLINUX_8 = dedent("""\
- NAME="EuroLinux"
- VERSION="8.4 (Vaduz)"
- ID="eurolinux"
- ID_LIKE="rhel fedora centos"
- VERSION_ID="8.4"
- PLATFORM_ID="platform:el8"
- PRETTY_NAME="EuroLinux 8.4 (Vaduz)"
- ANSI_COLOR="0;34"
- CPE_NAME="cpe:/o:eurolinux:eurolinux:8"
- HOME_URL="https://www.euro-linux.com/"
- BUG_REPORT_URL="https://github.com/EuroLinux/eurolinux-distro-bugs-and-rfc/"
- REDHAT_SUPPORT_PRODUCT="EuroLinux"
- REDHAT_SUPPORT_PRODUCT_VERSION="8"
-""")
-
-OS_RELEASE_ROCKY_8 = dedent("""\
- NAME="Rocky Linux"
- VERSION="8.3 (Green Obsidian)"
- ID="rocky"
- ID_LIKE="rhel fedora"
- VERSION_ID="8.3"
- PLATFORM_ID="platform:el8"
- PRETTY_NAME="Rocky Linux 8.3 (Green Obsidian)"
- ANSI_COLOR="0;31"
- CPE_NAME="cpe:/o:rocky:rocky:8"
- HOME_URL="https://rockylinux.org/"
- BUG_REPORT_URL="https://bugs.rockylinux.org/"
- ROCKY_SUPPORT_PRODUCT="Rocky Linux"
- ROCKY_SUPPORT_PRODUCT_VERSION="8"
-""")
-
-OS_RELEASE_VIRTUOZZO_8 = dedent("""\
- NAME="Virtuozzo Linux"
- VERSION="8"
- ID="virtuozzo"
- ID_LIKE="rhel fedora"
- VERSION_ID="8"
- PLATFORM_ID="platform:el8"
- PRETTY_NAME="Virtuozzo Linux"
- ANSI_COLOR="0;31"
- CPE_NAME="cpe:/o:virtuozzoproject:vzlinux:8"
- HOME_URL="https://www.vzlinux.org"
- BUG_REPORT_URL="https://bugs.openvz.org"
-""")
-
-OS_RELEASE_CLOUDLINUX_8 = dedent("""\
- NAME="CloudLinux"
- VERSION="8.4 (Valery Rozhdestvensky)"
- ID="cloudlinux"
- ID_LIKE="rhel fedora centos"
- VERSION_ID="8.4"
- PLATFORM_ID="platform:el8"
- PRETTY_NAME="CloudLinux 8.4 (Valery Rozhdestvensky)"
- ANSI_COLOR="0;31"
- CPE_NAME="cpe:/o:cloudlinux:cloudlinux:8.4:GA:server"
- HOME_URL="https://www.cloudlinux.com/"
- BUG_REPORT_URL="https://www.cloudlinux.com/support"
-""")
-
-OS_RELEASE_OPENEULER_20 = dedent("""\
- NAME="openEuler"
- VERSION="20.03 (LTS-SP2)"
- ID="openEuler"
- VERSION_ID="20.03"
- PRETTY_NAME="openEuler 20.03 (LTS-SP2)"
- ANSI_COLOR="0;31"
-""")
-
-REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)"
-REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)"
-REDHAT_RELEASE_REDHAT_6 = (
- "Red Hat Enterprise Linux Server release 6.10 (Santiago)")
-REDHAT_RELEASE_REDHAT_7 = (
- "Red Hat Enterprise Linux Server release 7.5 (Maipo)")
-REDHAT_RELEASE_ALMALINUX_8 = (
- "AlmaLinux release 8.3 (Purple Manul)")
-REDHAT_RELEASE_EUROLINUX_7 = "EuroLinux release 7.9 (Minsk)"
-REDHAT_RELEASE_EUROLINUX_8 = "EuroLinux release 8.4 (Vaduz)"
-REDHAT_RELEASE_ROCKY_8 = (
- "Rocky Linux release 8.3 (Green Obsidian)")
-REDHAT_RELEASE_VIRTUOZZO_8 = (
- "Virtuozzo Linux release 8")
-REDHAT_RELEASE_CLOUDLINUX_8 = (
- "CloudLinux release 8.4 (Valery Rozhdestvensky)")
-OS_RELEASE_DEBIAN = dedent("""\
- PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
- NAME="Debian GNU/Linux"
- VERSION_ID="9"
- VERSION="9 (stretch)"
- ID=debian
- HOME_URL="https://www.debian.org/"
- SUPPORT_URL="https://www.debian.org/support"
- BUG_REPORT_URL="https://bugs.debian.org/"
-""")
-
-OS_RELEASE_UBUNTU = dedent("""\
- NAME="Ubuntu"\n
- # comment test
- VERSION="16.04.3 LTS (Xenial Xerus)"\n
- ID=ubuntu\n
- ID_LIKE=debian\n
- PRETTY_NAME="Ubuntu 16.04.3 LTS"\n
- VERSION_ID="16.04"\n
- HOME_URL="http://www.ubuntu.com/"\n
- SUPPORT_URL="http://help.ubuntu.com/"\n
- BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"\n
- VERSION_CODENAME=xenial\n
- UBUNTU_CODENAME=xenial\n
-""")
-
-OS_RELEASE_PHOTON = ("""\
- NAME="VMware Photon OS"
- VERSION="4.0"
- ID=photon
- VERSION_ID=4.0
- PRETTY_NAME="VMware Photon OS/Linux"
- ANSI_COLOR="1;34"
- HOME_URL="https://vmware.github.io/photon/"
- BUG_REPORT_URL="https://github.com/vmware/photon/issues"
-""")
-
-
-class FakeCloud(object):
-
- def __init__(self, hostname, fqdn):
- self.hostname = hostname
- self.fqdn = fqdn
- self.calls = []
-
- def get_hostname(self, fqdn=None, metadata_only=None):
- myargs = {}
- if fqdn is not None:
- myargs['fqdn'] = fqdn
- if metadata_only is not None:
- myargs['metadata_only'] = metadata_only
- self.calls.append(myargs)
- if fqdn:
- return self.fqdn
- return self.hostname
-
-
-class TestUtil(CiTestCase):
-
- def test_parse_mount_info_no_opts_no_arg(self):
- result = util.parse_mount_info('/home', MOUNT_INFO, LOG)
- self.assertEqual(('/dev/sda2', 'xfs', '/home'), result)
-
- def test_parse_mount_info_no_opts_arg(self):
- result = util.parse_mount_info('/home', MOUNT_INFO, LOG, False)
- self.assertEqual(('/dev/sda2', 'xfs', '/home'), result)
-
- def test_parse_mount_info_with_opts(self):
- result = util.parse_mount_info('/', MOUNT_INFO, LOG, True)
- self.assertEqual(
- ('/dev/sda1', 'btrfs', '/', 'ro,relatime'),
- result
- )
-
- @mock.patch('cloudinit.util.get_mount_info')
- def test_mount_is_rw(self, m_mount_info):
- m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'rw,relatime')
- is_rw = util.mount_is_read_write('/')
- self.assertEqual(is_rw, True)
-
- @mock.patch('cloudinit.util.get_mount_info')
- def test_mount_is_ro(self, m_mount_info):
- m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime')
- is_rw = util.mount_is_read_write('/')
- self.assertEqual(is_rw, False)
-
-
-class TestUptime(CiTestCase):
-
- @mock.patch('cloudinit.util.boottime')
- @mock.patch('cloudinit.util.os.path.exists')
- @mock.patch('cloudinit.util.time.time')
- def test_uptime_non_linux_path(self, m_time, m_exists, m_boottime):
- boottime = 1000.0
- uptime = 10.0
- m_boottime.return_value = boottime
- m_time.return_value = boottime + uptime
- m_exists.return_value = False
- result = util.uptime()
- self.assertEqual(str(uptime), result)
-
-
-class TestShellify(CiTestCase):
-
- def test_input_dict_raises_type_error(self):
- self.assertRaisesRegex(
- TypeError, 'Input.*was.*dict.*xpected',
- util.shellify, {'mykey': 'myval'})
-
- def test_input_str_raises_type_error(self):
- self.assertRaisesRegex(
- TypeError, 'Input.*was.*str.*xpected', util.shellify, "foobar")
-
- def test_value_with_int_raises_type_error(self):
- self.assertRaisesRegex(
- TypeError, 'shellify.*int', util.shellify, ["foo", 1])
-
- def test_supports_strings_and_lists(self):
- self.assertEqual(
- '\n'.join(["#!/bin/sh", "echo hi mom", "'echo' 'hi dad'",
- "'echo' 'hi' 'sis'", ""]),
- util.shellify(["echo hi mom", ["echo", "hi dad"],
- ('echo', 'hi', 'sis')]))
-
- def test_supports_comments(self):
- self.assertEqual(
- '\n'.join(["#!/bin/sh", "echo start", "echo end", ""]),
- util.shellify(["echo start", None, "echo end"]))
-
-
-class TestGetHostnameFqdn(CiTestCase):
-
- def test_get_hostname_fqdn_from_only_cfg_fqdn(self):
- """When cfg only has the fqdn key, derive hostname and fqdn from it."""
- hostname, fqdn = util.get_hostname_fqdn(
- cfg={'fqdn': 'myhost.domain.com'}, cloud=None)
- self.assertEqual('myhost', hostname)
- self.assertEqual('myhost.domain.com', fqdn)
-
- def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self):
- """When cfg has both fqdn and hostname keys, return them."""
- hostname, fqdn = util.get_hostname_fqdn(
- cfg={'fqdn': 'myhost.domain.com', 'hostname': 'other'}, cloud=None)
- self.assertEqual('other', hostname)
- self.assertEqual('myhost.domain.com', fqdn)
-
- def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self):
- """When cfg has only hostname key which represents a fqdn, use that."""
- hostname, fqdn = util.get_hostname_fqdn(
- cfg={'hostname': 'myhost.domain.com'}, cloud=None)
- self.assertEqual('myhost', hostname)
- self.assertEqual('myhost.domain.com', fqdn)
-
- def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self):
- """When cfg has a hostname without a '.' query cloud.get_hostname."""
- mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com')
- hostname, fqdn = util.get_hostname_fqdn(
- cfg={'hostname': 'myhost'}, cloud=mycloud)
- self.assertEqual('myhost', hostname)
- self.assertEqual('cloudhost.mycloud.com', fqdn)
- self.assertEqual(
- [{'fqdn': True, 'metadata_only': False}], mycloud.calls)
-
- def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self):
- """When cfg has neither hostname nor fqdn cloud.get_hostname."""
- mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com')
- hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud)
- self.assertEqual('cloudhost', hostname)
- self.assertEqual('cloudhost.mycloud.com', fqdn)
- self.assertEqual(
- [{'fqdn': True, 'metadata_only': False},
- {'metadata_only': False}], mycloud.calls)
-
- def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self):
- """Calls to cloud.get_hostname pass the metadata_only parameter."""
- mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com')
- _hn, _fqdn = util.get_hostname_fqdn(
- cfg={}, cloud=mycloud, metadata_only=True)
- self.assertEqual(
- [{'fqdn': True, 'metadata_only': True},
- {'metadata_only': True}], mycloud.calls)
-
-
-class TestBlkid(CiTestCase):
- ids = {
- "id01": "1111-1111",
- "id02": "22222222-2222",
- "id03": "33333333-3333",
- "id04": "44444444-4444",
- "id05": "55555555-5555-5555-5555-555555555555",
- "id06": "66666666-6666-6666-6666-666666666666",
- "id07": "52894610484658920398",
- "id08": "86753098675309867530",
- "id09": "99999999-9999-9999-9999-999999999999",
- }
-
- blkid_out = dedent("""\
- /dev/loop0: TYPE="squashfs"
- /dev/loop1: TYPE="squashfs"
- /dev/loop2: TYPE="squashfs"
- /dev/loop3: TYPE="squashfs"
- /dev/sda1: UUID="{id01}" TYPE="vfat" PARTUUID="{id02}"
- /dev/sda2: UUID="{id03}" TYPE="ext4" PARTUUID="{id04}"
- /dev/sda3: UUID="{id05}" TYPE="ext4" PARTUUID="{id06}"
- /dev/sda4: LABEL="default" UUID="{id07}" UUID_SUB="{id08}" """
- """TYPE="zfs_member" PARTUUID="{id09}"
- /dev/loop4: TYPE="squashfs"
- """)
-
- maxDiff = None
-
- def _get_expected(self):
- return ({
- "/dev/loop0": {"DEVNAME": "/dev/loop0", "TYPE": "squashfs"},
- "/dev/loop1": {"DEVNAME": "/dev/loop1", "TYPE": "squashfs"},
- "/dev/loop2": {"DEVNAME": "/dev/loop2", "TYPE": "squashfs"},
- "/dev/loop3": {"DEVNAME": "/dev/loop3", "TYPE": "squashfs"},
- "/dev/loop4": {"DEVNAME": "/dev/loop4", "TYPE": "squashfs"},
- "/dev/sda1": {"DEVNAME": "/dev/sda1", "TYPE": "vfat",
- "UUID": self.ids["id01"],
- "PARTUUID": self.ids["id02"]},
- "/dev/sda2": {"DEVNAME": "/dev/sda2", "TYPE": "ext4",
- "UUID": self.ids["id03"],
- "PARTUUID": self.ids["id04"]},
- "/dev/sda3": {"DEVNAME": "/dev/sda3", "TYPE": "ext4",
- "UUID": self.ids["id05"],
- "PARTUUID": self.ids["id06"]},
- "/dev/sda4": {"DEVNAME": "/dev/sda4", "TYPE": "zfs_member",
- "LABEL": "default",
- "UUID": self.ids["id07"],
- "UUID_SUB": self.ids["id08"],
- "PARTUUID": self.ids["id09"]},
- })
-
- @mock.patch("cloudinit.subp.subp")
- def test_functional_blkid(self, m_subp):
- m_subp.return_value = (
- self.blkid_out.format(**self.ids), "")
- self.assertEqual(self._get_expected(), util.blkid())
- m_subp.assert_called_with(["blkid", "-o", "full"], capture=True,
- decode="replace")
-
- @mock.patch("cloudinit.subp.subp")
- def test_blkid_no_cache_uses_no_cache(self, m_subp):
- """blkid should turn off cache if disable_cache is true."""
- m_subp.return_value = (
- self.blkid_out.format(**self.ids), "")
- self.assertEqual(self._get_expected(),
- util.blkid(disable_cache=True))
- m_subp.assert_called_with(["blkid", "-o", "full", "-c", "/dev/null"],
- capture=True, decode="replace")
-
-
-@mock.patch('cloudinit.subp.subp')
-class TestUdevadmSettle(CiTestCase):
- def test_with_no_params(self, m_subp):
- """called with no parameters."""
- util.udevadm_settle()
- m_subp.called_once_with(mock.call(['udevadm', 'settle']))
-
- def test_with_exists_and_not_exists(self, m_subp):
- """with exists=file where file does not exist should invoke subp."""
- mydev = self.tmp_path("mydev")
- util.udevadm_settle(exists=mydev)
- m_subp.called_once_with(
- ['udevadm', 'settle', '--exit-if-exists=%s' % mydev])
-
- def test_with_exists_and_file_exists(self, m_subp):
- """with exists=file where file does exist should not invoke subp."""
- mydev = self.tmp_path("mydev")
- util.write_file(mydev, "foo\n")
- util.udevadm_settle(exists=mydev)
- self.assertIsNone(m_subp.call_args)
-
- def test_with_timeout_int(self, m_subp):
- """timeout can be an integer."""
- timeout = 9
- util.udevadm_settle(timeout=timeout)
- m_subp.called_once_with(
- ['udevadm', 'settle', '--timeout=%s' % timeout])
-
- def test_with_timeout_string(self, m_subp):
- """timeout can be a string."""
- timeout = "555"
- util.udevadm_settle(timeout=timeout)
- m_subp.assert_called_once_with(
- ['udevadm', 'settle', '--timeout=%s' % timeout])
-
- def test_with_exists_and_timeout(self, m_subp):
- """test call with both exists and timeout."""
- mydev = self.tmp_path("mydev")
- timeout = "3"
- util.udevadm_settle(exists=mydev)
- m_subp.called_once_with(
- ['udevadm', 'settle', '--exit-if-exists=%s' % mydev,
- '--timeout=%s' % timeout])
-
- def test_subp_exception_raises_to_caller(self, m_subp):
- m_subp.side_effect = subp.ProcessExecutionError("BOOM")
- self.assertRaises(subp.ProcessExecutionError, util.udevadm_settle)
-
-
-@mock.patch('os.path.exists')
-class TestGetLinuxDistro(CiTestCase):
-
- def setUp(self):
- # python2 has no lru_cache, and therefore, no cache_clear()
- if hasattr(util.get_linux_distro, "cache_clear"):
- util.get_linux_distro.cache_clear()
-
- @classmethod
- def os_release_exists(self, path):
- """Side effect function"""
- if path == '/etc/os-release':
- return 1
-
- @classmethod
- def redhat_release_exists(self, path):
- """Side effect function """
- if path == '/etc/redhat-release':
- return 1
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_distro_quoted_name(self, m_os_release, m_path_exists):
- """Verify we get the correct name if the os-release file has
- the distro name in quotes"""
- m_os_release.return_value = OS_RELEASE_SLES
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('sles', '12.3', platform.machine()), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_distro_bare_name(self, m_os_release, m_path_exists):
- """Verify we get the correct name if the os-release file does not
- have the distro name in quotes"""
- m_os_release.return_value = OS_RELEASE_UBUNTU
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('ubuntu', '16.04', 'xenial'), dist)
-
- @mock.patch('platform.system')
- @mock.patch('platform.release')
- @mock.patch('cloudinit.util._parse_redhat_release')
- def test_get_linux_freebsd(self, m_parse_redhat_release,
- m_platform_release,
- m_platform_system, m_path_exists):
- """Verify we get the correct name and release name on FreeBSD."""
- m_path_exists.return_value = False
- m_platform_release.return_value = '12.0-RELEASE-p10'
- m_platform_system.return_value = 'FreeBSD'
- m_parse_redhat_release.return_value = {}
- util.is_BSD.cache_clear()
- dist = util.get_linux_distro()
- self.assertEqual(('freebsd', '12.0-RELEASE-p10', ''), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_centos6(self, m_os_release, m_path_exists):
- """Verify we get the correct name and release name on CentOS 6."""
- m_os_release.return_value = REDHAT_RELEASE_CENTOS_6
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('centos', '6.10', 'Final'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_centos7_redhat_release(self, m_os_release, m_exists):
- """Verify the correct release info on CentOS 7 without os-release."""
- m_os_release.return_value = REDHAT_RELEASE_CENTOS_7
- m_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('centos', '7.5.1804', 'Core'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_redhat7_osrelease(self, m_os_release, m_path_exists):
- """Verify redhat 7 read from os-release."""
- m_os_release.return_value = OS_RELEASE_REDHAT_7
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('redhat', '7.5', 'Maipo'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_redhat7_rhrelease(self, m_os_release, m_path_exists):
- """Verify redhat 7 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_REDHAT_7
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('redhat', '7.5', 'Maipo'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_redhat6_rhrelease(self, m_os_release, m_path_exists):
- """Verify redhat 6 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_REDHAT_6
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('redhat', '6.10', 'Santiago'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_copr_centos(self, m_os_release, m_path_exists):
- """Verify we get the correct name and release name on COPR CentOS."""
- m_os_release.return_value = OS_RELEASE_CENTOS
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('centos', '7', 'Core'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_almalinux8_rhrelease(self, m_os_release, m_path_exists):
- """Verify almalinux 8 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_ALMALINUX_8
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_almalinux8_osrelease(self, m_os_release, m_path_exists):
- """Verify almalinux 8 read from os-release."""
- m_os_release.return_value = OS_RELEASE_ALMALINUX_8
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_eurolinux7_rhrelease(self, m_os_release, m_path_exists):
- """Verify eurolinux 7 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_7
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_eurolinux7_osrelease(self, m_os_release, m_path_exists):
- """Verify eurolinux 7 read from os-release."""
- m_os_release.return_value = OS_RELEASE_EUROLINUX_7
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_eurolinux8_rhrelease(self, m_os_release, m_path_exists):
- """Verify eurolinux 8 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_8
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_eurolinux8_osrelease(self, m_os_release, m_path_exists):
- """Verify eurolinux 8 read from os-release."""
- m_os_release.return_value = OS_RELEASE_EUROLINUX_8
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists):
- """Verify rocky linux 8 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_ROCKY_8
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_rocky8_osrelease(self, m_os_release, m_path_exists):
- """Verify rocky linux 8 read from os-release."""
- m_os_release.return_value = OS_RELEASE_ROCKY_8
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_virtuozzo8_rhrelease(self, m_os_release, m_path_exists):
- """Verify virtuozzo linux 8 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_VIRTUOZZO_8
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_virtuozzo8_osrelease(self, m_os_release, m_path_exists):
- """Verify virtuozzo linux 8 read from os-release."""
- m_os_release.return_value = OS_RELEASE_VIRTUOZZO_8
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_cloud8_rhrelease(self, m_os_release, m_path_exists):
- """Verify cloudlinux 8 read from redhat-release."""
- m_os_release.return_value = REDHAT_RELEASE_CLOUDLINUX_8
- m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_cloud8_osrelease(self, m_os_release, m_path_exists):
- """Verify cloudlinux 8 read from os-release."""
- m_os_release.return_value = OS_RELEASE_CLOUDLINUX_8
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_debian(self, m_os_release, m_path_exists):
- """Verify we get the correct name and release name on Debian."""
- m_os_release.return_value = OS_RELEASE_DEBIAN
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('debian', '9', 'stretch'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_openeuler(self, m_os_release, m_path_exists):
- """Verify get the correct name and release name on Openeuler."""
- m_os_release.return_value = OS_RELEASE_OPENEULER_20
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('openEuler', '20.03', 'LTS-SP2'), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_opensuse(self, m_os_release, m_path_exists):
- """Verify we get the correct name and machine arch on openSUSE
- prior to openSUSE Leap 15.
- """
- m_os_release.return_value = OS_RELEASE_OPENSUSE
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('opensuse', '42.3', platform.machine()), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_opensuse_l15(self, m_os_release, m_path_exists):
- """Verify we get the correct name and machine arch on openSUSE
- for openSUSE Leap 15.0 and later.
- """
- m_os_release.return_value = OS_RELEASE_OPENSUSE_L15
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(('opensuse-leap', '15.0', platform.machine()), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_opensuse_tw(self, m_os_release, m_path_exists):
- """Verify we get the correct name and machine arch on openSUSE
- for openSUSE Tumbleweed
- """
- m_os_release.return_value = OS_RELEASE_OPENSUSE_TW
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(
- ('opensuse-tumbleweed', '20180920', platform.machine()), dist)
-
- @mock.patch('cloudinit.util.load_file')
- def test_get_linux_photon_os_release(self, m_os_release, m_path_exists):
- """Verify we get the correct name and machine arch on PhotonOS"""
- m_os_release.return_value = OS_RELEASE_PHOTON
- m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
- dist = util.get_linux_distro()
- self.assertEqual(
- ('photon', '4.0', 'VMware Photon OS/Linux'), dist)
-
- @mock.patch('platform.system')
- @mock.patch('platform.dist', create=True)
- def test_get_linux_distro_no_data(self, m_platform_dist,
- m_platform_system, m_path_exists):
- """Verify we get no information if os-release does not exist"""
- m_platform_dist.return_value = ('', '', '')
- m_platform_system.return_value = "Linux"
- m_path_exists.return_value = 0
- dist = util.get_linux_distro()
- self.assertEqual(('', '', ''), dist)
-
- @mock.patch('platform.system')
- @mock.patch('platform.dist', create=True)
- def test_get_linux_distro_no_impl(self, m_platform_dist,
- m_platform_system, m_path_exists):
- """Verify we get an empty tuple when no information exists and
- Exceptions are not propagated"""
- m_platform_dist.side_effect = Exception()
- m_platform_system.return_value = "Linux"
- m_path_exists.return_value = 0
- dist = util.get_linux_distro()
- self.assertEqual(('', '', ''), dist)
-
- @mock.patch('platform.system')
- @mock.patch('platform.dist', create=True)
- def test_get_linux_distro_plat_data(self, m_platform_dist,
- m_platform_system, m_path_exists):
- """Verify we get the correct platform information"""
- m_platform_dist.return_value = ('foo', '1.1', 'aarch64')
- m_platform_system.return_value = "Linux"
- m_path_exists.return_value = 0
- dist = util.get_linux_distro()
- self.assertEqual(('foo', '1.1', 'aarch64'), dist)
-
-
-class TestGetVariant:
- @pytest.mark.parametrize('info, expected_variant', [
- ({'system': 'Linux', 'dist': ('almalinux',)}, 'almalinux'),
- ({'system': 'linux', 'dist': ('alpine',)}, 'alpine'),
- ({'system': 'linux', 'dist': ('arch',)}, 'arch'),
- ({'system': 'linux', 'dist': ('centos',)}, 'centos'),
- ({'system': 'linux', 'dist': ('cloudlinux',)}, 'cloudlinux'),
- ({'system': 'linux', 'dist': ('debian',)}, 'debian'),
- ({'system': 'linux', 'dist': ('eurolinux',)}, 'eurolinux'),
- ({'system': 'linux', 'dist': ('fedora',)}, 'fedora'),
- ({'system': 'linux', 'dist': ('openEuler',)}, 'openeuler'),
- ({'system': 'linux', 'dist': ('photon',)}, 'photon'),
- ({'system': 'linux', 'dist': ('rhel',)}, 'rhel'),
- ({'system': 'linux', 'dist': ('rocky',)}, 'rocky'),
- ({'system': 'linux', 'dist': ('suse',)}, 'suse'),
- ({'system': 'linux', 'dist': ('virtuozzo',)}, 'virtuozzo'),
- ({'system': 'linux', 'dist': ('ubuntu',)}, 'ubuntu'),
- ({'system': 'linux', 'dist': ('linuxmint',)}, 'ubuntu'),
- ({'system': 'linux', 'dist': ('mint',)}, 'ubuntu'),
- ({'system': 'linux', 'dist': ('redhat',)}, 'rhel'),
- ({'system': 'linux', 'dist': ('opensuse',)}, 'suse'),
- ({'system': 'linux', 'dist': ('opensuse-tumbleweed',)}, 'suse'),
- ({'system': 'linux', 'dist': ('opensuse-leap',)}, 'suse'),
- ({'system': 'linux', 'dist': ('sles',)}, 'suse'),
- ({'system': 'linux', 'dist': ('sle_hpc',)}, 'suse'),
- ({'system': 'linux', 'dist': ('my_distro',)}, 'linux'),
- ({'system': 'Windows', 'dist': ('dontcare',)}, 'windows'),
- ({'system': 'Darwin', 'dist': ('dontcare',)}, 'darwin'),
- ({'system': 'Freebsd', 'dist': ('dontcare',)}, 'freebsd'),
- ({'system': 'Netbsd', 'dist': ('dontcare',)}, 'netbsd'),
- ({'system': 'Openbsd', 'dist': ('dontcare',)}, 'openbsd'),
- ({'system': 'Dragonfly', 'dist': ('dontcare',)}, 'dragonfly'),
- ])
- def test_get_variant(self, info, expected_variant):
- """Verify we get the correct variant name"""
- assert util._get_variant(info) == expected_variant
-
-
-class TestJsonDumps(CiTestCase):
- def test_is_str(self):
- """json_dumps should return a string."""
- self.assertTrue(isinstance(util.json_dumps({'abc': '123'}), str))
-
- def test_utf8(self):
- smiley = '\\ud83d\\ude03'
- self.assertEqual(
- {'smiley': smiley},
- json.loads(util.json_dumps({'smiley': smiley})))
-
- def test_non_utf8(self):
- blob = b'\xba\x03Qx-#y\xea'
- self.assertEqual(
- {'blob': 'ci-b64:' + base64.b64encode(blob).decode('utf-8')},
- json.loads(util.json_dumps({'blob': blob})))
-
-
-@mock.patch('os.path.exists')
-class TestIsLXD(CiTestCase):
-
- def test_is_lxd_true_on_sock_device(self, m_exists):
- """When lxd's /dev/lxd/sock exists, is_lxd returns true."""
- m_exists.return_value = True
- self.assertTrue(util.is_lxd())
- m_exists.assert_called_once_with('/dev/lxd/sock')
-
- def test_is_lxd_false_when_sock_device_absent(self, m_exists):
- """When lxd's /dev/lxd/sock is absent, is_lxd returns false."""
- m_exists.return_value = False
- self.assertFalse(util.is_lxd())
- m_exists.assert_called_once_with('/dev/lxd/sock')
-
-
-class TestReadCcFromCmdline:
-
- @pytest.mark.parametrize(
- "cmdline,expected_cfg",
- [
- # Return None if cmdline has no cc:<YAML>end_cc content.
- (CiTestCase.random_string(), None),
- # Return None if YAML content is empty string.
- ('foo cc: end_cc bar', None),
- # Return expected dictionary without trailing end_cc marker.
- ('foo cc: ssh_pwauth: true', {'ssh_pwauth': True}),
- # Return expected dictionary w escaped newline and no end_cc.
- ('foo cc: ssh_pwauth: true\\n', {'ssh_pwauth': True}),
- # Return expected dictionary of yaml between cc: and end_cc.
- ('foo cc: ssh_pwauth: true end_cc bar', {'ssh_pwauth': True}),
- # Return dict with list value w escaped newline, no end_cc.
- (
- 'cc: ssh_import_id: [smoser, kirkland]\\n',
- {'ssh_import_id': ['smoser', 'kirkland']}
- ),
- # Parse urlencoded brackets in yaml content.
- (
- 'cc: ssh_import_id: %5Bsmoser, kirkland%5D end_cc',
- {'ssh_import_id': ['smoser', 'kirkland']}
- ),
- # Parse complete urlencoded yaml content.
- (
- 'cc: ssh_import_id%3A%20%5Buser1%2C%20user2%5D end_cc',
- {'ssh_import_id': ['user1', 'user2']}
- ),
- # Parse nested dictionary in yaml content.
- (
- 'cc: ntp: {enabled: true, ntp_client: myclient} end_cc',
- {'ntp': {'enabled': True, 'ntp_client': 'myclient'}}
- ),
- # Parse single mapping value in yaml content.
- ('cc: ssh_import_id: smoser end_cc', {'ssh_import_id': 'smoser'}),
- # Parse multiline content with multiple mapping and nested lists.
- (
- ('cc: ssh_import_id: [smoser, bob]\\n'
- 'runcmd: [ [ ls, -l ], echo hi ] end_cc'),
- {'ssh_import_id': ['smoser', 'bob'],
- 'runcmd': [['ls', '-l'], 'echo hi']}
- ),
- # Parse multiline encoded content w/ mappings and nested lists.
- (
- ('cc: ssh_import_id: %5Bsmoser, bob%5D\\n'
- 'runcmd: [ [ ls, -l ], echo hi ] end_cc'),
- {'ssh_import_id': ['smoser', 'bob'],
- 'runcmd': [['ls', '-l'], 'echo hi']}
- ),
- # test encoded escaped newlines work.
- #
- # unquote(encoded_content)
- # 'ssh_import_id: [smoser, bob]\\nruncmd: [ [ ls, -l ], echo hi ]'
- (
- ('cc: ' +
- ('ssh_import_id%3A%20%5Bsmoser%2C%20bob%5D%5Cn'
- 'runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%2C'
- '%20echo%20hi%20%5D') + ' end_cc'),
- {'ssh_import_id': ['smoser', 'bob'],
- 'runcmd': [['ls', '-l'], 'echo hi']}
- ),
- # test encoded newlines work.
- #
- # unquote(encoded_content)
- # 'ssh_import_id: [smoser, bob]\nruncmd: [ [ ls, -l ], echo hi ]'
- (
- ("cc: " +
- ('ssh_import_id%3A%20%5Bsmoser%2C%20bob%5D%0A'
- 'runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%2C'
- '%20echo%20hi%20%5D') + ' end_cc'),
- {'ssh_import_id': ['smoser', 'bob'],
- 'runcmd': [['ls', '-l'], 'echo hi']}
- ),
- # Parse and merge multiple yaml content sections.
- (
- ('cc:ssh_import_id: [smoser, bob] end_cc '
- 'cc: runcmd: [ [ ls, -l ] ] end_cc'),
- {'ssh_import_id': ['smoser', 'bob'],
- 'runcmd': [['ls', '-l']]}
- ),
- # Parse and merge multiple encoded yaml content sections.
- (
- ('cc:ssh_import_id%3A%20%5Bsmoser%5D end_cc '
- 'cc:runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%20%5D end_cc'),
- {'ssh_import_id': ['smoser'], 'runcmd': [['ls', '-l']]}
- ),
- ]
- )
- def test_read_conf_from_cmdline_config(self, expected_cfg, cmdline):
- assert expected_cfg == util.read_conf_from_cmdline(cmdline=cmdline)
-
-
-class TestMountCb:
- """Tests for ``util.mount_cb``.
-
- These tests consider the "unit" under test to be ``util.mount_cb`` and
- ``util.unmounter``, which is only used by ``mount_cb``.
-
- TODO: Test default mtype determination
- TODO: Test the if/else branch that actually performs the mounting operation
- """
-
- @pytest.yield_fixture
- def already_mounted_device_and_mountdict(self):
- """Mock an already-mounted device, and yield (device, mount dict)"""
- device = "/dev/fake0"
- mountpoint = "/mnt/fake"
- with mock.patch("cloudinit.util.subp.subp"):
- with mock.patch("cloudinit.util.mounts") as m_mounts:
- mounts = {device: {"mountpoint": mountpoint}}
- m_mounts.return_value = mounts
- yield device, mounts[device]
-
- @pytest.fixture
- def already_mounted_device(self, already_mounted_device_and_mountdict):
- """already_mounted_device_and_mountdict, but return only the device"""
- return already_mounted_device_and_mountdict[0]
-
- @pytest.mark.parametrize(
- "mtype,expected",
- [
- # While the filesystem is called iso9660, the mount type is cd9660
- ("iso9660", "cd9660"),
- # vfat is generally called "msdos" on BSD
- ("vfat", "msdos"),
- # judging from man pages, only FreeBSD has this alias
- ("msdosfs", "msdos"),
- # Test happy path
- ("ufs", "ufs")
- ],
- )
- @mock.patch("cloudinit.util.is_Linux", autospec=True)
- @mock.patch("cloudinit.util.is_BSD", autospec=True)
- @mock.patch("cloudinit.util.subp.subp")
- @mock.patch("cloudinit.temp_utils.tempdir", autospec=True)
- def test_normalize_mtype_on_bsd(
- self, m_tmpdir, m_subp, m_is_BSD, m_is_Linux, mtype, expected
- ):
- m_is_BSD.return_value = True
- m_is_Linux.return_value = False
- m_tmpdir.return_value.__enter__ = mock.Mock(
- autospec=True, return_value="/tmp/fake"
- )
- m_tmpdir.return_value.__exit__ = mock.Mock(
- autospec=True, return_value=True
- )
- callback = mock.Mock(autospec=True)
-
- util.mount_cb('/dev/fake0', callback, mtype=mtype)
- assert mock.call(
- ["mount", "-o", "ro", "-t", expected, "/dev/fake0", "/tmp/fake"],
- update_env=None) in m_subp.call_args_list
-
- @pytest.mark.parametrize("invalid_mtype", [int(0), float(0.0), dict()])
- def test_typeerror_raised_for_invalid_mtype(self, invalid_mtype):
- with pytest.raises(TypeError):
- util.mount_cb(mock.Mock(), mock.Mock(), mtype=invalid_mtype)
-
- @mock.patch("cloudinit.util.subp.subp")
- def test_already_mounted_does_not_mount_or_umount_anything(
- self, m_subp, already_mounted_device
- ):
- util.mount_cb(already_mounted_device, mock.Mock())
-
- assert 0 == m_subp.call_count
-
- @pytest.mark.parametrize("trailing_slash_in_mounts", ["/", ""])
- def test_already_mounted_calls_callback(
- self, trailing_slash_in_mounts, already_mounted_device_and_mountdict
- ):
- device, mount_dict = already_mounted_device_and_mountdict
- mountpoint = mount_dict["mountpoint"]
- mount_dict["mountpoint"] += trailing_slash_in_mounts
-
- callback = mock.Mock()
- util.mount_cb(device, callback)
-
- # The mountpoint passed to callback should always have a trailing
- # slash, regardless of the input
- assert [mock.call(mountpoint + "/")] == callback.call_args_list
-
- def test_already_mounted_calls_callback_with_data(
- self, already_mounted_device
- ):
- callback = mock.Mock()
- util.mount_cb(
- already_mounted_device, callback, data=mock.sentinel.data
- )
-
- assert [
- mock.call(mock.ANY, mock.sentinel.data)
- ] == callback.call_args_list
-
-
-@mock.patch("cloudinit.util.write_file")
-class TestEnsureFile:
- """Tests for ``cloudinit.util.ensure_file``."""
-
- def test_parameters_passed_through(self, m_write_file):
- """Test the parameters in the signature are passed to write_file."""
- util.ensure_file(
- mock.sentinel.path,
- mode=mock.sentinel.mode,
- preserve_mode=mock.sentinel.preserve_mode,
- )
-
- assert 1 == m_write_file.call_count
- args, kwargs = m_write_file.call_args
- assert (mock.sentinel.path,) == args
- assert mock.sentinel.mode == kwargs["mode"]
- assert mock.sentinel.preserve_mode == kwargs["preserve_mode"]
-
- @pytest.mark.parametrize(
- "kwarg,expected",
- [
- # Files should be world-readable by default
- ("mode", 0o644),
- # The previous behaviour of not preserving mode should be retained
- ("preserve_mode", False),
- ],
- )
- def test_defaults(self, m_write_file, kwarg, expected):
- """Test that ensure_file defaults appropriately."""
- util.ensure_file(mock.sentinel.path)
-
- assert 1 == m_write_file.call_count
- _args, kwargs = m_write_file.call_args
- assert expected == kwargs[kwarg]
-
- def test_static_parameters_are_passed(self, m_write_file):
- """Test that the static write_files parameters are passed correctly."""
- util.ensure_file(mock.sentinel.path)
-
- assert 1 == m_write_file.call_count
- _args, kwargs = m_write_file.call_args
- assert "" == kwargs["content"]
- assert "ab" == kwargs["omode"]
-
-
-@mock.patch("cloudinit.util.grp.getgrnam")
-@mock.patch("cloudinit.util.os.setgid")
-@mock.patch("cloudinit.util.os.umask")
-class TestRedirectOutputPreexecFn:
- """This tests specifically the preexec_fn used in redirect_output."""
-
- @pytest.fixture(params=["outfmt", "errfmt"])
- def preexec_fn(self, request):
- """A fixture to gather the preexec_fn used by redirect_output.
-
- This enables simpler direct testing of it, and parameterises any tests
- using it to cover both the stdout and stderr code paths.
- """
- test_string = "| piped output to invoke subprocess"
- if request.param == "outfmt":
- args = (test_string, None)
- elif request.param == "errfmt":
- args = (None, test_string)
- with mock.patch("cloudinit.util.subprocess.Popen") as m_popen:
- util.redirect_output(*args)
-
- assert 1 == m_popen.call_count
- _args, kwargs = m_popen.call_args
- assert "preexec_fn" in kwargs, "preexec_fn not passed to Popen"
- return kwargs["preexec_fn"]
-
- def test_preexec_fn_sets_umask(
- self, m_os_umask, _m_setgid, _m_getgrnam, preexec_fn
- ):
- """preexec_fn should set a mask that avoids world-readable files."""
- preexec_fn()
-
- assert [mock.call(0o037)] == m_os_umask.call_args_list
-
- def test_preexec_fn_sets_group_id_if_adm_group_present(
- self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn
- ):
- """We should setgrp to adm if present, so files are owned by them."""
- fake_group = mock.Mock(gr_gid=mock.sentinel.gr_gid)
- m_getgrnam.return_value = fake_group
-
- preexec_fn()
-
- assert [mock.call("adm")] == m_getgrnam.call_args_list
- assert [mock.call(mock.sentinel.gr_gid)] == m_setgid.call_args_list
-
- def test_preexec_fn_handles_absent_adm_group_gracefully(
- self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn
- ):
- """We should handle an absent adm group gracefully."""
- m_getgrnam.side_effect = KeyError("getgrnam(): name not found: 'adm'")
-
- preexec_fn()
-
- assert 0 == m_setgid.call_count
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_version.py b/cloudinit/tests/test_version.py
deleted file mode 100644
index 778a762c..00000000
--- a/cloudinit/tests/test_version.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from unittest import mock
-
-from cloudinit.tests.helpers import CiTestCase
-from cloudinit import version
-
-
-class TestExportsFeatures(CiTestCase):
- def test_has_network_config_v1(self):
- self.assertIn('NETWORK_CONFIG_V1', version.FEATURES)
-
- def test_has_network_config_v2(self):
- self.assertIn('NETWORK_CONFIG_V2', version.FEATURES)
-
-
-class TestVersionString(CiTestCase):
- @mock.patch("cloudinit.version._PACKAGED_VERSION",
- "17.2-3-gb05b9972-0ubuntu1")
- def test_package_version_respected(self):
- """If _PACKAGED_VERSION is filled in, then it should be returned."""
- self.assertEqual("17.2-3-gb05b9972-0ubuntu1", version.version_string())
-
- @mock.patch("cloudinit.version._PACKAGED_VERSION", "@@PACKAGED_VERSION@@")
- @mock.patch("cloudinit.version.__VERSION__", "17.2")
- def test_package_version_skipped(self):
- """If _PACKAGED_VERSION is not modified, then return __VERSION__."""
- self.assertEqual("17.2", version.version_string())
-
-
-# vi: ts=4 expandtab