cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #05880
[Merge] ~raharper/cloud-init:ubuntu/devel/newupstream-20181214 into cloud-init:ubuntu/devel
Ryan Harper has proposed merging ~raharper/cloud-init:ubuntu/devel/newupstream-20181214 into cloud-init:ubuntu/devel.
Commit message:
cloud-init (18.5-1-g5b065316-0ubuntu1) disco; urgency=medium
* New upstream snapshot.
- Update to pylint 2.2.2.
- Release 18.5 (LP: #1808380)
- tests: add Disco release [Joshua Powers]
- net: render 'metric' values in per-subnet routes (LP: #1805871)
- write_files: add support for appending to files. [James Baxter]
- config: On ubuntu select cloud archive mirrors for armel, armhf, arm64.
(LP: #1805854)
- dhclient-hook: cleanups, tests and fix a bug on 'down' event.
- NoCloud: Allow top level 'network' key in network-config. (LP: #1798117)
- ovf: Fix ovf network config generation gateway/routes (LP: #1806103)
-- Ryan Harper <ryan.harper@xxxxxxxxxxxxx> Fri, 14 Dec 2018 14:45:46 -0600
Requested reviews:
cloud-init commiters (cloud-init-dev)
Related bugs:
Bug #1805854 in cloud-init: "[feature-request] Add non-x86 Ubuntu EC2 mirrors in to default cloud-init configuration"
https://bugs.launchpad.net/cloud-init/+bug/1805854
Bug #1805871 in cloud-init (Ubuntu): "net renderers miss metric value in per-subnet routes"
https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1805871
Bug #1808380 in cloud-init: "Release 18.5"
https://bugs.launchpad.net/cloud-init/+bug/1808380
For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/360957
--
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:ubuntu/devel/newupstream-20181214 into cloud-init:ubuntu/devel.
diff --git a/ChangeLog b/ChangeLog
index 9c043b0..8fa6fdd 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,57 @@
+18.5:
+ - tests: add Disco release [Joshua Powers]
+ - net: render 'metric' values in per-subnet routes (LP: #1805871)
+ - write_files: add support for appending to files. [James Baxter]
+ - config: On ubuntu select cloud archive mirrors for armel, armhf, arm64.
+ (LP: #1805854)
+ - dhclient-hook: cleanups, tests and fix a bug on 'down' event.
+ - NoCloud: Allow top level 'network' key in network-config. (LP: #1798117)
+ - ovf: Fix ovf network config generation gateway/routes (LP: #1806103)
+ - azure: detect vnet migration via netlink media change event
+ [Tamilmani Manoharan]
+ - Azure: fix copy/paste error in error handling when reading azure ovf.
+ [Adam DePue]
+ - tests: fix incorrect order of mocks in test_handle_zfs_root.
+ - doc: Change dns_nameserver property to dns_nameservers. [Tomer Cohen]
+ - OVF: identify label iso9660 filesystems with label 'OVF ENV'.
+ - logs: collect-logs ignore instance-data-sensitive.json on non-root user
+ (LP: #1805201)
+ - net: Ephemeral*Network: add connectivity check via URL
+ - azure: _poll_imds only retry on 404. Fail on Timeout (LP: #1803598)
+ - resizefs: Prefix discovered devpath with '/dev/' when path does not
+ exist [Igor Galić]
+ - azure: retry imds polling on requests.Timeout (LP: #1800223)
+ - azure: Accept variation in error msg from mount for ntfs volumes
+ [Jason Zions] (LP: #1799338)
+ - azure: fix regression introduced when persisting ephemeral dhcp lease
+ [asakkurr]
+ - azure: add udev rules to create cloud-init Gen2 disk name symlinks
+ (LP: #1797480)
+ - tests: ec2 mock missing httpretty user-data and instance-identity routes
+ - azure: remove /etc/netplan/90-hotplug-azure.yaml when net from IMDS
+ - azure: report ready to fabric after reprovision and reduce logging
+ [asakkurr] (LP: #1799594)
+ - query: better error when missing read permission on instance-data
+ - instance-data: fallback to instance-data.json if sensitive is absent.
+ (LP: #1798189)
+ - docs: remove colon from network v1 config example. [Tomer Cohen]
+ - Add cloud-id binary to packages for SUSE [Jason Zions]
+ - systemd: On SUSE ensure cloud-init.service runs before wicked
+ [Robert Schweikert] (LP: #1799709)
+ - update detection of openSUSE variants [Robert Schweikert]
+ - azure: Add apply_network_config option to disable network from IMDS
+ (LP: #1798424)
+ - Correct spelling in an error message (udevadm). [Katie McLaughlin]
+ - tests: meta_data key changed to meta-data in ec2 instance-data.json
+ (LP: #1797231)
+ - tests: fix kvm integration test to assert flexible config-disk path
+ (LP: #1797199)
+ - tools: Add cloud-id command line utility
+ - instance-data: Add standard keys platform and subplatform. Refactor ec2.
+ - net: ignore nics that have "zero" mac address. (LP: #1796917)
+ - tests: fix apt_configure_primary to be more flexible
+ - Ubuntu: update sources.list to comment out deb-src entries. (LP: #74747)
+
18.4:
- add rtd example docs about new standardized keys
- use ds._crawled_metadata instance attribute if set when writing
diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init
index 8c25032..a9577e9 100644
--- a/bash_completion/cloud-init
+++ b/bash_completion/cloud-init
@@ -30,7 +30,10 @@ _cloudinit_complete()
devel)
COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word))
;;
- dhclient-hook|features)
+ dhclient-hook)
+ COMPREPLY=($(compgen -W "--help up down" -- $cur_word))
+ ;;
+ features)
COMPREPLY=($(compgen -W "--help" -- $cur_word))
;;
init)
diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py
index a0f58a0..1ad7e0b 100755
--- a/cloudinit/cmd/devel/net_convert.py
+++ b/cloudinit/cmd/devel/net_convert.py
@@ -9,6 +9,7 @@ import yaml
from cloudinit.sources.helpers import openstack
from cloudinit.sources import DataSourceAzure as azure
+from cloudinit.sources import DataSourceOVF as ovf
from cloudinit import distros
from cloudinit.net import eni, netplan, network_state, sysconfig
@@ -31,7 +32,7 @@ def get_parser(parser=None):
metavar="PATH", required=True)
parser.add_argument("-k", "--kind",
choices=['eni', 'network_data.json', 'yaml',
- 'azure-imds'],
+ 'azure-imds', 'vmware-imc'],
required=True)
parser.add_argument("-d", "--directory",
metavar="PATH",
@@ -76,7 +77,6 @@ def handle_args(name, args):
net_data = args.network_data.read()
if args.kind == "eni":
pre_ns = eni.convert_eni_data(net_data)
- ns = network_state.parse_net_config_data(pre_ns)
elif args.kind == "yaml":
pre_ns = yaml.load(net_data)
if 'network' in pre_ns:
@@ -85,15 +85,16 @@ def handle_args(name, args):
sys.stderr.write('\n'.join(
["Input YAML",
yaml.dump(pre_ns, default_flow_style=False, indent=4), ""]))
- ns = network_state.parse_net_config_data(pre_ns)
elif args.kind == 'network_data.json':
pre_ns = openstack.convert_net_json(
json.loads(net_data), known_macs=known_macs)
- ns = network_state.parse_net_config_data(pre_ns)
elif args.kind == 'azure-imds':
pre_ns = azure.parse_network_config(json.loads(net_data))
- ns = network_state.parse_net_config_data(pre_ns)
+ elif args.kind == 'vmware-imc':
+ config = ovf.Config(ovf.ConfigFile(args.network_data.name))
+ pre_ns = ovf.get_network_config_from_conf(config, False)
+ ns = network_state.parse_net_config_data(pre_ns)
if not ns:
raise RuntimeError("No valid network_state object created from"
"input data")
@@ -111,6 +112,10 @@ def handle_args(name, args):
elif args.output_kind == "netplan":
r_cls = netplan.Renderer
config = distro.renderer_configs.get('netplan')
+ # don't run netplan generate/apply
+ config['postcmds'] = False
+ # trim leading slash
+ config['netplan_path'] = config['netplan_path'][1:]
else:
r_cls = sysconfig.Renderer
config = distro.renderer_configs.get('sysconfig')
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index 5a43702..933c019 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -41,7 +41,7 @@ from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
from cloudinit import atomic_helper
from cloudinit.config import cc_set_hostname
-from cloudinit.dhclient_hook import LogDhclient
+from cloudinit import dhclient_hook
# Welcome message template
@@ -586,12 +586,6 @@ def main_single(name, args):
return 0
-def dhclient_hook(name, args):
- record = LogDhclient(args)
- record.check_hooks_dir()
- record.record()
-
-
def status_wrapper(name, args, data_d=None, link_d=None):
if data_d is None:
data_d = os.path.normpath("/var/lib/cloud/data")
@@ -795,15 +789,9 @@ def main(sysv_args=None):
'query',
help='Query standardized instance metadata from the command line.')
- parser_dhclient = subparsers.add_parser('dhclient-hook',
- help=('run the dhclient hook'
- 'to record network info'))
- parser_dhclient.add_argument("net_action",
- help=('action taken on the interface'))
- parser_dhclient.add_argument("net_interface",
- help=('the network interface being acted'
- ' upon'))
- parser_dhclient.set_defaults(action=('dhclient_hook', dhclient_hook))
+ parser_dhclient = subparsers.add_parser(
+ dhclient_hook.NAME, help=dhclient_hook.__doc__)
+ dhclient_hook.get_parser(parser_dhclient)
parser_features = subparsers.add_parser('features',
help=('list defined features'))
diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py
index 31d1db6..0b6546e 100644
--- a/cloudinit/config/cc_write_files.py
+++ b/cloudinit/config/cc_write_files.py
@@ -49,6 +49,10 @@ binary gzip data can be specified and will be decoded before being written.
...
path: /bin/arch
permissions: '0555'
+ - content: |
+ 15 * * * * root ship_logs
+ path: /etc/crontab
+ append: true
"""
import base64
@@ -113,7 +117,8 @@ def write_files(name, files):
contents = extract_contents(f_info.get('content', ''), extractions)
(u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER))
perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS)
- util.write_file(path, contents, mode=perms)
+ omode = 'ab' if util.get_cfg_option_bool(f_info, 'append') else 'wb'
+ util.write_file(path, contents, omode=omode, mode=perms)
util.chownbyname(path, u, g)
diff --git a/cloudinit/dhclient_hook.py b/cloudinit/dhclient_hook.py
index 7f02d7f..72b51b6 100644
--- a/cloudinit/dhclient_hook.py
+++ b/cloudinit/dhclient_hook.py
@@ -1,5 +1,8 @@
# This file is part of cloud-init. See LICENSE file for license information.
+"""Run the dhclient hook to record network info."""
+
+import argparse
import os
from cloudinit import atomic_helper
@@ -8,44 +11,75 @@ from cloudinit import stages
LOG = logging.getLogger(__name__)
+NAME = "dhclient-hook"
+UP = "up"
+DOWN = "down"
+EVENTS = (UP, DOWN)
+
+
+def _get_hooks_dir():
+ i = stages.Init()
+ return os.path.join(i.paths.get_runpath(), 'dhclient.hooks')
+
+
+def _filter_env_vals(info):
+ """Given info (os.environ), return a dictionary with
+ lower case keys for each entry starting with DHCP4_ or new_."""
+ new_info = {}
+ for k, v in info.items():
+ if k.startswith("DHCP4_") or k.startswith("new_"):
+ key = (k.replace('DHCP4_', '').replace('new_', '')).lower()
+ new_info[key] = v
+ return new_info
+
+
+def run_hook(interface, event, data_d=None, env=None):
+ if event not in EVENTS:
+ raise ValueError("Unexpected event '%s'. Expected one of: %s" %
+ (event, EVENTS))
+ if data_d is None:
+ data_d = _get_hooks_dir()
+ if env is None:
+ env = os.environ
+ hook_file = os.path.join(data_d, interface + ".json")
+
+ if event == UP:
+ if not os.path.exists(data_d):
+ os.makedirs(data_d)
+ atomic_helper.write_json(hook_file, _filter_env_vals(env))
+ LOG.debug("Wrote dhclient options in %s", hook_file)
+ elif event == DOWN:
+ if os.path.exists(hook_file):
+ os.remove(hook_file)
+ LOG.debug("Removed dhclient options file %s", hook_file)
+
+
+def get_parser(parser=None):
+ if parser is None:
+ parser = argparse.ArgumentParser(prog=NAME, description=__doc__)
+ parser.add_argument(
+ "event", help='event taken on the interface', choices=EVENTS)
+ parser.add_argument(
+ "interface", help='the network interface being acted upon')
+ # cloud-init main uses 'action'
+ parser.set_defaults(action=(NAME, handle_args))
+ return parser
+
+
+def handle_args(name, args, data_d=None):
+ """Handle the Namespace args.
+ Takes 'name' as passed by cloud-init main. not used here."""
+ return run_hook(interface=args.interface, event=args.event, data_d=data_d)
+
+
+if __name__ == '__main__':
+ import sys
+ parser = get_parser()
+ args = parser.parse_args(args=sys.argv[1:])
+ return_value = handle_args(
+ NAME, args, data_d=os.environ.get('_CI_DHCP_HOOK_DATA_D'))
+ if return_value:
+ sys.exit(return_value)
-class LogDhclient(object):
-
- def __init__(self, cli_args):
- self.hooks_dir = self._get_hooks_dir()
- self.net_interface = cli_args.net_interface
- self.net_action = cli_args.net_action
- self.hook_file = os.path.join(self.hooks_dir,
- self.net_interface + ".json")
-
- @staticmethod
- def _get_hooks_dir():
- i = stages.Init()
- return os.path.join(i.paths.get_runpath(), 'dhclient.hooks')
-
- def check_hooks_dir(self):
- if not os.path.exists(self.hooks_dir):
- os.makedirs(self.hooks_dir)
- else:
- # If the action is down and the json file exists, we need to
- # delete the file
- if self.net_action is 'down' and os.path.exists(self.hook_file):
- os.remove(self.hook_file)
-
- @staticmethod
- def get_vals(info):
- new_info = {}
- for k, v in info.items():
- if k.startswith("DHCP4_") or k.startswith("new_"):
- key = (k.replace('DHCP4_', '').replace('new_', '')).lower()
- new_info[key] = v
- return new_info
-
- def record(self):
- envs = os.environ
- if self.hook_file is None:
- return
- atomic_helper.write_json(self.hook_file, self.get_vals(envs))
- LOG.debug("Wrote dhclient options in %s", self.hook_file)
# vi: ts=4 expandtab
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index c6f631a..6423632 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -371,22 +371,23 @@ class Renderer(renderer.Renderer):
'gateway': 'gw',
'metric': 'metric',
}
+
+ default_gw = ''
if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
- default_gw = " default gw %s" % route['gateway']
- content.append(up + default_gw + or_true)
- content.append(down + default_gw + or_true)
+ default_gw = ' default'
elif route['network'] == '::' and route['prefix'] == 0:
- # ipv6!
- default_gw = " -A inet6 default gw %s" % route['gateway']
- content.append(up + default_gw + or_true)
- content.append(down + default_gw + or_true)
- else:
- route_line = ""
- for k in ['network', 'netmask', 'gateway', 'metric']:
- if k in route:
- route_line += " %s %s" % (mapping[k], route[k])
- content.append(up + route_line + or_true)
- content.append(down + route_line + or_true)
+ default_gw = ' -A inet6 default'
+
+ route_line = ''
+ for k in ['network', 'netmask', 'gateway', 'metric']:
+ if default_gw and k in ['network', 'netmask']:
+ continue
+ if k == 'gateway':
+ route_line += '%s %s %s' % (default_gw, mapping[k], route[k])
+ elif k in route:
+ route_line += ' %s %s' % (mapping[k], route[k])
+ content.append(up + route_line + or_true)
+ content.append(down + route_line + or_true)
return content
def _render_iface(self, iface, render_hwaddress=False):
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index bc1087f..21517fd 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -114,13 +114,13 @@ def _extract_addresses(config, entry, ifname):
for route in subnet.get('routes', []):
to_net = "%s/%s" % (route.get('network'),
route.get('prefix'))
- route = {
+ new_route = {
'via': route.get('gateway'),
'to': to_net,
}
if 'metric' in route:
- route.update({'metric': route.get('metric', 100)})
- routes.append(route)
+ new_route.update({'metric': route.get('metric', 100)})
+ routes.append(new_route)
addresses.append(addr)
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 9c16d3a..17293e1 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -156,13 +156,23 @@ class Route(ConfigMap):
_quote_value(gateway_value)))
buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
_quote_value(netmask_value)))
+ metric_key = 'METRIC' + index
+ if metric_key in self._conf:
+ metric_value = str(self._conf['METRIC' + index])
+ buf.write("%s=%s\n" % ('METRIC' + str(reindex),
+ _quote_value(metric_value)))
elif proto == "ipv6" and self.is_ipv6_route(address_value):
netmask_value = str(self._conf['NETMASK' + index])
gateway_value = str(self._conf['GATEWAY' + index])
- buf.write("%s/%s via %s dev %s\n" % (address_value,
- netmask_value,
- gateway_value,
- self._route_name))
+ metric_value = (
+ 'metric ' + str(self._conf['METRIC' + index])
+ if 'METRIC' + index in self._conf else '')
+ buf.write(
+ "%s/%s via %s %s dev %s\n" % (address_value,
+ netmask_value,
+ gateway_value,
+ metric_value,
+ self._route_name))
return buf.getvalue()
@@ -370,6 +380,9 @@ class Renderer(renderer.Renderer):
else:
iface_cfg['GATEWAY'] = subnet['gateway']
+ if 'metric' in subnet:
+ iface_cfg['METRIC'] = subnet['metric']
+
if 'dns_search' in subnet:
iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search'])
@@ -414,15 +427,19 @@ class Renderer(renderer.Renderer):
else:
iface_cfg['GATEWAY'] = route['gateway']
route_cfg.has_set_default_ipv4 = True
+ if 'metric' in route:
+ iface_cfg['METRIC'] = route['metric']
else:
gw_key = 'GATEWAY%s' % route_cfg.last_idx
nm_key = 'NETMASK%s' % route_cfg.last_idx
addr_key = 'ADDRESS%s' % route_cfg.last_idx
+ metric_key = 'METRIC%s' % route_cfg.last_idx
route_cfg.last_idx += 1
# add default routes only to ifcfg files, not
# to route-* or route6-*
for (old_key, new_key) in [('gateway', gw_key),
+ ('metric', metric_key),
('netmask', nm_key),
('network', addr_key)]:
if old_key in route:
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index e076d5d..46efca4 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -980,8 +980,8 @@ def read_azure_ovf(contents):
raise NonAzureDataSource("No LinuxProvisioningConfigurationSet")
if len(lpcs_nodes) > 1:
raise BrokenAzureDataSource("found '%d' %ss" %
- ("LinuxProvisioningConfigurationSet",
- len(lpcs_nodes)))
+ (len(lpcs_nodes),
+ "LinuxProvisioningConfigurationSet"))
lpcs = lpcs_nodes[0]
if not lpcs.hasChildNodes():
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index 9010f06..6860f0c 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -311,6 +311,35 @@ def parse_cmdline_data(ds_id, fill, cmdline=None):
return True
+def _maybe_remove_top_network(cfg):
+ """If network-config contains top level 'network' key, then remove it.
+
+ Some providers of network configuration may provide a top level
+ 'network' key (LP: #1798117) even though it is not necessary.
+
+ Be friendly and remove it if it really seems so.
+
+ Return the original value if no change or the updated value if changed."""
+ nullval = object()
+ network_val = cfg.get('network', nullval)
+ if network_val is nullval:
+ return cfg
+ bmsg = 'Top level network key in network-config %s: %s'
+ if not isinstance(network_val, dict):
+ LOG.debug(bmsg, "was not a dict", cfg)
+ return cfg
+ if len(list(cfg.keys())) != 1:
+ LOG.debug(bmsg, "had multiple top level keys", cfg)
+ return cfg
+ if network_val.get('config') == "disabled":
+ LOG.debug(bmsg, "was config/disabled", cfg)
+ elif not all(('config' in network_val, 'version' in network_val)):
+ LOG.debug(bmsg, "but missing 'config' or 'version'", cfg)
+ return cfg
+ LOG.debug(bmsg, "fixed by removing shifting network.", cfg)
+ return network_val
+
+
def _merge_new_seed(cur, seeded):
ret = cur.copy()
@@ -320,7 +349,8 @@ def _merge_new_seed(cur, seeded):
ret['meta-data'] = util.mergemanydict([cur['meta-data'], newmd])
if seeded.get('network-config'):
- ret['network-config'] = util.load_yaml(seeded['network-config'])
+ ret['network-config'] = _maybe_remove_top_network(
+ util.load_yaml(seeded.get('network-config')))
if 'user-data' in seeded:
ret['user-data'] = seeded['user-data']
diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
index e1890e2..77cbf3b 100644
--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
@@ -165,9 +165,8 @@ class NicConfigurator(object):
# Add routes if there is no primary nic
if not self._primaryNic and v4.gateways:
- route_list.extend(self.gen_ipv4_route(nic,
- v4.gateways,
- v4.netmask))
+ subnet.update(
+ {'routes': self.gen_ipv4_route(nic, v4.gateways, v4.netmask)})
return ([subnet], route_list)
diff --git a/cloudinit/tests/test_dhclient_hook.py b/cloudinit/tests/test_dhclient_hook.py
new file mode 100644
index 0000000..7aab8dd
--- /dev/null
+++ b/cloudinit/tests/test_dhclient_hook.py
@@ -0,0 +1,105 @@
+# 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 mock
+import os
+
+
+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/version.py b/cloudinit/version.py
index 844a02e..a2c5d43 100644
--- a/cloudinit/version.py
+++ b/cloudinit/version.py
@@ -4,7 +4,7 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
-__VERSION__ = "18.4"
+__VERSION__ = "18.5"
_PACKAGED_VERSION = '@@PACKAGED_VERSION@@'
FEATURES = [
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index 1fef133..7513176 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -167,7 +167,17 @@ system_info:
- http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/
- http://%(region)s.clouds.archive.ubuntu.com/ubuntu/
security: []
- - arches: [armhf, armel, default]
+ - arches: [arm64, armel, armhf]
+ failsafe:
+ primary: http://ports.ubuntu.com/ubuntu-ports
+ security: http://ports.ubuntu.com/ubuntu-ports
+ search:
+ primary:
+ - http://%(ec2_region)s.ec2.ports.ubuntu.com/ubuntu-ports/
+ - http://%(availability_zone)s.clouds.ports.ubuntu.com/ubuntu-ports/
+ - http://%(region)s.clouds.ports.ubuntu.com/ubuntu-ports/
+ security: []
+ - arches: [default]
failsafe:
primary: http://ports.ubuntu.com/ubuntu-ports
security: http://ports.ubuntu.com/ubuntu-ports
diff --git a/debian/changelog b/debian/changelog
index 283bcd8..09a0034 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,19 @@
+cloud-init (18.5-1-g5b065316-0ubuntu1) disco; urgency=medium
+
+ * New upstream snapshot.
+ - Update to pylint 2.2.2.
+ - Release 18.5 (LP: #1808380)
+ - tests: add Disco release [Joshua Powers]
+ - net: render 'metric' values in per-subnet routes (LP: #1805871)
+ - write_files: add support for appending to files. [James Baxter]
+ - config: On ubuntu select cloud archive mirrors for armel, armhf, arm64.
+ (LP: #1805854)
+ - dhclient-hook: cleanups, tests and fix a bug on 'down' event.
+ - NoCloud: Allow top level 'network' key in network-config. (LP: #1798117)
+ - ovf: Fix ovf network config generation gateway/routes (LP: #1806103)
+
+ -- Ryan Harper <ryan.harper@xxxxxxxxxxxxx> Fri, 14 Dec 2018 14:45:46 -0600
+
cloud-init (18.4-31-gbf791715-0ubuntu1) disco; urgency=medium
* New upstream snapshot.
diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
index defae02..ec5da72 100644
--- a/tests/cloud_tests/releases.yaml
+++ b/tests/cloud_tests/releases.yaml
@@ -129,6 +129,22 @@ features:
releases:
# UBUNTU =================================================================
+ disco:
+ # EOL: Jan 2020
+ default:
+ enabled: true
+ release: disco
+ version: 19.04
+ os: ubuntu
+ feature_groups:
+ - base
+ - debian_base
+ - ubuntu_specific
+ lxd:
+ sstreams_server: https://cloud-images.ubuntu.com/daily
+ alias: disco
+ setup_overrides: null
+ override_templates: false
cosmic:
# EOL: Jul 2019
default:
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index 199d69b..d283f13 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -246,18 +246,18 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
self.assertEqual('cc_ntp', parseargs.name)
self.assertFalse(parseargs.report)
- @mock.patch('cloudinit.cmd.main.dhclient_hook')
- def test_dhclient_hook_subcommand(self, m_dhclient_hook):
+ @mock.patch('cloudinit.cmd.main.dhclient_hook.handle_args')
+ def test_dhclient_hook_subcommand(self, m_handle_args):
"""The subcommand 'dhclient-hook' calls dhclient_hook with args."""
- self._call_main(['cloud-init', 'dhclient-hook', 'net_action', 'eth0'])
- (name, parseargs) = m_dhclient_hook.call_args_list[0][0]
- self.assertEqual('dhclient_hook', name)
+ self._call_main(['cloud-init', 'dhclient-hook', 'up', 'eth0'])
+ (name, parseargs) = m_handle_args.call_args_list[0][0]
+ self.assertEqual('dhclient-hook', name)
self.assertEqual('dhclient-hook', parseargs.subcommand)
- self.assertEqual('dhclient_hook', parseargs.action[0])
+ self.assertEqual('dhclient-hook', parseargs.action[0])
self.assertFalse(parseargs.debug)
self.assertFalse(parseargs.force)
- self.assertEqual('net_action', parseargs.net_action)
- self.assertEqual('eth0', parseargs.net_interface)
+ self.assertEqual('up', parseargs.event)
+ self.assertEqual('eth0', parseargs.interface)
@mock.patch('cloudinit.cmd.main.main_features')
def test_features_hook_subcommand(self, m_features):
diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py
index b6468b6..3429272 100644
--- a/tests/unittests/test_datasource/test_nocloud.py
+++ b/tests/unittests/test_datasource/test_nocloud.py
@@ -1,7 +1,10 @@
# This file is part of cloud-init. See LICENSE file for license information.
from cloudinit import helpers
-from cloudinit.sources import DataSourceNoCloud
+from cloudinit.sources.DataSourceNoCloud import (
+ DataSourceNoCloud as dsNoCloud,
+ _maybe_remove_top_network,
+ parse_cmdline_data)
from cloudinit import util
from cloudinit.tests.helpers import CiTestCase, populate_dir, mock, ExitStack
@@ -40,9 +43,7 @@ class TestNoCloudDataSource(CiTestCase):
'datasource': {'NoCloud': {'fs_label': None}}
}
- ds = DataSourceNoCloud.DataSourceNoCloud
-
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertEqual(dsrc.userdata_raw, ud)
self.assertEqual(dsrc.metadata, md)
@@ -63,9 +64,7 @@ class TestNoCloudDataSource(CiTestCase):
'datasource': {'NoCloud': {'fs_label': None}}
}
- ds = DataSourceNoCloud.DataSourceNoCloud
-
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
self.assertTrue(dsrc.get_data())
self.assertEqual(dsrc.platform_type, 'nocloud')
self.assertEqual(
@@ -73,8 +72,6 @@ class TestNoCloudDataSource(CiTestCase):
def test_fs_label(self, m_is_lxd):
# find_devs_with should not be called ff fs_label is None
- ds = DataSourceNoCloud.DataSourceNoCloud
-
class PsuedoException(Exception):
pass
@@ -84,12 +81,12 @@ class TestNoCloudDataSource(CiTestCase):
# by default, NoCloud should search for filesystems by label
sys_cfg = {'datasource': {'NoCloud': {}}}
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
self.assertRaises(PsuedoException, dsrc.get_data)
# but disabling searching should just end up with None found
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertFalse(ret)
@@ -97,13 +94,10 @@ class TestNoCloudDataSource(CiTestCase):
# no source should be found if no cmdline, config, and fs_label=None
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- ds = DataSourceNoCloud.DataSourceNoCloud
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
self.assertFalse(dsrc.get_data())
def test_seed_in_config(self, m_is_lxd):
- ds = DataSourceNoCloud.DataSourceNoCloud
-
data = {
'fs_label': None,
'meta-data': yaml.safe_dump({'instance-id': 'IID'}),
@@ -111,7 +105,7 @@ class TestNoCloudDataSource(CiTestCase):
}
sys_cfg = {'datasource': {'NoCloud': data}}
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertEqual(dsrc.userdata_raw, b"USER_DATA_RAW")
self.assertEqual(dsrc.metadata.get('instance-id'), 'IID')
@@ -130,9 +124,7 @@ class TestNoCloudDataSource(CiTestCase):
'datasource': {'NoCloud': {'fs_label': None}}
}
- ds = DataSourceNoCloud.DataSourceNoCloud
-
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertEqual(dsrc.userdata_raw, ud)
self.assertEqual(dsrc.metadata, md)
@@ -145,9 +137,7 @@ class TestNoCloudDataSource(CiTestCase):
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- ds = DataSourceNoCloud.DataSourceNoCloud
-
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertEqual(dsrc.userdata_raw, b"ud")
self.assertFalse(dsrc.vendordata)
@@ -174,9 +164,7 @@ class TestNoCloudDataSource(CiTestCase):
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- ds = DataSourceNoCloud.DataSourceNoCloud
-
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertTrue(ret)
# very simple check just for the strings above
@@ -195,9 +183,23 @@ class TestNoCloudDataSource(CiTestCase):
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- ds = DataSourceNoCloud.DataSourceNoCloud
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ ret = dsrc.get_data()
+ self.assertTrue(ret)
+ self.assertEqual(netconf, dsrc.network_config)
+
+ def test_metadata_network_config_with_toplevel_network(self, m_is_lxd):
+ """network-config may have 'network' top level key."""
+ netconf = {'config': 'disabled'}
+ populate_dir(
+ os.path.join(self.paths.seed_dir, "nocloud"),
+ {'user-data': b"ud",
+ 'meta-data': "instance-id: IID\n",
+ 'network-config': yaml.dump({'network': netconf}) + "\n"})
+
+ sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertTrue(ret)
self.assertEqual(netconf, dsrc.network_config)
@@ -228,9 +230,7 @@ class TestNoCloudDataSource(CiTestCase):
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
- ds = DataSourceNoCloud.DataSourceNoCloud
-
- dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
ret = dsrc.get_data()
self.assertTrue(ret)
self.assertEqual(netconf, dsrc.network_config)
@@ -258,8 +258,7 @@ class TestParseCommandLineData(CiTestCase):
for (fmt, expected) in pairs:
fill = {}
cmdline = fmt % {'ds_id': ds_id}
- ret = DataSourceNoCloud.parse_cmdline_data(ds_id=ds_id, fill=fill,
- cmdline=cmdline)
+ ret = parse_cmdline_data(ds_id=ds_id, fill=fill, cmdline=cmdline)
self.assertEqual(expected, fill)
self.assertTrue(ret)
@@ -276,10 +275,43 @@ class TestParseCommandLineData(CiTestCase):
for cmdline in cmdlines:
fill = {}
- ret = DataSourceNoCloud.parse_cmdline_data(ds_id=ds_id, fill=fill,
- cmdline=cmdline)
+ ret = parse_cmdline_data(ds_id=ds_id, fill=fill, cmdline=cmdline)
self.assertEqual(fill, {})
self.assertFalse(ret)
+class TestMaybeRemoveToplevelNetwork(CiTestCase):
+ """test _maybe_remove_top_network function."""
+ basecfg = [{'type': 'physical', 'name': 'interface0',
+ 'subnets': [{'type': 'dhcp'}]}]
+
+ def test_should_remove_safely(self):
+ mcfg = {'config': self.basecfg, 'version': 1}
+ self.assertEqual(mcfg, _maybe_remove_top_network({'network': mcfg}))
+
+ def test_no_remove_if_other_keys(self):
+ """should not shift if other keys at top level."""
+ mcfg = {'network': {'config': self.basecfg, 'version': 1},
+ 'unknown_keyname': 'keyval'}
+ self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
+
+ def test_no_remove_if_non_dict(self):
+ """should not shift if not a dict."""
+ mcfg = {'network': '"content here'}
+ self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
+
+ def test_no_remove_if_missing_config_or_version(self):
+ """should not shift unless network entry has config and version."""
+ mcfg = {'network': {'config': self.basecfg}}
+ self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
+
+ mcfg = {'network': {'version': 1}}
+ self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
+
+ def test_remove_with_config_disabled(self):
+ """network/config=disabled should be shifted."""
+ mcfg = {'config': 'disabled'}
+ self.assertEqual(mcfg, _maybe_remove_top_network({'network': mcfg}))
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_write_files.py b/tests/unittests/test_handler/test_handler_write_files.py
index 7fa8fd2..bc8756c 100644
--- a/tests/unittests/test_handler/test_handler_write_files.py
+++ b/tests/unittests/test_handler/test_handler_write_files.py
@@ -52,6 +52,18 @@ class TestWriteFiles(FilesystemMockingTestCase):
"test_simple", [{"content": expected, "path": filename}])
self.assertEqual(util.load_file(filename), expected)
+ def test_append(self):
+ self.patchUtils(self.tmp)
+ existing = "hello "
+ added = "world\n"
+ expected = existing + added
+ filename = "/tmp/append.file"
+ util.write_file(filename, existing)
+ write_files(
+ "test_append",
+ [{"content": added, "path": filename, "append": "true"}])
+ self.assertEqual(util.load_file(filename), expected)
+
def test_yaml_binary(self):
self.patchUtils(self.tmp)
data = util.load_yaml(YAML_TEXT)
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 8e38373..195f261 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -488,8 +488,8 @@ NETWORK_CONFIGS = {
address 192.168.21.3/24
dns-nameservers 8.8.8.8 8.8.4.4
dns-search barley.maas sach.maas
- post-up route add default gw 65.61.151.37 || true
- pre-down route del default gw 65.61.151.37 || true
+ post-up route add default gw 65.61.151.37 metric 10000 || true
+ pre-down route del default gw 65.61.151.37 metric 10000 || true
""").rstrip(' '),
'expected_netplan': textwrap.dedent("""
network:
@@ -513,7 +513,8 @@ NETWORK_CONFIGS = {
- barley.maas
- sach.maas
routes:
- - to: 0.0.0.0/0
+ - metric: 10000
+ to: 0.0.0.0/0
via: 65.61.151.37
set-name: eth99
""").rstrip(' '),
@@ -537,6 +538,7 @@ NETWORK_CONFIGS = {
HWADDR=c0:d6:9f:2c:e8:80
IPADDR=192.168.21.3
NETMASK=255.255.255.0
+ METRIC=10000
NM_CONTROLLED=no
ONBOOT=yes
TYPE=Ethernet
@@ -561,7 +563,7 @@ NETWORK_CONFIGS = {
- gateway: 65.61.151.37
netmask: 0.0.0.0
network: 0.0.0.0
- metric: 2
+ metric: 10000
- type: physical
name: eth1
mac_address: "cf:d6:af:48:e8:80"
@@ -1161,6 +1163,13 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
- gateway: 192.168.0.3
netmask: 255.255.255.0
network: 10.1.3.0
+ - gateway: 2001:67c:1562:1
+ network: 2001:67c:1
+ netmask: ffff:ffff:0
+ - gateway: 3001:67c:1562:1
+ network: 3001:67c:1
+ netmask: ffff:ffff:0
+ metric: 10000
- type: static
address: 192.168.1.2/24
- type: static
@@ -1197,6 +1206,11 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
routes:
- to: 10.1.3.0/24
via: 192.168.0.3
+ - to: 2001:67c:1/32
+ via: 2001:67c:1562:1
+ - metric: 10000
+ to: 3001:67c:1/32
+ via: 3001:67c:1562:1
"""),
'yaml-v2': textwrap.dedent("""
version: 2
@@ -1228,6 +1242,11 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
routes:
- to: 10.1.3.0/24
via: 192.168.0.3
+ - to: 2001:67c:1562:8007::1/64
+ via: 2001:67c:1562:8007::aac:40b2
+ - metric: 10000
+ to: 3001:67c:1562:8007::1/64
+ via: 3001:67c:1562:8007::aac:40b2
"""),
'expected_netplan-v2': textwrap.dedent("""
network:
@@ -1249,6 +1268,11 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
routes:
- to: 10.1.3.0/24
via: 192.168.0.3
+ - to: 2001:67c:1562:8007::1/64
+ via: 2001:67c:1562:8007::aac:40b2
+ - metric: 10000
+ to: 3001:67c:1562:8007::1/64
+ via: 3001:67c:1562:8007::aac:40b2
ethernets:
eth0:
match:
@@ -1349,6 +1373,10 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
USERCTL=no
"""),
'route6-bond0': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ 2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1 dev bond0
+ 3001:67c:1/ffff:ffff:0 via 3001:67c:1562:1 metric 10000 dev bond0
"""),
'route-bond0': textwrap.dedent("""\
ADDRESS0=10.1.3.0
@@ -1879,14 +1907,24 @@ class TestRhelSysConfigRendering(CiTestCase):
return dir2dict(dir)
def _compare_files_to_expected(self, expected, found):
+
+ def _try_load(f):
+ ''' Attempt to load shell content, otherwise return as-is '''
+ try:
+ return util.load_shell_content(f)
+ except ValueError:
+ pass
+ # route6- * files aren't shell content, but iproute2 params
+ return f
+
orig_maxdiff = self.maxDiff
expected_d = dict(
- (os.path.join(self.scripts_dir, k), util.load_shell_content(v))
+ (os.path.join(self.scripts_dir, k), _try_load(v))
for k, v in expected.items())
# only compare the files in scripts_dir
scripts_found = dict(
- (k, util.load_shell_content(v)) for k, v in found.items()
+ (k, _try_load(v)) for k, v in found.items()
if k.startswith(self.scripts_dir))
try:
self.maxDiff = None
diff --git a/tests/unittests/test_vmware_config_file.py b/tests/unittests/test_vmware_config_file.py
index 602dedb..f47335e 100644
--- a/tests/unittests/test_vmware_config_file.py
+++ b/tests/unittests/test_vmware_config_file.py
@@ -263,7 +263,7 @@ class TestVmwareConfigFile(CiTestCase):
nicConfigurator = NicConfigurator(config.nics, False)
nics_cfg_list = nicConfigurator.generate()
- self.assertEqual(5, len(nics_cfg_list), "number of elements")
+ self.assertEqual(2, len(nics_cfg_list), "number of elements")
nic1 = {'name': 'NIC1'}
nic2 = {'name': 'NIC2'}
@@ -275,8 +275,6 @@ class TestVmwareConfigFile(CiTestCase):
nic1.update(cfg)
elif cfg.get('name') == nic2.get('name'):
nic2.update(cfg)
- elif cfg_type == 'route':
- route_list.append(cfg)
self.assertEqual('physical', nic1.get('type'), 'type of NIC1')
self.assertEqual('NIC1', nic1.get('name'), 'name of NIC1')
@@ -297,6 +295,9 @@ class TestVmwareConfigFile(CiTestCase):
static6_subnet.append(subnet)
else:
self.assertEqual(True, False, 'Unknown type')
+ if 'route' in subnet:
+ for route in subnet.get('routes'):
+ route_list.append(route)
self.assertEqual(1, len(static_subnet), 'Number of static subnet')
self.assertEqual(1, len(static6_subnet), 'Number of static6 subnet')
@@ -351,6 +352,8 @@ class TestVmwareConfigFile(CiTestCase):
class TestVmwareNetConfig(CiTestCase):
"""Test conversion of vmware config to cloud-init config."""
+ maxDiff = None
+
def _get_NicConfigurator(self, text):
fp = None
try:
@@ -420,9 +423,52 @@ class TestVmwareNetConfig(CiTestCase):
'mac_address': '00:50:56:a6:8c:08',
'subnets': [
{'control': 'auto', 'type': 'static',
- 'address': '10.20.87.154', 'netmask': '255.255.252.0'}]},
- {'type': 'route', 'destination': '10.20.84.0/22',
- 'gateway': '10.20.87.253', 'metric': 10000}],
+ 'address': '10.20.87.154', 'netmask': '255.255.252.0',
+ 'routes':
+ [{'type': 'route', 'destination': '10.20.84.0/22',
+ 'gateway': '10.20.87.253', 'metric': 10000}]}]}],
+ nc.generate())
+
+ def test_cust_non_primary_nic_with_gateway_(self):
+ """A customer non primary nic set can have a gateway."""
+ config = textwrap.dedent("""\
+ [NETWORK]
+ NETWORKING = yes
+ BOOTPROTO = dhcp
+ HOSTNAME = static-debug-vm
+ DOMAINNAME = cluster.local
+
+ [NIC-CONFIG]
+ NICS = NIC1
+
+ [NIC1]
+ MACADDR = 00:50:56:ac:d1:8a
+ ONBOOT = yes
+ IPv4_MODE = BACKWARDS_COMPATIBLE
+ BOOTPROTO = static
+ IPADDR = 100.115.223.75
+ NETMASK = 255.255.255.0
+ GATEWAY = 100.115.223.254
+
+
+ [DNS]
+ DNSFROMDHCP=no
+
+ NAMESERVER|1 = 8.8.8.8
+
+ [DATETIME]
+ UTC = yes
+ """)
+ nc = self._get_NicConfigurator(config)
+ self.assertEqual(
+ [{'type': 'physical', 'name': 'NIC1',
+ 'mac_address': '00:50:56:ac:d1:8a',
+ 'subnets': [
+ {'control': 'auto', 'type': 'static',
+ 'address': '100.115.223.75', 'netmask': '255.255.255.0',
+ 'routes':
+ [{'type': 'route', 'destination': '100.115.223.0/24',
+ 'gateway': '100.115.223.254', 'metric': 10000}]}]}],
nc.generate())
def test_a_primary_nic_with_gateway(self):
diff --git a/tox.ini b/tox.ini
index 2fb3209..d983348 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,7 +21,7 @@ setenv =
basepython = python3
deps =
# requirements
- pylint==1.8.1
+ pylint==2.2.2
# test-requirements because unit tests are now present in cloudinit tree
-r{toxinidir}/test-requirements.txt
commands = {envpython} -m pylint {posargs:cloudinit tests tools}
Follow ups