cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #03542
[Merge] ~raharper/cloud-init:ubuntu-devel-new-artful-release-v5 into cloud-init:ubuntu/devel
Scott Moser has proposed merging ~raharper/cloud-init:ubuntu-devel-new-artful-release-v5 into cloud-init:ubuntu/devel.
Requested reviews:
cloud-init commiters (cloud-init-dev)
Related bugs:
Bug #1717969 in cloud-init: "Exhausting the task limit"
https://bugs.launchpad.net/cloud-init/+bug/1717969
Bug #1718029 in cloud-init: "cloudstack and azure datasources broken when using netplan/systemd-networkd"
https://bugs.launchpad.net/cloud-init/+bug/1718029
Bug #1718675 in cloud-init: "It should be possible to add repos in SUSE distros"
https://bugs.launchpad.net/cloud-init/+bug/1718675
For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/331723
--
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:ubuntu-devel-new-artful-release-v5 into cloud-init:ubuntu/devel.
diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py
index 0066e97..35d8c57 100755
--- a/cloudinit/config/cc_ssh_authkey_fingerprints.py
+++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py
@@ -28,7 +28,7 @@ the keys can be specified, but defaults to ``md5``.
import base64
import hashlib
-from prettytable import PrettyTable
+from cloudinit.simpletable import SimpleTable
from cloudinit.distros import ug_util
from cloudinit import ssh_util
@@ -74,7 +74,7 @@ def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5',
return
tbl_fields = ['Keytype', 'Fingerprint (%s)' % (hash_meth), 'Options',
'Comment']
- tbl = PrettyTable(tbl_fields)
+ tbl = SimpleTable(tbl_fields)
for entry in key_entries:
if _is_printable_key(entry):
row = []
diff --git a/cloudinit/config/cc_zypper_add_repo.py b/cloudinit/config/cc_zypper_add_repo.py
new file mode 100644
index 0000000..aba2695
--- /dev/null
+++ b/cloudinit/config/cc_zypper_add_repo.py
@@ -0,0 +1,218 @@
+#
+# Copyright (C) 2017 SUSE LLC.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""zypper_add_repo: Add zyper repositories to the system"""
+
+import configobj
+import os
+from six import string_types
+from textwrap import dedent
+
+from cloudinit.config.schema import get_schema_doc
+from cloudinit import log as logging
+from cloudinit.settings import PER_ALWAYS
+from cloudinit import util
+
+distros = ['opensuse', 'sles']
+
+schema = {
+ 'id': 'cc_zypper_add_repo',
+ 'name': 'ZypperAddRepo',
+ 'title': 'Configure zypper behavior and add zypper repositories',
+ 'description': dedent("""\
+ Configure zypper behavior by modifying /etc/zypp/zypp.conf. The
+ configuration writer is "dumb" and will simply append the provided
+ configuration options to the configuration file. Option settings
+ that may be duplicate will be resolved by the way the zypp.conf file
+ is parsed. The file is in INI format.
+ Add repositories to the system. No validation is performed on the
+ repository file entries, it is assumed the user is familiar with
+ the zypper repository file format."""),
+ 'distros': distros,
+ 'examples': [dedent("""\
+ zypper:
+ repos:
+ - id: opensuse-oss
+ name: os-oss
+ baseurl: http://dl.opensuse.org/dist/leap/v/repo/oss/
+ enabled: 1
+ autorefresh: 1
+ - id: opensuse-oss-update
+ name: os-oss-up
+ baseurl: http://dl.opensuse.org/dist/leap/v/update
+ # any setting per
+ # https://en.opensuse.org/openSUSE:Standards_RepoInfo
+ # enable and autorefresh are on by default
+ config:
+ reposdir: /etc/zypp/repos.dir
+ servicesdir: /etc/zypp/services.d
+ download.use_deltarpm: true
+ # any setting in /etc/zypp/zypp.conf
+ """)],
+ 'frequency': PER_ALWAYS,
+ 'type': 'object',
+ 'properties': {
+ 'zypper': {
+ 'type': 'object',
+ 'properties': {
+ 'repos': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {
+ 'type': 'string',
+ 'description': dedent("""\
+ The unique id of the repo, used when
+ writing
+ /etc/zypp/repos.d/<id>.repo.""")
+ },
+ 'baseurl': {
+ 'type': 'string',
+ 'format': 'uri', # built-in format type
+ 'description': 'The base repositoy URL'
+ }
+ },
+ 'required': ['id', 'baseurl'],
+ 'additionalProperties': True
+ },
+ 'minItems': 1
+ },
+ 'config': {
+ 'type': 'object',
+ 'description': dedent("""\
+ Any supported zypo.conf key is written to
+ /etc/zypp/zypp.conf'""")
+ }
+ },
+ 'required': [],
+ 'minProperties': 1, # Either config or repo must be provided
+ 'additionalProperties': False, # only repos and config allowed
+ }
+ }
+}
+
+__doc__ = get_schema_doc(schema) # Supplement python help()
+
+LOG = logging.getLogger(__name__)
+
+
+def _canonicalize_id(repo_id):
+ repo_id = repo_id.replace(" ", "_")
+ return repo_id
+
+
+def _format_repo_value(val):
+ if isinstance(val, bool):
+ # zypp prefers 1/0
+ return 1 if val else 0
+ if isinstance(val, (list, tuple)):
+ return "\n ".join([_format_repo_value(v) for v in val])
+ if not isinstance(val, string_types):
+ return str(val)
+ return val
+
+
+def _format_repository_config(repo_id, repo_config):
+ to_be = configobj.ConfigObj()
+ to_be[repo_id] = {}
+ # Do basic translation of the items -> values
+ for (k, v) in repo_config.items():
+ # For now assume that people using this know the format
+ # of zypper repos and don't verify keys/values further
+ to_be[repo_id][k] = _format_repo_value(v)
+ lines = to_be.write()
+ return "\n".join(lines)
+
+
+def _write_repos(repos, repo_base_path):
+ """Write the user-provided repo definition files
+ @param repos: A list of repo dictionary objects provided by the user's
+ cloud config.
+ @param repo_base_path: The directory path to which repo definitions are
+ written.
+ """
+
+ if not repos:
+ return
+ valid_repos = {}
+ for index, user_repo_config in enumerate(repos):
+ # Skip on absent required keys
+ missing_keys = set(['id', 'baseurl']).difference(set(user_repo_config))
+ if missing_keys:
+ LOG.warning(
+ "Repo config at index %d is missing required config keys: %s",
+ index, ",".join(missing_keys))
+ continue
+ repo_id = user_repo_config.get('id')
+ canon_repo_id = _canonicalize_id(repo_id)
+ repo_fn_pth = os.path.join(repo_base_path, "%s.repo" % (canon_repo_id))
+ if os.path.exists(repo_fn_pth):
+ LOG.info("Skipping repo %s, file %s already exists!",
+ repo_id, repo_fn_pth)
+ continue
+ elif repo_id in valid_repos:
+ LOG.info("Skipping repo %s, file %s already pending!",
+ repo_id, repo_fn_pth)
+ continue
+
+ # Do some basic key formatting
+ repo_config = dict(
+ (k.lower().strip().replace("-", "_"), v)
+ for k, v in user_repo_config.items()
+ if k and k != 'id')
+
+ # Set defaults if not present
+ for field in ['enabled', 'autorefresh']:
+ if field not in repo_config:
+ repo_config[field] = '1'
+
+ valid_repos[repo_id] = (repo_fn_pth, repo_config)
+
+ for (repo_id, repo_data) in valid_repos.items():
+ repo_blob = _format_repository_config(repo_id, repo_data[-1])
+ util.write_file(repo_data[0], repo_blob)
+
+
+def _write_zypp_config(zypper_config):
+ """Write to the default zypp configuration file /etc/zypp/zypp.conf"""
+ if not zypper_config:
+ return
+ zypp_config = '/etc/zypp/zypp.conf'
+ zypp_conf_content = util.load_file(zypp_config)
+ new_settings = ['# Added via cloud.cfg']
+ for setting, value in zypper_config.items():
+ if setting == 'configdir':
+ msg = 'Changing the location of the zypper configuration is '
+ msg += 'not supported, skipping "configdir" setting'
+ LOG.warning(msg)
+ continue
+ if value:
+ new_settings.append('%s=%s' % (setting, value))
+ if len(new_settings) > 1:
+ new_config = zypp_conf_content + '\n'.join(new_settings)
+ else:
+ new_config = zypp_conf_content
+ util.write_file(zypp_config, new_config)
+
+
+def handle(name, cfg, _cloud, log, _args):
+ zypper_section = cfg.get('zypper')
+ if not zypper_section:
+ LOG.debug(("Skipping module named %s,"
+ " no 'zypper' relevant configuration found"), name)
+ return
+ repos = zypper_section.get('repos')
+ if not repos:
+ LOG.debug(("Skipping module named %s,"
+ " no 'repos' configuration found"), name)
+ return
+ zypper_config = zypper_section.get('config', {})
+ repo_base_path = zypper_config.get('reposdir', '/etc/zypp/repos.d/')
+
+ _write_zypp_config(zypper_config)
+ _write_repos(repos, repo_base_path)
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 0535063..0cba703 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -4,6 +4,7 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+import configobj
import logging
import os
import re
@@ -11,9 +12,12 @@ import re
from cloudinit.net import find_fallback_nic, get_devicelist
from cloudinit import temp_utils
from cloudinit import util
+from six import StringIO
LOG = logging.getLogger(__name__)
+NETWORKD_LEASES_DIR = '/run/systemd/netif/leases'
+
class InvalidDHCPLeaseFileError(Exception):
"""Raised when parsing an empty or invalid dhcp.leases file.
@@ -118,4 +122,42 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
return parse_dhcp_lease_file(lease_file)
+def networkd_parse_lease(content):
+ """Parse a systemd lease file content as in /run/systemd/netif/leases/
+
+ Parse this (almost) ini style file even though it says:
+ # This is private data. Do not parse.
+
+ Simply return a dictionary of key/values."""
+
+ return dict(configobj.ConfigObj(StringIO(content), list_values=False))
+
+
+def networkd_load_leases(leases_d=None):
+ """Return a dictionary of dictionaries representing each lease
+ found in lease_d.i
+
+ The top level key will be the filename, which is typically the ifindex."""
+
+ if leases_d is None:
+ leases_d = NETWORKD_LEASES_DIR
+
+ ret = {}
+ if not os.path.isdir(leases_d):
+ return ret
+ for lfile in os.listdir(leases_d):
+ ret[lfile] = networkd_parse_lease(
+ util.load_file(os.path.join(leases_d, lfile)))
+ return ret
+
+
+def networkd_get_option_from_leases(keyname, leases_d=None):
+ if leases_d is None:
+ leases_d = NETWORKD_LEASES_DIR
+ leases = networkd_load_leases(leases_d=leases_d)
+ for ifindex, data in sorted(leases.items()):
+ if data.get(keyname):
+ return data[keyname]
+ return None
+
# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index a38edae..1c1f504 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -6,9 +6,9 @@ from textwrap import dedent
from cloudinit.net.dhcp import (
InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
- parse_dhcp_lease_file, dhcp_discovery)
+ parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
from cloudinit.util import ensure_file, write_file
-from cloudinit.tests.helpers import CiTestCase, wrap_and_call
+from cloudinit.tests.helpers import CiTestCase, wrap_and_call, populate_dir
class TestParseDHCPLeasesFile(CiTestCase):
@@ -149,3 +149,112 @@ class TestDHCPDiscoveryClean(CiTestCase):
[os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf',
lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),
'eth9', '-sf', '/bin/true'], capture=True)])
+
+
+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))
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 39c79de..8f99d99 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -13,7 +13,7 @@ import re
from cloudinit import log as logging
from cloudinit import util
-from prettytable import PrettyTable
+from cloudinit.simpletable import SimpleTable
LOG = logging.getLogger()
@@ -170,7 +170,7 @@ def netdev_pformat():
lines.append(util.center("Net device info failed", '!', 80))
else:
fields = ['Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address']
- tbl = PrettyTable(fields)
+ tbl = SimpleTable(fields)
for (dev, d) in netdev.items():
tbl.add_row([dev, d["up"], d["addr"], d["mask"], ".", d["hwaddr"]])
if d.get('addr6'):
@@ -194,7 +194,7 @@ def route_pformat():
if routes.get('ipv4'):
fields_v4 = ['Route', 'Destination', 'Gateway',
'Genmask', 'Interface', 'Flags']
- tbl_v4 = PrettyTable(fields_v4)
+ tbl_v4 = SimpleTable(fields_v4)
for (n, r) in enumerate(routes.get('ipv4')):
route_id = str(n)
tbl_v4.add_row([route_id, r['destination'],
@@ -207,7 +207,7 @@ def route_pformat():
if routes.get('ipv6'):
fields_v6 = ['Route', 'Proto', 'Recv-Q', 'Send-Q',
'Local Address', 'Foreign Address', 'State']
- tbl_v6 = PrettyTable(fields_v6)
+ tbl_v6 = SimpleTable(fields_v6)
for (n, r) in enumerate(routes.get('ipv6')):
route_id = str(n)
tbl_v6.add_row([route_id, r['proto'],
diff --git a/cloudinit/simpletable.py b/cloudinit/simpletable.py
new file mode 100644
index 0000000..9060322
--- /dev/null
+++ b/cloudinit/simpletable.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2017 Amazon.com, Inc. or its affiliates
+#
+# Author: Ethan Faust <efaust@xxxxxxxxxx>
+# Author: Andrew Jorgensen <ajorgens@xxxxxxxxxx>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+
+class SimpleTable(object):
+ """A minimal implementation of PrettyTable
+ for distribution with cloud-init.
+ """
+
+ def __init__(self, fields):
+ self.fields = fields
+ self.rows = []
+
+ # initialize list of 0s the same length
+ # as the number of fields
+ self.column_widths = [0] * len(self.fields)
+ self.update_column_widths(fields)
+
+ def update_column_widths(self, values):
+ for i, value in enumerate(values):
+ self.column_widths[i] = max(
+ len(value),
+ self.column_widths[i])
+
+ def add_row(self, values):
+ if len(values) > len(self.fields):
+ raise TypeError('too many values')
+ values = [str(value) for value in values]
+ self.rows.append(values)
+ self.update_column_widths(values)
+
+ def _hdiv(self):
+ """Returns a horizontal divider for the table."""
+ return '+' + '+'.join(
+ ['-' * (w + 2) for w in self.column_widths]) + '+'
+
+ def _row(self, row):
+ """Returns a formatted row."""
+ return '|' + '|'.join(
+ [col.center(self.column_widths[i] + 2)
+ for i, col in enumerate(row)]) + '|'
+
+ def __str__(self):
+ """Returns a string representation of the table with lines around.
+
+ +-----+-----+
+ | one | two |
+ +-----+-----+
+ | 1 | 2 |
+ | 01 | 10 |
+ +-----+-----+
+ """
+ lines = [self._hdiv(), self._row(self.fields), self._hdiv()]
+ lines += [self._row(r) for r in self.rows] + [self._hdiv()]
+ return '\n'.join(lines)
+
+ def get_string(self):
+ return repr(self)
diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py
index ed1d691..c78ad9e 100644
--- a/cloudinit/sources/DataSourceAltCloud.py
+++ b/cloudinit/sources/DataSourceAltCloud.py
@@ -28,8 +28,8 @@ LOG = logging.getLogger(__name__)
CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info'
# Shell command lists
-CMD_PROBE_FLOPPY = ['/sbin/modprobe', 'floppy']
-CMD_UDEVADM_SETTLE = ['/sbin/udevadm', 'settle', '--timeout=5']
+CMD_PROBE_FLOPPY = ['modprobe', 'floppy']
+CMD_UDEVADM_SETTLE = ['udevadm', 'settle', '--timeout=5']
META_DATA_NOT_SUPPORTED = {
'block-device-mapping': {},
diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 7e0f9bb..9dc473f 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -19,6 +19,7 @@ import time
from cloudinit import ec2_utils as ec2
from cloudinit import log as logging
+from cloudinit.net import dhcp
from cloudinit import sources
from cloudinit import url_helper as uhelp
from cloudinit import util
@@ -224,20 +225,28 @@ def get_vr_address():
# Get the address of the virtual router via dhcp leases
# If no virtual router is detected, fallback on default gateway.
# See http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.8/virtual_machines/user-data.html # noqa
+
+ # Try networkd first...
+ latest_address = dhcp.networkd_get_option_from_leases('SERVER_ADDRESS')
+ if latest_address:
+ LOG.debug("Found SERVER_ADDRESS '%s' via networkd_leases",
+ latest_address)
+ return latest_address
+
+ # Try dhcp lease files next...
lease_file = get_latest_lease()
if not lease_file:
LOG.debug("No lease file found, using default gateway")
return get_default_gateway()
- latest_address = None
with open(lease_file, "r") as fd:
for line in fd:
if "dhcp-server-identifier" in line:
words = line.strip(" ;\r\n").split(" ")
if len(words) > 2:
- dhcp = words[2]
- LOG.debug("Found DHCP identifier %s", dhcp)
- latest_address = dhcp
+ dhcptok = words[2]
+ LOG.debug("Found DHCP identifier %s", dhcptok)
+ latest_address = dhcptok
if not latest_address:
# No virtual router found, fallback on default gateway
LOG.debug("No DHCP found, using default gateway")
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index 24b45d5..ccebf11 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -375,26 +375,56 @@ def get_ovf_env(dirname):
return (None, False)
-# Transport functions take no input and return
-# a 3 tuple of content, path, filename
-def transport_iso9660(require_iso=True):
+def maybe_cdrom_device(devname):
+ """Test if devname matches known list of devices which may contain iso9660
+ filesystems.
- # default_regex matches values in
- # /lib/udev/rules.d/60-cdrom_id.rules
- # KERNEL!="sr[0-9]*|hd[a-z]|xvd*", GOTO="cdrom_end"
- envname = "CLOUD_INIT_CDROM_DEV_REGEX"
- default_regex = "^(sr[0-9]+|hd[a-z]|xvd.*)"
+ Be helpful in accepting either knames (with no leading /dev/) or full path
+ names, but do not allow paths outside of /dev/, like /dev/foo/bar/xxx.
+ """
+ if not devname:
+ return False
+ elif not isinstance(devname, util.string_types):
+ raise ValueError("Unexpected input for devname: %s" % devname)
+
+ # resolve '..' and multi '/' elements
+ devname = os.path.normpath(devname)
- devname_regex = os.environ.get(envname, default_regex)
+ # drop leading '/dev/'
+ if devname.startswith("/dev/"):
+ # partition returns tuple (before, partition, after)
+ devname = devname.partition("/dev/")[-1]
+
+ # ignore leading slash (/sr0), else fail on / in name (foo/bar/xvdc)
+ if devname.startswith("/"):
+ devname = devname.split("/")[-1]
+ elif devname.count("/") > 0:
+ return False
+
+ # if empty string
+ if not devname:
+ return False
+
+ # default_regex matches values in /lib/udev/rules.d/60-cdrom_id.rules
+ # KERNEL!="sr[0-9]*|hd[a-z]|xvd*", GOTO="cdrom_end"
+ default_regex = r"^(sr[0-9]+|hd[a-z]|xvd.*)"
+ devname_regex = os.environ.get("CLOUD_INIT_CDROM_DEV_REGEX", default_regex)
cdmatch = re.compile(devname_regex)
+ return cdmatch.match(devname) is not None
+
+
+# Transport functions take no input and return
+# a 3 tuple of content, path, filename
+def transport_iso9660(require_iso=True):
+
# Go through mounts to see if it was already mounted
mounts = util.mounts()
for (dev, info) in mounts.items():
fstype = info['fstype']
if fstype != "iso9660" and require_iso:
continue
- if cdmatch.match(dev[5:]) is None: # take off '/dev/'
+ if not maybe_cdrom_device(dev):
continue
mp = info['mountpoint']
(fname, contents) = get_ovf_env(mp)
@@ -406,29 +436,19 @@ def transport_iso9660(require_iso=True):
else:
mtype = None
- devs = os.listdir("/dev/")
- devs.sort()
+ # generate a list of devices with mtype filesystem, filter by regex
+ devs = [dev for dev in
+ util.find_devs_with("TYPE=%s" % mtype if mtype else None)
+ if maybe_cdrom_device(dev)]
for dev in devs:
- fullp = os.path.join("/dev/", dev)
-
- if (fullp in mounts or
- not cdmatch.match(dev) or os.path.isdir(fullp)):
- continue
-
- try:
- # See if we can read anything at all...??
- util.peek_file(fullp, 512)
- except IOError:
- continue
-
try:
- (fname, contents) = util.mount_cb(fullp, get_ovf_env, mtype=mtype)
+ (fname, contents) = util.mount_cb(dev, get_ovf_env, mtype=mtype)
except util.MountFailedError:
- LOG.debug("%s not mountable as iso9660", fullp)
+ LOG.debug("%s not mountable as iso9660", dev)
continue
if contents is not False:
- return (contents, fullp, fname)
+ return (contents, dev, fname)
return (False, None, None)
diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
index 28ed0ae..959b1bd 100644
--- a/cloudinit/sources/helpers/azure.py
+++ b/cloudinit/sources/helpers/azure.py
@@ -8,6 +8,7 @@ import socket
import struct
import time
+from cloudinit.net import dhcp
from cloudinit import stages
from cloudinit import temp_utils
from contextlib import contextmanager
@@ -15,7 +16,6 @@ from xml.etree import ElementTree
from cloudinit import util
-
LOG = logging.getLogger(__name__)
@@ -239,6 +239,11 @@ class WALinuxAgentShim(object):
return socket.inet_ntoa(packed_bytes)
@staticmethod
+ def _networkd_get_value_from_leases(leases_d=None):
+ return dhcp.networkd_get_option_from_leases(
+ 'OPTION_245', leases_d=leases_d)
+
+ @staticmethod
def _get_value_from_leases_file(fallback_lease_file):
leases = []
content = util.load_file(fallback_lease_file)
@@ -287,12 +292,15 @@ class WALinuxAgentShim(object):
@staticmethod
def find_endpoint(fallback_lease_file=None):
- LOG.debug('Finding Azure endpoint...')
value = None
- # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json
- # a dhclient exit hook that calls cloud-init-dhclient-hook
- dhcp_options = WALinuxAgentShim._load_dhclient_json()
- value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options)
+ LOG.debug('Finding Azure endpoint from networkd...')
+ value = WALinuxAgentShim._networkd_get_value_from_leases()
+ if value is None:
+ # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json
+ # a dhclient exit hook that calls cloud-init-dhclient-hook
+ LOG.debug('Finding Azure endpoint from hook json...')
+ dhcp_options = WALinuxAgentShim._load_dhclient_json()
+ value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options)
if value is None:
# Fallback and check the leases file if unsuccessful
LOG.debug("Unable to find endpoint in dhclient logs. "
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 28e2662..6f88a5b 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -104,6 +104,16 @@ class TestCase(unittest2.TestCase):
super(TestCase, self).setUp()
self.reset_global_state()
+ def add_patch(self, target, attr, **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, **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
diff --git a/cloudinit/tests/test_simpletable.py b/cloudinit/tests/test_simpletable.py
new file mode 100644
index 0000000..96bc24c
--- /dev/null
+++ b/cloudinit/tests/test_simpletable.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2017 Amazon.com, Inc. or its affiliates
+#
+# Author: Andrew Jorgensen <ajorgens@xxxxxxxxxx>
+#
+# 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)
+ self.assertEqual(str(table), AUTHORIZED_KEYS_TABLE)
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index 50e3bd8..32de9c9 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -84,6 +84,9 @@ cloud_config_modules:
- apt-pipelining
- apt-configure
{% endif %}
+{% if variant in ["suse"] %}
+ - zypper-add-repo
+{% endif %}
{% if variant not in ["freebsd"] %}
- ntp
{% endif %}
diff --git a/debian/changelog b/debian/changelog
index 2080def..1e1007d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,31 @@
-cloud-init (17.1-0ubuntu2) UNRELEASED; urgency=medium
+cloud-init (17.1-13-g7fd0425-0ubuntu1) artful; urgency=medium
* debian/copyright: dep5 updates, reorganize, add Apache 2.0 license.
(LP: #1718681)
-
- -- Scott Moser <smoser@xxxxxxxxxxxx> Fri, 29 Sep 2017 08:44:15 -0400
+ * debian/control: drop dependency on python3-prettytable
+ * New upstream snapshot.
+ - systemd: remove limit on tasks created by cloud-init-final.service.
+ [Robert Schweikert] (LP: #1717969)
+ - suse: Support addition of zypper repos via cloud-config.
+ [Robert Schweikert] (LP: #1718675)
+ - tests: Combine integration configs and testcases [Joshua Powers]
+ - Azure, CloudStack: Support reading dhcp options from systemd-networkd.
+ [Dimitri John Ledkov] (LP: #1718029)
+ - packages/debian/copyright: remove mention of boto and MIT license
+ - systemd: only mention Before=apt-daily.service on debian based distros.
+ [Robert Schweikert]
+ - Add missing simpletable and simpletable tests for failed merge
+ [Chad Smith]
+ - Remove prettytable dependency, introduce simpletable [Andrew Jorgensen]
+ - debian/copyright: dep5 updates, reorganize, add Apache 2.0 license.
+ [Joshua Powers] (LP: #1718681)
+ - tests: remove dependency on shlex [Joshua Powers]
+ - AltCloud: Trust PATH for udevadm and modprobe.
+ - DataSourceOVF: use util.find_devs_with(TYPE=iso9660)
+ [Ryan Harper] (LP: #1718287)
+ - tests: remove a temp file used in bootcmd tests.
+
+ -- Ryan Harper <ryan.harper@xxxxxxxxxxxxx> Tue, 03 Oct 2017 10:59:52 -0500
cloud-init (17.1-0ubuntu1) artful; urgency=medium
diff --git a/debian/control b/debian/control
index 731821e..3f46d7b 100644
--- a/debian/control
+++ b/debian/control
@@ -19,7 +19,6 @@ Build-Depends: debhelper (>= 9),
python3-nose,
python3-oauthlib,
python3-pep8,
- python3-prettytable,
python3-pyflakes | pyflakes (<< 1.1.0-2),
python3-requests,
python3-serial,
diff --git a/packages/debian/copyright b/packages/debian/copyright
index c9c7d23..598cda1 100644
--- a/packages/debian/copyright
+++ b/packages/debian/copyright
@@ -1,33 +1,28 @@
-Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
-Name: cloud-init
-Maintainer: Scott Moser <scott.moser@xxxxxxxxxxxxx>
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: cloud-init
+Upstream-Contact: cloud-init-dev@xxxxxxxxxxxxxxxxxxx
Source: https://launchpad.net/cloud-init
-This package was debianized by Soren Hansen <soren@xxxxxxxxxx> on
-Thu, 04 Sep 2008 12:49:15 +0200 as ec2-init. It was later renamed to
-cloud-init by Scott Moser <scott.moser@xxxxxxxxxxxxx>
-
-Upstream Author: Scott Moser <smoser@xxxxxxxxxxxxx>
- Soren Hansen <soren@xxxxxxxxxxxxx>
- Chuck Short <chuck.short@xxxxxxxxxxxxx>
-
-Copyright: 2010, Canonical Ltd.
+Files: *
+Copyright: 2010, Canonical Ltd.
License: GPL-3 or Apache-2.0
+
License: GPL-3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3, as
published by the Free Software Foundation.
-
+ .
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
-
+ .
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-
+ .
The complete text of the GPL version 3 can be seen in
/usr/share/common-licenses/GPL-3.
+
License: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json
index 822d29d..72409dd 100644
--- a/packages/pkg-deps.json
+++ b/packages/pkg-deps.json
@@ -34,9 +34,6 @@
"jsonschema" : {
"3" : "python34-jsonschema"
},
- "prettytable" : {
- "3" : "python34-prettytable"
- },
"pyflakes" : {
"2" : "pyflakes",
"3" : "python34-pyflakes"
diff --git a/requirements.txt b/requirements.txt
index 61d1e90..dd10d85 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,9 +3,6 @@
# Used for untemplating any files or strings with parameters.
jinja2
-# This is used for any pretty printing of tabular data.
-PrettyTable
-
# This one is currently only used by the MAAS datasource. If that
# datasource is removed, this is no longer needed
oauthlib
diff --git a/systemd/cloud-final.service.tmpl b/systemd/cloud-final.service.tmpl
index fc01b89..8207b18 100644
--- a/systemd/cloud-final.service.tmpl
+++ b/systemd/cloud-final.service.tmpl
@@ -4,9 +4,10 @@ Description=Execute cloud user/final scripts
After=network-online.target cloud-config.service rc-local.service
{% if variant in ["ubuntu", "unknown", "debian"] %}
After=multi-user.target
+Before=apt-daily.service
{% endif %}
Wants=network-online.target cloud-config.service
-Before=apt-daily.service
+
[Service]
Type=oneshot
@@ -14,6 +15,7 @@ ExecStart=/usr/bin/cloud-init modules --mode=final
RemainAfterExit=yes
TimeoutSec=0
KillMode=process
+TasksMax=infinity
# Output needs to appear in instance console output
StandardOutput=journal+console
diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py
index 07148c1..98c1d6c 100644
--- a/tests/cloud_tests/__init__.py
+++ b/tests/cloud_tests/__init__.py
@@ -7,7 +7,7 @@ import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')
-TEST_CONF_DIR = os.path.join(BASE_DIR, 'configs')
+TEST_CONF_DIR = os.path.join(BASE_DIR, 'testcases')
TREE_BASE = os.sep.join(BASE_DIR.split(os.sep)[:-2])
diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
index 7abfe73..8a0e531 100644
--- a/tests/cloud_tests/instances/nocloudkvm.py
+++ b/tests/cloud_tests/instances/nocloudkvm.py
@@ -4,7 +4,6 @@
import os
import paramiko
-import shlex
import socket
import subprocess
import time
@@ -83,10 +82,10 @@ class NoCloudKVMInstance(base.Instance):
def mount_image_callback(self, cmd):
"""Run mount-image-callback."""
- mic = ('sudo mount-image-callback --system-mounts --system-resolvconf '
- '%s -- chroot _MOUNTPOINT_ ' % self.name)
-
- out, err = c_util.subp(shlex.split(mic) + cmd)
+ out, err = c_util.subp(['sudo', 'mount-image-callback',
+ '--system-mounts', '--system-resolvconf',
+ self.name, '--', 'chroot',
+ '_MOUNTPOINT_'] + cmd)
return out, err
@@ -122,11 +121,11 @@ class NoCloudKVMInstance(base.Instance):
if self.pid:
super(NoCloudKVMInstance, self).push_file()
else:
- cmd = ("sudo mount-image-callback --system-mounts "
- "--system-resolvconf %s -- chroot _MOUNTPOINT_ "
- "/bin/sh -c 'cat - > %s'" % (self.name, remote_path))
local_file = open(local_path)
- p = subprocess.Popen(shlex.split(cmd),
+ p = subprocess.Popen(['sudo', 'mount-image-callback',
+ '--system-mounts', '--system-resolvconf',
+ self.name, '--', 'chroot', '_MOUNTPOINT_',
+ '/bin/sh', '-c', 'cat - > %s' % remote_path],
stdin=local_file,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
@@ -186,12 +185,14 @@ class NoCloudKVMInstance(base.Instance):
self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
self.ssh_port = self.get_free_port()
- cmd = ('./tools/xkvm --disk %s,cache=unsafe --disk %s,cache=unsafe '
- '--netdev user,hostfwd=tcp::%s-:22 '
- '-- -pidfile %s -vnc none -m 2G -smp 2'
- % (self.name, seed, self.ssh_port, self.pid_file))
-
- subprocess.Popen(shlex.split(cmd), close_fds=True,
+ subprocess.Popen(['./tools/xkvm',
+ '--disk', '%s,cache=unsafe' % self.name,
+ '--disk', '%s,cache=unsafe' % seed,
+ '--netdev',
+ 'user,hostfwd=tcp::%s-:22' % self.ssh_port,
+ '--', '-pidfile', self.pid_file, '-vnc', 'none',
+ '-m', '2G', '-smp', '2'],
+ close_fds=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
diff --git a/tests/cloud_tests/configs/bugs/README.md b/tests/cloud_tests/testcases/bugs/README.md
index 09ce076..09ce076 100644
--- a/tests/cloud_tests/configs/bugs/README.md
+++ b/tests/cloud_tests/testcases/bugs/README.md
diff --git a/tests/cloud_tests/configs/bugs/lp1511485.yaml b/tests/cloud_tests/testcases/bugs/lp1511485.yaml
index ebf9763..ebf9763 100644
--- a/tests/cloud_tests/configs/bugs/lp1511485.yaml
+++ b/tests/cloud_tests/testcases/bugs/lp1511485.yaml
diff --git a/tests/cloud_tests/configs/bugs/lp1611074.yaml b/tests/cloud_tests/testcases/bugs/lp1611074.yaml
index 960679d..960679d 100644
--- a/tests/cloud_tests/configs/bugs/lp1611074.yaml
+++ b/tests/cloud_tests/testcases/bugs/lp1611074.yaml
diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/testcases/bugs/lp1628337.yaml
index e39b3cd..e39b3cd 100644
--- a/tests/cloud_tests/configs/bugs/lp1628337.yaml
+++ b/tests/cloud_tests/testcases/bugs/lp1628337.yaml
diff --git a/tests/cloud_tests/configs/examples/README.md b/tests/cloud_tests/testcases/examples/README.md
index 110a223..110a223 100644
--- a/tests/cloud_tests/configs/examples/README.md
+++ b/tests/cloud_tests/testcases/examples/README.md
diff --git a/tests/cloud_tests/configs/examples/TODO.md b/tests/cloud_tests/testcases/examples/TODO.md
index 8db0e98..8db0e98 100644
--- a/tests/cloud_tests/configs/examples/TODO.md
+++ b/tests/cloud_tests/testcases/examples/TODO.md
diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/testcases/examples/add_apt_repositories.yaml
index 4b8575f..4b8575f 100644
--- a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
+++ b/tests/cloud_tests/testcases/examples/add_apt_repositories.yaml
diff --git a/tests/cloud_tests/configs/examples/alter_completion_message.yaml b/tests/cloud_tests/testcases/examples/alter_completion_message.yaml
index 9e154f8..9e154f8 100644
--- a/tests/cloud_tests/configs/examples/alter_completion_message.yaml
+++ b/tests/cloud_tests/testcases/examples/alter_completion_message.yaml
diff --git a/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.yaml
index ad32b08..ad32b08 100644
--- a/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml
+++ b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.yaml
diff --git a/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.yaml
index f3eaf3c..f3eaf3c 100644
--- a/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml
+++ b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.yaml
diff --git a/tests/cloud_tests/configs/examples/including_user_groups.yaml b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
index 0aa7ad2..0aa7ad2 100644
--- a/tests/cloud_tests/configs/examples/including_user_groups.yaml
+++ b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
diff --git a/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.yaml
index d398022..d398022 100644
--- a/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml
+++ b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.yaml
diff --git a/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.yaml
index 0bec305..0bec305 100644
--- a/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml
+++ b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.yaml
diff --git a/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml b/tests/cloud_tests/testcases/examples/run_apt_upgrade.yaml
index 2b7eae4..2b7eae4 100644
--- a/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml
+++ b/tests/cloud_tests/testcases/examples/run_apt_upgrade.yaml
diff --git a/tests/cloud_tests/configs/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml
index b0e311b..b0e311b 100644
--- a/tests/cloud_tests/configs/examples/run_commands.yaml
+++ b/tests/cloud_tests/testcases/examples/run_commands.yaml
diff --git a/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml b/tests/cloud_tests/testcases/examples/run_commands_first_boot.yaml
index 7bd803d..7bd803d 100644
--- a/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml
+++ b/tests/cloud_tests/testcases/examples/run_commands_first_boot.yaml
diff --git a/tests/cloud_tests/configs/examples/setup_run_puppet.yaml b/tests/cloud_tests/testcases/examples/setup_run_puppet.yaml
index e366c04..e366c04 100644
--- a/tests/cloud_tests/configs/examples/setup_run_puppet.yaml
+++ b/tests/cloud_tests/testcases/examples/setup_run_puppet.yaml
diff --git a/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.yaml
index 6f78f99..6f78f99 100644
--- a/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml
+++ b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.yaml
diff --git a/tests/cloud_tests/configs/main/README.md b/tests/cloud_tests/testcases/main/README.md
index 6034606..6034606 100644
--- a/tests/cloud_tests/configs/main/README.md
+++ b/tests/cloud_tests/testcases/main/README.md
diff --git a/tests/cloud_tests/configs/main/command_output_simple.yaml b/tests/cloud_tests/testcases/main/command_output_simple.yaml
index 08ca894..08ca894 100644
--- a/tests/cloud_tests/configs/main/command_output_simple.yaml
+++ b/tests/cloud_tests/testcases/main/command_output_simple.yaml
diff --git a/tests/cloud_tests/configs/modules/README.md b/tests/cloud_tests/testcases/modules/README.md
index d66101f..d66101f 100644
--- a/tests/cloud_tests/configs/modules/README.md
+++ b/tests/cloud_tests/testcases/modules/README.md
diff --git a/tests/cloud_tests/configs/modules/TODO.md b/tests/cloud_tests/testcases/modules/TODO.md
index 0b933b3..0b933b3 100644
--- a/tests/cloud_tests/configs/modules/TODO.md
+++ b/tests/cloud_tests/testcases/modules/TODO.md
diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/testcases/modules/apt_configure_conf.yaml
index de45300..de45300 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_conf.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.yaml
index 9880067..9880067 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/testcases/modules/apt_configure_primary.yaml
index 41bcf2f..41bcf2f 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_primary.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/testcases/modules/apt_configure_proxy.yaml
index be6c6f8..be6c6f8 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_proxy.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/testcases/modules/apt_configure_security.yaml
index 83dd51d..83dd51d 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_security.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_security.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_key.yaml
index bde9398..bde9398 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_key.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.yaml
index 2508813..2508813 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_list.yaml
index 143cb08..143cb08 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_list.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
index 9efdae5..9efdae5 100644
--- a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
index bd9b5d0..bd9b5d0 100644
--- a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
index cbed3ba..cbed3ba 100644
--- a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
diff --git a/tests/cloud_tests/configs/modules/bootcmd.yaml b/tests/cloud_tests/testcases/modules/bootcmd.yaml
index 3a73994..3a73994 100644
--- a/tests/cloud_tests/configs/modules/bootcmd.yaml
+++ b/tests/cloud_tests/testcases/modules/bootcmd.yaml
diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/testcases/modules/byobu.yaml
index a9aa1f3..a9aa1f3 100644
--- a/tests/cloud_tests/configs/modules/byobu.yaml
+++ b/tests/cloud_tests/testcases/modules/byobu.yaml
diff --git a/tests/cloud_tests/configs/modules/ca_certs.yaml b/tests/cloud_tests/testcases/modules/ca_certs.yaml
index d939f43..d939f43 100644
--- a/tests/cloud_tests/configs/modules/ca_certs.yaml
+++ b/tests/cloud_tests/testcases/modules/ca_certs.yaml
diff --git a/tests/cloud_tests/configs/modules/debug_disable.yaml b/tests/cloud_tests/testcases/modules/debug_disable.yaml
index 63218b1..63218b1 100644
--- a/tests/cloud_tests/configs/modules/debug_disable.yaml
+++ b/tests/cloud_tests/testcases/modules/debug_disable.yaml
diff --git a/tests/cloud_tests/configs/modules/debug_enable.yaml b/tests/cloud_tests/testcases/modules/debug_enable.yaml
index d44147d..d44147d 100644
--- a/tests/cloud_tests/configs/modules/debug_enable.yaml
+++ b/tests/cloud_tests/testcases/modules/debug_enable.yaml
diff --git a/tests/cloud_tests/configs/modules/final_message.yaml b/tests/cloud_tests/testcases/modules/final_message.yaml
index c9ed611..c9ed611 100644
--- a/tests/cloud_tests/configs/modules/final_message.yaml
+++ b/tests/cloud_tests/testcases/modules/final_message.yaml
diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/testcases/modules/keys_to_console.yaml
index 5d86e73..5d86e73 100644
--- a/tests/cloud_tests/configs/modules/keys_to_console.yaml
+++ b/tests/cloud_tests/testcases/modules/keys_to_console.yaml
diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/testcases/modules/landscape.yaml
index ed2c37c..ed2c37c 100644
--- a/tests/cloud_tests/configs/modules/landscape.yaml
+++ b/tests/cloud_tests/testcases/modules/landscape.yaml
diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/testcases/modules/locale.yaml
index e01518a..e01518a 100644
--- a/tests/cloud_tests/configs/modules/locale.yaml
+++ b/tests/cloud_tests/testcases/modules/locale.yaml
diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/testcases/modules/lxd_bridge.yaml
index e6b7e76..e6b7e76 100644
--- a/tests/cloud_tests/configs/modules/lxd_bridge.yaml
+++ b/tests/cloud_tests/testcases/modules/lxd_bridge.yaml
diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/testcases/modules/lxd_dir.yaml
index f93a3fa..f93a3fa 100644
--- a/tests/cloud_tests/configs/modules/lxd_dir.yaml
+++ b/tests/cloud_tests/testcases/modules/lxd_dir.yaml
diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/testcases/modules/ntp.yaml
index fbef431..fbef431 100644
--- a/tests/cloud_tests/configs/modules/ntp.yaml
+++ b/tests/cloud_tests/testcases/modules/ntp.yaml
diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/testcases/modules/ntp_pools.yaml
index 3a93faa..3a93faa 100644
--- a/tests/cloud_tests/configs/modules/ntp_pools.yaml
+++ b/tests/cloud_tests/testcases/modules/ntp_pools.yaml
diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/testcases/modules/ntp_servers.yaml
index d59d45a..d59d45a 100644
--- a/tests/cloud_tests/configs/modules/ntp_servers.yaml
+++ b/tests/cloud_tests/testcases/modules/ntp_servers.yaml
diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/testcases/modules/package_update_upgrade_install.yaml
index 71d24b8..71d24b8 100644
--- a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
+++ b/tests/cloud_tests/testcases/modules/package_update_upgrade_install.yaml
diff --git a/tests/cloud_tests/configs/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml
index 04e5a05..04e5a05 100644
--- a/tests/cloud_tests/configs/modules/runcmd.yaml
+++ b/tests/cloud_tests/testcases/modules/runcmd.yaml
diff --git a/tests/cloud_tests/configs/modules/salt_minion.yaml b/tests/cloud_tests/testcases/modules/salt_minion.yaml
index f20d24f..f20d24f 100644
--- a/tests/cloud_tests/configs/modules/salt_minion.yaml
+++ b/tests/cloud_tests/testcases/modules/salt_minion.yaml
diff --git a/tests/cloud_tests/configs/modules/seed_random_command.yaml b/tests/cloud_tests/testcases/modules/seed_random_command.yaml
index 6a9157e..6a9157e 100644
--- a/tests/cloud_tests/configs/modules/seed_random_command.yaml
+++ b/tests/cloud_tests/testcases/modules/seed_random_command.yaml
diff --git a/tests/cloud_tests/configs/modules/seed_random_data.yaml b/tests/cloud_tests/testcases/modules/seed_random_data.yaml
index a9b2c88..a9b2c88 100644
--- a/tests/cloud_tests/configs/modules/seed_random_data.yaml
+++ b/tests/cloud_tests/testcases/modules/seed_random_data.yaml
diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml
index c96344c..c96344c 100644
--- a/tests/cloud_tests/configs/modules/set_hostname.yaml
+++ b/tests/cloud_tests/testcases/modules/set_hostname.yaml
diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
index daf7593..daf7593 100644
--- a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
+++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/testcases/modules/set_password.yaml
index 04d7c58..04d7c58 100644
--- a/tests/cloud_tests/configs/modules/set_password.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password.yaml
diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
index 789604b..789604b 100644
--- a/tests/cloud_tests/configs/modules/set_password_expire.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
diff --git a/tests/cloud_tests/configs/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml
index a2a89c9..a2a89c9 100644
--- a/tests/cloud_tests/configs/modules/set_password_list.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password_list.yaml
diff --git a/tests/cloud_tests/configs/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
index c2a0f63..c2a0f63 100644
--- a/tests/cloud_tests/configs/modules/set_password_list_string.yaml
+++ b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/testcases/modules/snappy.yaml
index 43f9329..43f9329 100644
--- a/tests/cloud_tests/configs/modules/snappy.yaml
+++ b/tests/cloud_tests/testcases/modules/snappy.yaml
diff --git a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
index 746653e..746653e 100644
--- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
diff --git a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.yaml
index 9f5dc34..9f5dc34 100644
--- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.yaml
diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/testcases/modules/ssh_import_id.yaml
index b62d3f6..b62d3f6 100644
--- a/tests/cloud_tests/configs/modules/ssh_import_id.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_import_id.yaml
diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
index 659fd93..659fd93 100644
--- a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
index 5ceb362..5ceb362 100644
--- a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
+++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/testcases/modules/timezone.yaml
index 5112aa9..5112aa9 100644
--- a/tests/cloud_tests/configs/modules/timezone.yaml
+++ b/tests/cloud_tests/testcases/modules/timezone.yaml
diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/testcases/modules/user_groups.yaml
index 71cc9da..71cc9da 100644
--- a/tests/cloud_tests/configs/modules/user_groups.yaml
+++ b/tests/cloud_tests/testcases/modules/user_groups.yaml
diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/testcases/modules/write_files.yaml
index ce936b7..ce936b7 100644
--- a/tests/cloud_tests/configs/modules/write_files.yaml
+++ b/tests/cloud_tests/testcases/modules/write_files.yaml
diff --git a/tests/unittests/test_datasource/test_altcloud.py b/tests/unittests/test_datasource/test_altcloud.py
index 3b274d9..a4dfb54 100644
--- a/tests/unittests/test_datasource/test_altcloud.py
+++ b/tests/unittests/test_datasource/test_altcloud.py
@@ -280,8 +280,8 @@ class TestUserDataRhevm(TestCase):
pass
dsac.CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info'
- dsac.CMD_PROBE_FLOPPY = ['/sbin/modprobe', 'floppy']
- dsac.CMD_UDEVADM_SETTLE = ['/sbin/udevadm', 'settle',
+ dsac.CMD_PROBE_FLOPPY = ['modprobe', 'floppy']
+ dsac.CMD_UDEVADM_SETTLE = ['udevadm', 'settle',
'--quiet', '--timeout=5']
def test_mount_cb_fails(self):
diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
index 44b99ec..b42b073 100644
--- a/tests/unittests/test_datasource/test_azure_helper.py
+++ b/tests/unittests/test_datasource/test_azure_helper.py
@@ -1,10 +1,12 @@
# This file is part of cloud-init. See LICENSE file for license information.
import os
+from textwrap import dedent
from cloudinit.sources.helpers import azure as azure_helper
-from cloudinit.tests.helpers import ExitStack, mock, TestCase
+from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
+from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim
GOAL_STATE_TEMPLATE = """\
<?xml version="1.0" encoding="utf-8"?>
@@ -45,7 +47,7 @@ GOAL_STATE_TEMPLATE = """\
"""
-class TestFindEndpoint(TestCase):
+class TestFindEndpoint(CiTestCase):
def setUp(self):
super(TestFindEndpoint, self).setUp()
@@ -56,18 +58,19 @@ class TestFindEndpoint(TestCase):
mock.patch.object(azure_helper.util, 'load_file'))
self.dhcp_options = patches.enter_context(
- mock.patch.object(azure_helper.WALinuxAgentShim,
- '_load_dhclient_json'))
+ mock.patch.object(wa_shim, '_load_dhclient_json'))
+
+ self.networkd_leases = patches.enter_context(
+ mock.patch.object(wa_shim, '_networkd_get_value_from_leases'))
+ self.networkd_leases.return_value = None
def test_missing_file(self):
- self.assertRaises(ValueError,
- azure_helper.WALinuxAgentShim.find_endpoint)
+ self.assertRaises(ValueError, wa_shim.find_endpoint)
def test_missing_special_azure_line(self):
self.load_file.return_value = ''
self.dhcp_options.return_value = {'eth0': {'key': 'value'}}
- self.assertRaises(ValueError,
- azure_helper.WALinuxAgentShim.find_endpoint)
+ self.assertRaises(ValueError, wa_shim.find_endpoint)
@staticmethod
def _build_lease_content(encoded_address):
@@ -80,8 +83,7 @@ class TestFindEndpoint(TestCase):
def test_from_dhcp_client(self):
self.dhcp_options.return_value = {"eth0": {"unknown_245": "5:4:3:2"}}
- self.assertEqual('5.4.3.2',
- azure_helper.WALinuxAgentShim.find_endpoint(None))
+ self.assertEqual('5.4.3.2', wa_shim.find_endpoint(None))
def test_latest_lease_used(self):
encoded_addresses = ['5:4:3:2', '4:3:2:1']
@@ -89,53 +91,38 @@ class TestFindEndpoint(TestCase):
for encoded_address in encoded_addresses])
self.load_file.return_value = file_content
self.assertEqual(encoded_addresses[-1].replace(':', '.'),
- azure_helper.WALinuxAgentShim.find_endpoint("foobar"))
+ wa_shim.find_endpoint("foobar"))
-class TestExtractIpAddressFromLeaseValue(TestCase):
+class TestExtractIpAddressFromLeaseValue(CiTestCase):
def test_hex_string(self):
ip_address, encoded_address = '98.76.54.32', '62:4c:36:20'
self.assertEqual(
- ip_address,
- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
- encoded_address
- ))
+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
def test_hex_string_with_single_character_part(self):
ip_address, encoded_address = '4.3.2.1', '4:3:2:1'
self.assertEqual(
- ip_address,
- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
- encoded_address
- ))
+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
def test_packed_string(self):
ip_address, encoded_address = '98.76.54.32', 'bL6 '
self.assertEqual(
- ip_address,
- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
- encoded_address
- ))
+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
def test_packed_string_with_escaped_quote(self):
ip_address, encoded_address = '100.72.34.108', 'dH\\"l'
self.assertEqual(
- ip_address,
- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
- encoded_address
- ))
+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
def test_packed_string_containing_a_colon(self):
ip_address, encoded_address = '100.72.58.108', 'dH:l'
self.assertEqual(
- ip_address,
- azure_helper.WALinuxAgentShim.get_ip_from_lease_value(
- encoded_address
- ))
+ ip_address, wa_shim.get_ip_from_lease_value(encoded_address))
-class TestGoalStateParsing(TestCase):
+class TestGoalStateParsing(CiTestCase):
default_parameters = {
'incarnation': 1,
@@ -195,7 +182,7 @@ class TestGoalStateParsing(TestCase):
self.assertIsNone(certificates_xml)
-class TestAzureEndpointHttpClient(TestCase):
+class TestAzureEndpointHttpClient(CiTestCase):
regular_headers = {
'x-ms-agent-name': 'WALinuxAgent',
@@ -258,7 +245,7 @@ class TestAzureEndpointHttpClient(TestCase):
self.read_file_or_url.call_args)
-class TestOpenSSLManager(TestCase):
+class TestOpenSSLManager(CiTestCase):
def setUp(self):
super(TestOpenSSLManager, self).setUp()
@@ -300,7 +287,7 @@ class TestOpenSSLManager(TestCase):
self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)
-class TestWALinuxAgentShim(TestCase):
+class TestWALinuxAgentShim(CiTestCase):
def setUp(self):
super(TestWALinuxAgentShim, self).setUp()
@@ -310,8 +297,7 @@ class TestWALinuxAgentShim(TestCase):
self.AzureEndpointHttpClient = patches.enter_context(
mock.patch.object(azure_helper, 'AzureEndpointHttpClient'))
self.find_endpoint = patches.enter_context(
- mock.patch.object(
- azure_helper.WALinuxAgentShim, 'find_endpoint'))
+ mock.patch.object(wa_shim, 'find_endpoint'))
self.GoalState = patches.enter_context(
mock.patch.object(azure_helper, 'GoalState'))
self.OpenSSLManager = patches.enter_context(
@@ -320,7 +306,7 @@ class TestWALinuxAgentShim(TestCase):
mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock()))
def test_http_client_uses_certificate(self):
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
shim.register_with_azure_and_fetch_data()
self.assertEqual(
[mock.call(self.OpenSSLManager.return_value.certificate)],
@@ -328,7 +314,7 @@ class TestWALinuxAgentShim(TestCase):
def test_correct_url_used_for_goalstate(self):
self.find_endpoint.return_value = 'test_endpoint'
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
shim.register_with_azure_and_fetch_data()
get = self.AzureEndpointHttpClient.return_value.get
self.assertEqual(
@@ -340,7 +326,7 @@ class TestWALinuxAgentShim(TestCase):
self.GoalState.call_args_list)
def test_certificates_used_to_determine_public_keys(self):
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
data = shim.register_with_azure_and_fetch_data()
self.assertEqual(
[mock.call(self.GoalState.return_value.certificates_xml)],
@@ -351,13 +337,13 @@ class TestWALinuxAgentShim(TestCase):
def test_absent_certificates_produces_empty_public_keys(self):
self.GoalState.return_value.certificates_xml = None
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
data = shim.register_with_azure_and_fetch_data()
self.assertEqual([], data['public-keys'])
def test_correct_url_used_for_report_ready(self):
self.find_endpoint.return_value = 'test_endpoint'
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
shim.register_with_azure_and_fetch_data()
expected_url = 'http://test_endpoint/machine?comp=health'
self.assertEqual(
@@ -368,7 +354,7 @@ class TestWALinuxAgentShim(TestCase):
self.GoalState.return_value.incarnation = 'TestIncarnation'
self.GoalState.return_value.container_id = 'TestContainerId'
self.GoalState.return_value.instance_id = 'TestInstanceId'
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
shim.register_with_azure_and_fetch_data()
posted_document = (
self.AzureEndpointHttpClient.return_value.post.call_args[1]['data']
@@ -378,11 +364,11 @@ class TestWALinuxAgentShim(TestCase):
self.assertIn('TestInstanceId', posted_document)
def test_clean_up_can_be_called_at_any_time(self):
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
shim.clean_up()
def test_clean_up_will_clean_up_openssl_manager_if_instantiated(self):
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
shim.register_with_azure_and_fetch_data()
shim.clean_up()
self.assertEqual(
@@ -393,12 +379,12 @@ class TestWALinuxAgentShim(TestCase):
pass
self.AzureEndpointHttpClient.return_value.get.side_effect = (
SentinelException)
- shim = azure_helper.WALinuxAgentShim()
+ shim = wa_shim()
self.assertRaises(SentinelException,
shim.register_with_azure_and_fetch_data)
-class TestGetMetadataFromFabric(TestCase):
+class TestGetMetadataFromFabric(CiTestCase):
@mock.patch.object(azure_helper, 'WALinuxAgentShim')
def test_data_from_shim_returned(self, shim):
@@ -422,4 +408,65 @@ class TestGetMetadataFromFabric(TestCase):
azure_helper.get_metadata_from_fabric)
self.assertEqual(1, shim.return_value.clean_up.call_count)
+
+class TestExtractIpAddressFromNetworkd(CiTestCase):
+
+ 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
+ """)
+
+ def setUp(self):
+ super(TestExtractIpAddressFromNetworkd, self).setUp()
+ self.lease_d = self.tmp_dir()
+
+ def test_no_valid_leases_is_none(self):
+ """No valid leases should return None."""
+ self.assertIsNone(
+ wa_shim._networkd_get_value_from_leases(self.lease_d))
+
+ def test_option_245_is_found_in_single(self):
+ """A single valid lease with 245 option should return it."""
+ populate_dir(self.lease_d, {'9': self.azure_lease})
+ self.assertEqual(
+ '624c3620', wa_shim._networkd_get_value_from_leases(self.lease_d))
+
+ def test_option_245_not_found_returns_None(self):
+ """A valid lease, but no option 245 should return None."""
+ populate_dir(
+ self.lease_d,
+ {'9': self.azure_lease.replace("OPTION_245", "OPTION_999")})
+ self.assertIsNone(
+ wa_shim._networkd_get_value_from_leases(self.lease_d))
+
+ def test_multiple_returns_first(self):
+ """Somewhat arbitrarily return the first address when multiple.
+
+ Most important at the moment is that this is consistent behavior
+ rather than changing randomly as in order of a dictionary."""
+ myval = "624c3601"
+ populate_dir(
+ self.lease_d,
+ {'9': self.azure_lease,
+ '2': self.azure_lease.replace("624c3620", myval)})
+ self.assertEqual(
+ myval, wa_shim._networkd_get_value_from_leases(self.lease_d))
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
index 8e98e1b..96144b6 100644
--- a/tests/unittests/test_datasource/test_cloudstack.py
+++ b/tests/unittests/test_datasource/test_cloudstack.py
@@ -23,13 +23,16 @@ class TestCloudStackPasswordFetching(CiTestCase):
default_gw = "192.201.20.0"
get_latest_lease = mock.MagicMock(return_value=None)
self.patches.enter_context(mock.patch(
- 'cloudinit.sources.DataSourceCloudStack.get_latest_lease',
- get_latest_lease))
+ mod_name + '.get_latest_lease', get_latest_lease))
get_default_gw = mock.MagicMock(return_value=default_gw)
self.patches.enter_context(mock.patch(
- 'cloudinit.sources.DataSourceCloudStack.get_default_gateway',
- get_default_gw))
+ mod_name + '.get_default_gateway', get_default_gw))
+
+ get_networkd_server_address = mock.MagicMock(return_value=None)
+ self.patches.enter_context(mock.patch(
+ mod_name + '.dhcp.networkd_get_option_from_leases',
+ get_networkd_server_address))
def _set_password_server_response(self, response_string):
subp = mock.MagicMock(return_value=(response_string, ''))
diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
index 9dbf4dd..700da86 100644
--- a/tests/unittests/test_datasource/test_ovf.py
+++ b/tests/unittests/test_datasource/test_ovf.py
@@ -5,6 +5,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import base64
+from collections import OrderedDict
from cloudinit.tests import helpers as test_helpers
@@ -70,4 +71,167 @@ class TestReadOvfEnv(test_helpers.TestCase):
self.assertEqual({'password': "passw0rd"}, cfg)
self.assertIsNone(ud)
+
+class TestTransportIso9660(test_helpers.CiTestCase):
+
+ def setUp(self):
+ super(TestTransportIso9660, self).setUp()
+ self.add_patch('cloudinit.util.find_devs_with',
+ 'm_find_devs_with')
+ self.add_patch('cloudinit.util.mounts', 'm_mounts')
+ self.add_patch('cloudinit.util.mount_cb', 'm_mount_cb')
+ self.add_patch('cloudinit.sources.DataSourceOVF.get_ovf_env',
+ 'm_get_ovf_env')
+ self.m_get_ovf_env.return_value = ('myfile', 'mycontent')
+
+ def test_find_already_mounted(self):
+ """Check we call get_ovf_env from on matching mounted devices"""
+ mounts = {
+ '/dev/sr9': {
+ 'fstype': 'iso9660',
+ 'mountpoint': 'wark/media/sr9',
+ 'opts': 'ro',
+ }
+ }
+ self.m_mounts.return_value = mounts
+
+ (contents, fullp, fname) = dsovf.transport_iso9660()
+ self.assertEqual("mycontent", contents)
+ self.assertEqual("/dev/sr9", fullp)
+ self.assertEqual("myfile", fname)
+
+ def test_find_already_mounted_skips_non_iso9660(self):
+ """Check we call get_ovf_env ignoring non iso9660"""
+ mounts = {
+ '/dev/xvdb': {
+ 'fstype': 'vfat',
+ 'mountpoint': 'wark/foobar',
+ 'opts': 'defaults,noatime',
+ },
+ '/dev/xvdc': {
+ 'fstype': 'iso9660',
+ 'mountpoint': 'wark/media/sr9',
+ 'opts': 'ro',
+ }
+ }
+ # We use an OrderedDict here to ensure we check xvdb before xvdc
+ # as we're not mocking the regex matching, however, if we place
+ # an entry in the results then we can be reasonably sure that
+ # we're skipping an entry which fails to match.
+ self.m_mounts.return_value = (
+ OrderedDict(sorted(mounts.items(), key=lambda t: t[0])))
+
+ (contents, fullp, fname) = dsovf.transport_iso9660()
+ self.assertEqual("mycontent", contents)
+ self.assertEqual("/dev/xvdc", fullp)
+ self.assertEqual("myfile", fname)
+
+ def test_find_already_mounted_matches_kname(self):
+ """Check we dont regex match on basename of the device"""
+ mounts = {
+ '/dev/foo/bar/xvdc': {
+ 'fstype': 'iso9660',
+ 'mountpoint': 'wark/media/sr9',
+ 'opts': 'ro',
+ }
+ }
+ # we're skipping an entry which fails to match.
+ self.m_mounts.return_value = mounts
+
+ (contents, fullp, fname) = dsovf.transport_iso9660()
+ self.assertEqual(False, contents)
+ self.assertIsNone(fullp)
+ self.assertIsNone(fname)
+
+ def test_mount_cb_called_on_blkdevs_with_iso9660(self):
+ """Check we call mount_cb on blockdevs with iso9660 only"""
+ self.m_mounts.return_value = {}
+ self.m_find_devs_with.return_value = ['/dev/sr0']
+ self.m_mount_cb.return_value = ("myfile", "mycontent")
+
+ (contents, fullp, fname) = dsovf.transport_iso9660()
+
+ self.m_mount_cb.assert_called_with(
+ "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660")
+ self.assertEqual("mycontent", contents)
+ self.assertEqual("/dev/sr0", fullp)
+ self.assertEqual("myfile", fname)
+
+ def test_mount_cb_called_on_blkdevs_with_iso9660_check_regex(self):
+ """Check we call mount_cb on blockdevs with iso9660 and match regex"""
+ self.m_mounts.return_value = {}
+ self.m_find_devs_with.return_value = [
+ '/dev/abc', '/dev/my-cdrom', '/dev/sr0']
+ self.m_mount_cb.return_value = ("myfile", "mycontent")
+
+ (contents, fullp, fname) = dsovf.transport_iso9660()
+
+ self.m_mount_cb.assert_called_with(
+ "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660")
+ self.assertEqual("mycontent", contents)
+ self.assertEqual("/dev/sr0", fullp)
+ self.assertEqual("myfile", fname)
+
+ def test_mount_cb_not_called_no_matches(self):
+ """Check we don't call mount_cb if nothing matches"""
+ self.m_mounts.return_value = {}
+ self.m_find_devs_with.return_value = ['/dev/vg/myovf']
+
+ (contents, fullp, fname) = dsovf.transport_iso9660()
+
+ self.assertEqual(0, self.m_mount_cb.call_count)
+ self.assertEqual(False, contents)
+ self.assertIsNone(fullp)
+ self.assertIsNone(fname)
+
+ def test_mount_cb_called_require_iso_false(self):
+ """Check we call mount_cb on blockdevs with require_iso=False"""
+ self.m_mounts.return_value = {}
+ self.m_find_devs_with.return_value = ['/dev/xvdz']
+ self.m_mount_cb.return_value = ("myfile", "mycontent")
+
+ (contents, fullp, fname) = dsovf.transport_iso9660(require_iso=False)
+
+ self.m_mount_cb.assert_called_with(
+ "/dev/xvdz", dsovf.get_ovf_env, mtype=None)
+ self.assertEqual("mycontent", contents)
+ self.assertEqual("/dev/xvdz", fullp)
+ self.assertEqual("myfile", fname)
+
+ def test_maybe_cdrom_device_none(self):
+ """Test maybe_cdrom_device returns False for none/empty input"""
+ self.assertFalse(dsovf.maybe_cdrom_device(None))
+ self.assertFalse(dsovf.maybe_cdrom_device(''))
+
+ def test_maybe_cdrom_device_non_string_exception(self):
+ """Test maybe_cdrom_device raises ValueError on non-string types"""
+ with self.assertRaises(ValueError):
+ dsovf.maybe_cdrom_device({'a': 'eleven'})
+
+ def test_maybe_cdrom_device_false_on_multi_dir_paths(self):
+ """Test maybe_cdrom_device is false on /dev[/.*]/* paths"""
+ self.assertFalse(dsovf.maybe_cdrom_device('/dev/foo/sr0'))
+ self.assertFalse(dsovf.maybe_cdrom_device('foo/sr0'))
+ self.assertFalse(dsovf.maybe_cdrom_device('../foo/sr0'))
+ self.assertFalse(dsovf.maybe_cdrom_device('../foo/sr0'))
+
+ def test_maybe_cdrom_device_true_on_hd_partitions(self):
+ """Test maybe_cdrom_device is false on /dev/hd[a-z][0-9]+ paths"""
+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/hda1'))
+ self.assertTrue(dsovf.maybe_cdrom_device('hdz9'))
+
+ def test_maybe_cdrom_device_true_on_valid_relative_paths(self):
+ """Test maybe_cdrom_device normalizes paths"""
+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/wark/../sr9'))
+ self.assertTrue(dsovf.maybe_cdrom_device('///sr0'))
+ self.assertTrue(dsovf.maybe_cdrom_device('/sr0'))
+ self.assertTrue(dsovf.maybe_cdrom_device('//dev//hda'))
+
+ def test_maybe_cdrom_device_true_on_xvd_partitions(self):
+ """Test maybe_cdrom_device returns true on xvd*"""
+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/xvda'))
+ self.assertTrue(dsovf.maybe_cdrom_device('/dev/xvda1'))
+ self.assertTrue(dsovf.maybe_cdrom_device('xvdza1'))
+
+#
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_bootcmd.py b/tests/unittests/test_handler/test_handler_bootcmd.py
index 580017e..dbf43e0 100644
--- a/tests/unittests/test_handler/test_handler_bootcmd.py
+++ b/tests/unittests/test_handler/test_handler_bootcmd.py
@@ -29,6 +29,7 @@ class FakeExtendedTempFile(object):
def __exit__(self, exc_type, exc_value, traceback):
self.handle.close()
+ util.del_file(self.handle.name)
class TestBootcmd(CiTestCase):
diff --git a/tests/unittests/test_handler/test_handler_zypper_add_repo.py b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
new file mode 100644
index 0000000..315c2a5
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
@@ -0,0 +1,237 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import glob
+import os
+
+from cloudinit.config import cc_zypper_add_repo
+from cloudinit import util
+
+from cloudinit.tests import helpers
+from cloudinit.tests.helpers import mock
+
+try:
+ from configparser import ConfigParser
+except ImportError:
+ from ConfigParser import ConfigParser
+import logging
+from six import StringIO
+
+LOG = logging.getLogger(__name__)
+
+
+class TestConfig(helpers.FilesystemMockingTestCase):
+ def setUp(self):
+ super(TestConfig, self).setUp()
+ self.tmp = self.tmp_dir()
+ self.zypp_conf = 'etc/zypp/zypp.conf'
+
+ def test_bad_repo_config(self):
+ """Config has no baseurl, no file should be written"""
+ cfg = {
+ 'repos': [
+ {
+ 'id': 'foo',
+ 'name': 'suse-test',
+ 'enabled': '1'
+ },
+ ]
+ }
+ self.patchUtils(self.tmp)
+ cc_zypper_add_repo._write_repos(cfg['repos'], '/etc/zypp/repos.d')
+ self.assertRaises(IOError, util.load_file,
+ "/etc/zypp/repos.d/foo.repo")
+
+ def test_write_repos(self):
+ """Verify valid repos get written"""
+ cfg = self._get_base_config_repos()
+ root_d = self.tmp_dir()
+ cc_zypper_add_repo._write_repos(cfg['zypper']['repos'], root_d)
+ repos = glob.glob('%s/*.repo' % root_d)
+ expected_repos = ['testing-foo.repo', 'testing-bar.repo']
+ if len(repos) != 2:
+ assert 'Number of repos written is "%d" expected 2' % len(repos)
+ for repo in repos:
+ repo_name = os.path.basename(repo)
+ if repo_name not in expected_repos:
+ assert 'Found repo with name "%s"; unexpected' % repo_name
+ # Validation that the content gets properly written is in another test
+
+ def test_write_repo(self):
+ """Verify the content of a repo file"""
+ cfg = {
+ 'repos': [
+ {
+ 'baseurl': 'http://foo',
+ 'name': 'test-foo',
+ 'id': 'testing-foo'
+ },
+ ]
+ }
+ root_d = self.tmp_dir()
+ cc_zypper_add_repo._write_repos(cfg['repos'], root_d)
+ contents = util.load_file("%s/testing-foo.repo" % root_d)
+ parser = ConfigParser()
+ parser.readfp(StringIO(contents))
+ expected = {
+ 'testing-foo': {
+ 'name': 'test-foo',
+ 'baseurl': 'http://foo',
+ 'enabled': '1',
+ 'autorefresh': '1'
+ }
+ }
+ for section in expected:
+ self.assertTrue(parser.has_section(section),
+ "Contains section {0}".format(section))
+ for k, v in expected[section].items():
+ self.assertEqual(parser.get(section, k), v)
+
+ def test_config_write(self):
+ """Write valid configuration data"""
+ cfg = {
+ 'config': {
+ 'download.deltarpm': 'False',
+ 'reposdir': 'foo'
+ }
+ }
+ root_d = self.tmp_dir()
+ helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'})
+ self.reRoot(root_d)
+ cc_zypper_add_repo._write_zypp_config(cfg['config'])
+ cfg_out = os.path.join(root_d, self.zypp_conf)
+ contents = util.load_file(cfg_out)
+ expected = [
+ '# Zypp config',
+ '# Added via cloud.cfg',
+ 'download.deltarpm=False',
+ 'reposdir=foo'
+ ]
+ for item in contents.split('\n'):
+ if item not in expected:
+ self.assertIsNone(item)
+
+ @mock.patch('cloudinit.log.logging')
+ def test_config_write_skip_configdir(self, mock_logging):
+ """Write configuration but skip writing 'configdir' setting"""
+ cfg = {
+ 'config': {
+ 'download.deltarpm': 'False',
+ 'reposdir': 'foo',
+ 'configdir': 'bar'
+ }
+ }
+ root_d = self.tmp_dir()
+ helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'})
+ self.reRoot(root_d)
+ cc_zypper_add_repo._write_zypp_config(cfg['config'])
+ cfg_out = os.path.join(root_d, self.zypp_conf)
+ contents = util.load_file(cfg_out)
+ expected = [
+ '# Zypp config',
+ '# Added via cloud.cfg',
+ 'download.deltarpm=False',
+ 'reposdir=foo'
+ ]
+ for item in contents.split('\n'):
+ if item not in expected:
+ self.assertIsNone(item)
+ # Not finding teh right path for mocking :(
+ # assert mock_logging.warning.called
+
+ def test_empty_config_section_no_new_data(self):
+ """When the config section is empty no new data should be written to
+ zypp.conf"""
+ cfg = self._get_base_config_repos()
+ cfg['zypper']['config'] = None
+ root_d = self.tmp_dir()
+ helpers.populate_dir(root_d, {self.zypp_conf: '# No data'})
+ self.reRoot(root_d)
+ cc_zypper_add_repo._write_zypp_config(cfg.get('config', {}))
+ cfg_out = os.path.join(root_d, self.zypp_conf)
+ contents = util.load_file(cfg_out)
+ self.assertEqual(contents, '# No data')
+
+ def test_empty_config_value_no_new_data(self):
+ """When the config section is not empty but there are no values
+ no new data should be written to zypp.conf"""
+ cfg = self._get_base_config_repos()
+ cfg['zypper']['config'] = {
+ 'download.deltarpm': None
+ }
+ root_d = self.tmp_dir()
+ helpers.populate_dir(root_d, {self.zypp_conf: '# No data'})
+ self.reRoot(root_d)
+ cc_zypper_add_repo._write_zypp_config(cfg.get('config', {}))
+ cfg_out = os.path.join(root_d, self.zypp_conf)
+ contents = util.load_file(cfg_out)
+ self.assertEqual(contents, '# No data')
+
+ def test_handler_full_setup(self):
+ """Test that the handler ends up calling the renderers"""
+ cfg = self._get_base_config_repos()
+ cfg['zypper']['config'] = {
+ 'download.deltarpm': 'False',
+ }
+ root_d = self.tmp_dir()
+ os.makedirs('%s/etc/zypp/repos.d' % root_d)
+ helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'})
+ self.reRoot(root_d)
+ cc_zypper_add_repo.handle('zypper_add_repo', cfg, None, LOG, [])
+ cfg_out = os.path.join(root_d, self.zypp_conf)
+ contents = util.load_file(cfg_out)
+ expected = [
+ '# Zypp config',
+ '# Added via cloud.cfg',
+ 'download.deltarpm=False',
+ ]
+ for item in contents.split('\n'):
+ if item not in expected:
+ self.assertIsNone(item)
+ repos = glob.glob('%s/etc/zypp/repos.d/*.repo' % root_d)
+ expected_repos = ['testing-foo.repo', 'testing-bar.repo']
+ if len(repos) != 2:
+ assert 'Number of repos written is "%d" expected 2' % len(repos)
+ for repo in repos:
+ repo_name = os.path.basename(repo)
+ if repo_name not in expected_repos:
+ assert 'Found repo with name "%s"; unexpected' % repo_name
+
+ def test_no_config_section_no_new_data(self):
+ """When there is no config section no new data should be written to
+ zypp.conf"""
+ cfg = self._get_base_config_repos()
+ root_d = self.tmp_dir()
+ helpers.populate_dir(root_d, {self.zypp_conf: '# No data'})
+ self.reRoot(root_d)
+ cc_zypper_add_repo._write_zypp_config(cfg.get('config', {}))
+ cfg_out = os.path.join(root_d, self.zypp_conf)
+ contents = util.load_file(cfg_out)
+ self.assertEqual(contents, '# No data')
+
+ def test_no_repo_data(self):
+ """When there is no repo data nothing should happen"""
+ root_d = self.tmp_dir()
+ self.reRoot(root_d)
+ cc_zypper_add_repo._write_repos(None, root_d)
+ content = glob.glob('%s/*' % root_d)
+ self.assertEqual(len(content), 0)
+
+ def _get_base_config_repos(self):
+ """Basic valid repo configuration"""
+ cfg = {
+ 'zypper': {
+ 'repos': [
+ {
+ 'baseurl': 'http://foo',
+ 'name': 'test-foo',
+ 'id': 'testing-foo'
+ },
+ {
+ 'baseurl': 'http://bar',
+ 'name': 'test-bar',
+ 'id': 'testing-bar'
+ }
+ ]
+ }
+ }
+ return cfg
diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
index 745bb0f..b8fc893 100644
--- a/tests/unittests/test_handler/test_schema.py
+++ b/tests/unittests/test_handler/test_schema.py
@@ -27,7 +27,13 @@ class GetSchemaTest(CiTestCase):
"""Every cloudconfig module with schema is listed in allOf keyword."""
schema = get_schema()
self.assertItemsEqual(
- ['cc_bootcmd', 'cc_ntp', 'cc_resizefs', 'cc_runcmd'],
+ [
+ 'cc_bootcmd',
+ 'cc_ntp',
+ 'cc_resizefs',
+ 'cc_runcmd',
+ 'cc_zypper_add_repo'
+ ],
[subschema['id'] for subschema in schema['allOf']])
self.assertEqual('cloud-config-schema', schema['id'])
self.assertEqual(
diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd
index ff9153a..d23fde2 100755
--- a/tools/build-on-freebsd
+++ b/tools/build-on-freebsd
@@ -18,7 +18,6 @@ pkgs="
py27-jsonpatch
py27-jsonpointer
py27-oauthlib
- py27-prettytable
py27-requests
py27-serial
py27-six
diff --git a/tox.ini b/tox.ini
index 776f425..aef1f84 100644
--- a/tox.ini
+++ b/tox.ini
@@ -64,7 +64,6 @@ deps =
# requirements
jinja2==2.8
pyyaml==3.11
- PrettyTable==0.7.2
oauthlib==1.0.3
pyserial==3.0.1
configobj==5.0.6
@@ -89,7 +88,6 @@ deps =
argparse==1.2.1
jinja2==2.2.1
pyyaml==3.10
- PrettyTable==0.7.2
oauthlib==0.6.0
configobj==4.6.0
requests==2.6.0
@@ -105,7 +103,6 @@ deps =
argparse==1.3.0
jinja2==2.8
PyYAML==3.11
- PrettyTable==0.7.2
oauthlib==0.7.2
configobj==5.0.6
requests==2.11.1