summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--cloudinit/config/cc_mcollective.py72
-rw-r--r--cloudinit/distros/__init__.py28
-rw-r--r--cloudinit/distros/debian.py3
-rw-r--r--cloudinit/net/__init__.py3
-rw-r--r--cloudinit/net/eni.py182
-rw-r--r--cloudinit/sources/DataSourceConfigDrive.py26
-rw-r--r--cloudinit/sources/DataSourceSmartOS.py8
-rw-r--r--cloudinit/sources/helpers/openstack.py8
-rw-r--r--packages/redhat/cloud-init.spec.in4
-rw-r--r--tests/unittests/helpers.py12
-rw-r--r--tests/unittests/test_distros/test_netconfig.py60
-rw-r--r--tests/unittests/test_handler/test_handler_mcollective.py60
-rw-r--r--tests/unittests/test_net.py358
14 files changed, 710 insertions, 115 deletions
diff --git a/ChangeLog b/ChangeLog
index fa5e7df4..bae982e3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -126,6 +126,7 @@
- support network rendering to sysconfig (for centos and RHEL)
- write_files: if no permissions are given, just use default without warn.
- user_data: fix error when user-data is not utf-8 decodable (LP: #1532072)
+ - fix mcollective module with python3 (LP: #1597699) [Sergii Golovatiuk]
0.7.6:
- open 0.7.6
diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py
index 425420ae..0c84d600 100644
--- a/cloudinit/config/cc_mcollective.py
+++ b/cloudinit/config/cc_mcollective.py
@@ -20,54 +20,49 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import six
-from six import StringIO
+from six import BytesIO
# Used since this can maintain comments
# and doesn't need a top level section
from configobj import ConfigObj
+from cloudinit import log as logging
from cloudinit import util
PUBCERT_FILE = "/etc/mcollective/ssl/server-public.pem"
PRICERT_FILE = "/etc/mcollective/ssl/server-private.pem"
SERVER_CFG = '/etc/mcollective/server.cfg'
+LOG = logging.getLogger(__name__)
-def handle(name, cfg, cloud, log, _args):
-
- # If there isn't a mcollective key in the configuration don't do anything
- if 'mcollective' not in cfg:
- log.debug(("Skipping module named %s, "
- "no 'mcollective' key in configuration"), name)
- return
-
- mcollective_cfg = cfg['mcollective']
-
- # Start by installing the mcollective package ...
- cloud.distro.install_packages(("mcollective",))
- # ... and then update the mcollective configuration
- if 'conf' in mcollective_cfg:
- # Read server.cfg values from the
- # original file in order to be able to mix the rest up
- mcollective_config = ConfigObj(SERVER_CFG)
- # See: http://tiny.cc/jh9agw
- for (cfg_name, cfg) in mcollective_cfg['conf'].items():
+def configure(config):
+ # Read server.cfg values from the
+ # original file in order to be able to mix the rest up
+ try:
+ mcollective_config = ConfigObj(SERVER_CFG, file_error=True)
+ except IOError:
+ LOG.warn("Did not find file %s", SERVER_CFG)
+ mcollective_config = ConfigObj(config)
+ else:
+ for (cfg_name, cfg) in config.items():
if cfg_name == 'public-cert':
util.write_file(PUBCERT_FILE, cfg, mode=0o644)
- mcollective_config['plugin.ssl_server_public'] = PUBCERT_FILE
+ mcollective_config[
+ 'plugin.ssl_server_public'] = PUBCERT_FILE
mcollective_config['securityprovider'] = 'ssl'
elif cfg_name == 'private-cert':
util.write_file(PRICERT_FILE, cfg, mode=0o600)
- mcollective_config['plugin.ssl_server_private'] = PRICERT_FILE
+ mcollective_config[
+ 'plugin.ssl_server_private'] = PRICERT_FILE
mcollective_config['securityprovider'] = 'ssl'
else:
if isinstance(cfg, six.string_types):
# Just set it in the 'main' section
mcollective_config[cfg_name] = cfg
elif isinstance(cfg, (dict)):
- # Iterate through the config items, create a section
- # if it is needed and then add/or create items as needed
+ # Iterate through the config items, create a section if
+ # it is needed and then add/or create items as needed
if cfg_name not in mcollective_config.sections:
mcollective_config[cfg_name] = {}
for (o, v) in cfg.items():
@@ -78,11 +73,30 @@ def handle(name, cfg, cloud, log, _args):
# We got all our config as wanted we'll rename
# the previous server.cfg and create our new one
util.rename(SERVER_CFG, "%s.old" % (SERVER_CFG))
- # Now we got the whole file, write to disk...
- contents = StringIO()
- mcollective_config.write(contents)
- contents = contents.getvalue()
- util.write_file(SERVER_CFG, contents, mode=0o644)
+
+ # Now we got the whole file, write to disk...
+ contents = BytesIO()
+ mcollective_config.write(contents)
+ contents = contents.getvalue()
+ util.write_file(SERVER_CFG, contents, mode=0o644)
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ # If there isn't a mcollective key in the configuration don't do anything
+ if 'mcollective' not in cfg:
+ log.debug(("Skipping module named %s, "
+ "no 'mcollective' key in configuration"), name)
+ return
+
+ mcollective_cfg = cfg['mcollective']
+
+ # Start by installing the mcollective package ...
+ cloud.distro.install_packages(("mcollective",))
+
+ # ... and then update the mcollective configuration
+ if 'conf' in mcollective_cfg:
+ configure(config=mcollective_cfg['conf'])
# Start mcollective
util.subp(['service', 'mcollective', 'start'], capture=False)
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 14b500f8..40af8802 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -32,6 +32,8 @@ import stat
from cloudinit import importer
from cloudinit import log as logging
from cloudinit import net
+from cloudinit.net import eni
+from cloudinit.net import network_state
from cloudinit import ssh_util
from cloudinit import type_utils
from cloudinit import util
@@ -138,9 +140,31 @@ class Distro(object):
return self._bring_up_interfaces(dev_names)
return False
+ def _apply_network_from_network_config(self, netconfig, bring_up=True):
+ distro = self.__class__
+ LOG.warn("apply_network_config is not currently implemented "
+ "for distribution '%s'. Attempting to use apply_network",
+ distro)
+ header = '\n'.join([
+ "# Converted from network_config for distro %s" % distro,
+ "# Implmentation of _write_network_config is needed."
+ ])
+ ns = network_state.parse_net_config_data(netconfig)
+ contents = eni.network_state_to_eni(
+ ns, header=header, render_hwaddress=True)
+ return self.apply_network(contents, bring_up=bring_up)
+
def apply_network_config(self, netconfig, bring_up=False):
- # Write it out
- dev_names = self._write_network_config(netconfig)
+ # apply network config netconfig
+ # This method is preferred to apply_network which only takes
+ # a much less complete network config format (interfaces(5)).
+ try:
+ dev_names = self._write_network_config(netconfig)
+ except NotImplementedError:
+ # backwards compat until all distros have apply_network_config
+ return self._apply_network_from_network_config(
+ netconfig, bring_up=bring_up)
+
# Now try to bring them up
if bring_up:
return self._bring_up_interfaces(dev_names)
diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index 5ae9a509..f9b3b92e 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -55,7 +55,6 @@ class Distro(distros.Distro):
hostname_conf_fn = "/etc/hostname"
locale_conf_fn = "/etc/default/locale"
network_conf_fn = "/etc/network/interfaces.d/50-cloud-init.cfg"
- links_prefix = "/etc/systemd/network/50-cloud-init-"
def __init__(self, name, cfg, paths):
distros.Distro.__init__(self, name, cfg, paths)
@@ -67,7 +66,7 @@ class Distro(distros.Distro):
self._net_renderer = eni.Renderer({
'eni_path': self.network_conf_fn,
'eni_header': ENI_HEADER,
- 'links_prefix_path': None,
+ 'links_path_prefix': None,
'netrules_path': None,
})
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 63e54f91..21cc602b 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -252,7 +252,8 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True,
cur_bymac[mac] = cur
def update_byname(bymac):
- return {data['name']: data for data in bymac.values()}
+ return dict((data['name'], data)
+ for data in bymac.values())
def rename(cur, new):
util.subp(["ip", "link", "set", cur, "name", new], capture=True)
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index e5ed10fd..eff5b924 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -12,6 +12,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import copy
import glob
import os
import re
@@ -42,7 +43,7 @@ NET_CONFIG_OPTIONS = [
# TODO: switch valid_map based on mode inet/inet6
def _iface_add_subnet(iface, subnet):
- content = ""
+ content = []
valid_map = [
'address',
'netmask',
@@ -61,15 +62,21 @@ def _iface_add_subnet(iface, subnet):
value = " ".join(value)
if '_' in key:
key = key.replace('_', '-')
- content += " {} {}\n".format(key, value)
+ content.append(" {0} {1}".format(key, value))
- return content
+ return sorted(content)
# TODO: switch to valid_map for attrs
-
-def _iface_add_attrs(iface):
- content = ""
+def _iface_add_attrs(iface, index):
+ # If the index is non-zero, this is an alias interface. Alias interfaces
+ # represent additional interface addresses, and should not have additional
+ # attributes. (extra attributes here are almost always either incorrect,
+ # or are applied to the parent interface.) So if this is an alias, stop
+ # right here.
+ if index != 0:
+ return []
+ content = []
ignore_map = [
'control',
'index',
@@ -79,19 +86,21 @@ def _iface_add_attrs(iface):
'subnets',
'type',
]
+ renames = {'mac_address': 'hwaddress'}
if iface['type'] not in ['bond', 'bridge', 'vlan']:
ignore_map.append('mac_address')
for key, value in iface.items():
- if value and key not in ignore_map:
- if type(value) == list:
- value = " ".join(value)
- content += " {} {}\n".format(key, value)
+ if not value or key in ignore_map:
+ continue
+ if type(value) == list:
+ value = " ".join(value)
+ content.append(" {0} {1}".format(renames.get(key, key), value))
- return content
+ return sorted(content)
-def _iface_start_entry(iface, index):
+def _iface_start_entry(iface, index, render_hwaddress=False):
fullname = iface['name']
if index != 0:
fullname += ":%s" % index
@@ -107,8 +116,13 @@ def _iface_start_entry(iface, index):
subst = iface.copy()
subst.update({'fullname': fullname, 'cverb': cverb})
- return ("{cverb} {fullname}\n"
- "iface {fullname} {inet} {mode}\n").format(**subst)
+ lines = [
+ "{cverb} {fullname}".format(**subst),
+ "iface {fullname} {inet} {mode}".format(**subst)]
+ if render_hwaddress and iface.get('mac_address'):
+ lines.append(" hwaddress {mac_address}".format(**subst))
+
+ return lines
def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
@@ -262,10 +276,6 @@ def _ifaces_to_net_config_data(ifaces):
for name, data in ifaces.items():
# devname is 'eth0' for name='eth0:1'
devname = name.partition(":")[0]
- if devname == "lo":
- # currently provding 'lo' in network config results in duplicate
- # entries. in rendered interfaces file. so skip it.
- continue
if devname not in devs:
devs[devname] = {'type': 'physical', 'name': devname,
'subnets': []}
@@ -324,10 +334,10 @@ class Renderer(renderer.Renderer):
1. http://askubuntu.com/questions/168033/
how-to-set-static-routes-in-ubuntu-server
"""
- content = ""
+ content = []
up = indent + "post-up route add"
down = indent + "pre-down route del"
- eol = " || true\n"
+ or_true = " || true"
mapping = {
'network': '-net',
'netmask': 'netmask',
@@ -336,34 +346,84 @@ class Renderer(renderer.Renderer):
}
if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
default_gw = " default gw %s" % route['gateway']
- content += up + default_gw + eol
- content += down + default_gw + eol
+ content.append(up + default_gw + or_true)
+ content.append(down + default_gw + or_true)
elif route['network'] == '::' and route['netmask'] == 0:
# ipv6!
default_gw = " -A inet6 default gw %s" % route['gateway']
- content += up + default_gw + eol
- content += down + default_gw + eol
+ 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 += up + route_line + eol
- content += down + route_line + eol
+ content.append(up + route_line + or_true)
+ content.append(down + route_line + or_true)
return content
- def _render_interfaces(self, network_state):
+ def _render_iface(self, iface, render_hwaddress=False):
+ sections = []
+ subnets = iface.get('subnets', {})
+ if subnets:
+ for index, subnet in zip(range(0, len(subnets)), subnets):
+ iface['index'] = index
+ iface['mode'] = subnet['type']
+ iface['control'] = subnet.get('control', 'auto')
+ subnet_inet = 'inet'
+ if iface['mode'].endswith('6'):
+ # This is a request for DHCPv6.
+ subnet_inet += '6'
+ elif iface['mode'] == 'static' and ":" in subnet['address']:
+ # This is a static IPv6 address.
+ subnet_inet += '6'
+ iface['inet'] = subnet_inet
+ if iface['mode'].startswith('dhcp'):
+ iface['mode'] = 'dhcp'
+
+ lines = list(
+ _iface_start_entry(
+ iface, index, render_hwaddress=render_hwaddress) +
+ _iface_add_subnet(iface, subnet) +
+ _iface_add_attrs(iface, index)
+ )
+ for route in subnet.get('routes', []):
+ lines.extend(self._render_route(route, indent=" "))
+
+ if len(subnets) > 1 and index == 0:
+ tmpl = " post-up ifup %s:%s\n"
+ for i in range(1, len(subnets)):
+ lines.append(tmpl % (iface['name'], i))
+
+ sections.append(lines)
+ else:
+ # ifenslave docs say to auto the slave devices
+ lines = []
+ if 'bond-master' in iface:
+ lines.append("auto {name}".format(**iface))
+ lines.append("iface {name} {inet} {mode}".format(**iface))
+ lines.extend(_iface_add_attrs(iface, index=0))
+ sections.append(lines)
+ return sections
+
+ def _render_interfaces(self, network_state, render_hwaddress=False):
'''Given state, emit etc/network/interfaces content.'''
- content = ""
- content += "auto lo\niface lo inet loopback\n"
+ # handle 'lo' specifically as we need to insert the global dns entries
+ # there (as that is the only interface that will be always up).
+ lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet',
+ 'subnets': [{'type': 'loopback', 'control': 'auto'}]}
+ for iface in network_state.iter_interfaces():
+ if iface.get('name') == "lo":
+ lo = copy.deepcopy(iface)
nameservers = network_state.dns_nameservers
if nameservers:
- content += " dns-nameservers %s\n" % (" ".join(nameservers))
+ lo['subnets'][0]["dns_nameservers"] = (" ".join(nameservers))
+
searchdomains = network_state.dns_searchdomains
if searchdomains:
- content += " dns-search %s\n" % (" ".join(searchdomains))
+ lo['subnets'][0]["dns_search"] = (" ".join(searchdomains))
''' Apply a sort order to ensure that we write out
the physical interfaces first; this is critical for
@@ -375,45 +435,21 @@ class Renderer(renderer.Renderer):
'bridge': 2,
'vlan': 3,
}
+
+ sections = []
+ sections.extend(self._render_iface(lo))
for iface in sorted(network_state.iter_interfaces(),
key=lambda k: (order[k['type']], k['name'])):
- if content[-2:] != "\n\n":
- content += "\n"
- subnets = iface.get('subnets', {})
- if subnets:
- for index, subnet in zip(range(0, len(subnets)), subnets):
- if content[-2:] != "\n\n":
- content += "\n"
- iface['index'] = index
- iface['mode'] = subnet['type']
- iface['control'] = subnet.get('control', 'auto')
- if iface['mode'].endswith('6'):
- iface['inet'] += '6'
- elif (iface['mode'] == 'static' and
- ":" in subnet['address']):
- iface['inet'] += '6'
- if iface['mode'].startswith('dhcp'):
- iface['mode'] = 'dhcp'
-
- content += _iface_start_entry(iface, index)
- content += _iface_add_subnet(iface, subnet)
- content += _iface_add_attrs(iface)
- for route in subnet.get('routes', []):
- content += self._render_route(route, indent=" ")
- else:
- # ifenslave docs say to auto the slave devices
- if 'bond-master' in iface:
- content += "auto {name}\n".format(**iface)
- content += "iface {name} {inet} {mode}\n".format(**iface)
- content += _iface_add_attrs(iface)
+ if iface.get('name') == "lo":
+ continue
+ sections.extend(
+ self._render_iface(iface, render_hwaddress=render_hwaddress))
for route in network_state.iter_routes():
- content += self._render_route(route)
+ sections.append(self._render_route(route))
- # global replacements until v2 format
- content = content.replace('mac_address', 'hwaddress')
- return content
+ return '\n\n'.join(['\n'.join(s) for s in sections]) + "\n"
def render_network_state(self, target, network_state):
fpeni = os.path.join(target, self.eni_path)
@@ -448,3 +484,21 @@ class Renderer(renderer.Renderer):
""
])
util.write_file(fname, content)
+
+
+def network_state_to_eni(network_state, header=None, render_hwaddress=False):
+ # render the provided network state, return a string of equivalent eni
+ eni_path = 'etc/network/interfaces'
+ renderer = Renderer({
+ 'eni_path': eni_path,
+ 'eni_header': header,
+ 'links_path_prefix': None,
+ 'netrules_path': None,
+ })
+ if not header:
+ header = ""
+ if not header.endswith("\n"):
+ header += "\n"
+ contents = renderer._render_interfaces(
+ network_state, render_hwaddress=render_hwaddress)
+ return header + contents
diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py
index 3130e618..91d6ff13 100644
--- a/cloudinit/sources/DataSourceConfigDrive.py
+++ b/cloudinit/sources/DataSourceConfigDrive.py
@@ -107,12 +107,19 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
if self.dsmode == sources.DSMODE_DISABLED:
return False
- # This is legacy and sneaky. If dsmode is 'pass' then write
- # 'injected files' and apply legacy ENI network format.
prev_iid = get_previous_iid(self.paths)
cur_iid = md['instance-id']
- if prev_iid != cur_iid and self.dsmode == sources.DSMODE_PASS:
- on_first_boot(results, distro=self.distro)
+ if prev_iid != cur_iid:
+ # better would be to handle this centrally, allowing
+ # the datasource to do something on new instance id
+ # note, networking is only rendered here if dsmode is DSMODE_PASS
+ # which means "DISABLED, but render files and networking"
+ on_first_boot(results, distro=self.distro,
+ network=self.dsmode == sources.DSMODE_PASS)
+
+ # This is legacy and sneaky. If dsmode is 'pass' then do not claim
+ # the datasource was used, even though we did run on_first_boot above.
+ if self.dsmode == sources.DSMODE_PASS:
LOG.debug("%s: not claiming datasource, dsmode=%s", self,
self.dsmode)
return False
@@ -184,15 +191,16 @@ def get_previous_iid(paths):
return None
-def on_first_boot(data, distro=None):
+def on_first_boot(data, distro=None, network=True):
"""Performs any first-boot actions using data read from a config-drive."""
if not isinstance(data, dict):
raise TypeError("Config-drive data expected to be a dict; not %s"
% (type(data)))
- net_conf = data.get("network_config", '')
- if net_conf and distro:
- LOG.warn("Updating network interfaces from config drive")
- distro.apply_network(net_conf)
+ if network:
+ net_conf = data.get("network_config", '')
+ if net_conf and distro:
+ LOG.warn("Updating network interfaces from config drive")
+ distro.apply_network(net_conf)
write_injected_files(data.get('files'))
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index 08bc132b..ccc86883 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -718,8 +718,8 @@ def convert_smartos_network_data(network_data=None):
config = []
for nic in network_data:
- cfg = {k: v for k, v in nic.items()
- if k in valid_keys['physical']}
+ cfg = dict((k, v) for k, v in nic.items()
+ if k in valid_keys['physical'])
cfg.update({
'type': 'physical',
'name': nic['interface']})
@@ -728,8 +728,8 @@ def convert_smartos_network_data(network_data=None):
subnets = []
for ip, gw in zip(nic['ips'], nic['gateways']):
- subnet = {k: v for k, v in nic.items()
- if k in valid_keys['subnet']}
+ subnet = dict((k, v) for k, v in nic.items()
+ if k in valid_keys['subnet'])
subnet.update({
'type': 'static',
'address': ip,
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index d52cb56a..2e7a1d47 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -542,8 +542,8 @@ def convert_net_json(network_json=None, known_macs=None):
config = []
for link in links:
subnets = []
- cfg = {k: v for k, v in link.items()
- if k in valid_keys['physical']}
+ cfg = dict((k, v) for k, v in link.items()
+ if k in valid_keys['physical'])
# 'name' is not in openstack spec yet, but we will support it if it is
# present. The 'id' in the spec is currently implemented as the host
# nic's name, meaning something like 'tap-adfasdffd'. We do not want
@@ -553,8 +553,8 @@ def convert_net_json(network_json=None, known_macs=None):
for network in [n for n in networks
if n['link'] == link['id']]:
- subnet = {k: v for k, v in network.items()
- if k in valid_keys['subnet']}
+ subnet = dict((k, v) for k, v in network.items()
+ if k in valid_keys['subnet'])
if 'dhcp' in network['type']:
t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
subnet.update({
diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
index 81cccce5..254d209b 100644
--- a/packages/redhat/cloud-init.spec.in
+++ b/packages/redhat/cloud-init.spec.in
@@ -160,6 +160,8 @@ fi
%files
+/lib/udev/rules.d/66-azure-ephemeral.rules
+
#if $sysvinit
%attr(0755, root, root) %{_initddir}/cloud-config
%attr(0755, root, root) %{_initddir}/cloud-final
@@ -168,6 +170,8 @@ fi
#end if
#if $systemd
+/usr/lib/systemd/system-generators/cloud-init-generator
+%{_unitdir}/cloud-*
%{_unitdir}/cloud-*
#end if
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index 8d46a8bf..972245df 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -264,6 +264,18 @@ def populate_dir(path, files):
fp.close()
+def dir2dict(startdir, prefix=None):
+ flist = {}
+ if prefix is None:
+ prefix = startdir
+ for root, dirs, files in os.walk(startdir):
+ for fname in files:
+ fpath = os.path.join(root, fname)
+ key = fpath[len(prefix):]
+ flist[key] = util.load_file(fpath)
+ return flist
+
+
try:
skipIf = unittest.skipIf
except AttributeError:
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index 9172e3aa..36eae2dc 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -319,3 +319,63 @@ defaultrouter="192.168.1.254"
'''
self.assertCfgEquals(expected_buf, str(write_buf))
self.assertEqual(write_buf.mode, 0o644)
+
+ def test_apply_network_config_fallback(self):
+ fbsd_distro = self._get_distro('freebsd')
+
+ # a weak attempt to verify that we don't have an implementation
+ # of _write_network_config or apply_network_config in fbsd now,
+ # which would make this test not actually test the fallback.
+ self.assertRaises(
+ NotImplementedError, fbsd_distro._write_network_config,
+ BASE_NET_CFG)
+
+ # now run
+ mynetcfg = {
+ 'config': [{"type": "physical", "name": "eth0",
+ "mac_address": "c0:d6:9f:2c:e8:80",
+ "subnets": [{"type": "dhcp"}]}],
+ 'version': 1}
+
+ write_bufs = {}
+ read_bufs = {
+ '/etc/rc.conf': '',
+ '/etc/resolv.conf': '',
+ }
+
+ def replace_write(filename, content, mode=0o644, omode="wb"):
+ buf = WriteBuffer()
+ buf.mode = mode
+ buf.omode = omode
+ buf.write(content)
+ write_bufs[filename] = buf
+
+ def replace_read(fname, read_cb=None, quiet=False):
+ if fname not in read_bufs:
+ if fname in write_bufs:
+ return str(write_bufs[fname])
+ raise IOError("%s not found" % fname)
+ else:
+ if fname in write_bufs:
+ return str(write_bufs[fname])
+ return read_bufs[fname]
+
+ with ExitStack() as mocks:
+ mocks.enter_context(
+ mock.patch.object(util, 'subp', return_value=('vtnet0', '')))
+ mocks.enter_context(
+ mock.patch.object(os.path, 'exists', return_value=False))
+ mocks.enter_context(
+ mock.patch.object(util, 'write_file', replace_write))
+ mocks.enter_context(
+ mock.patch.object(util, 'load_file', replace_read))
+
+ fbsd_distro.apply_network_config(mynetcfg, bring_up=False)
+
+ self.assertIn('/etc/rc.conf', write_bufs)
+ write_buf = write_bufs['/etc/rc.conf']
+ expected_buf = '''
+ifconfig_vtnet0="DHCP"
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
diff --git a/tests/unittests/test_handler/test_handler_mcollective.py b/tests/unittests/test_handler/test_handler_mcollective.py
new file mode 100644
index 00000000..f9448d80
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_mcollective.py
@@ -0,0 +1,60 @@
+from cloudinit.config import cc_mcollective
+from cloudinit import util
+
+from .. import helpers
+
+import configobj
+import logging
+import shutil
+from six import BytesIO
+import tempfile
+
+LOG = logging.getLogger(__name__)
+
+
+class TestConfig(helpers.FilesystemMockingTestCase):
+ def setUp(self):
+ super(TestConfig, self).setUp()
+ self.tmp = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.tmp)
+
+ def test_basic_config(self):
+ cfg = {
+ 'mcollective': {
+ 'conf': {
+ 'loglevel': 'debug',
+ 'connector': 'rabbitmq',
+ 'logfile': '/var/log/mcollective.log',
+ 'ttl': '4294957',
+ 'collectives': 'mcollective',
+ 'main_collective': 'mcollective',
+ 'securityprovider': 'psk',
+ 'daemonize': '1',
+ 'factsource': 'yaml',
+ 'direct_addressing': '1',
+ 'plugin.psk': 'unset',
+ 'libdir': '/usr/share/mcollective/plugins',
+ 'identity': '1',
+ },
+ },
+ }
+ self.patchUtils(self.tmp)
+ cc_mcollective.configure(cfg['mcollective']['conf'])
+ contents = util.load_file("/etc/mcollective/server.cfg", decode=False)
+ contents = configobj.ConfigObj(BytesIO(contents))
+ expected = {
+ 'loglevel': 'debug',
+ 'connector': 'rabbitmq',
+ 'logfile': '/var/log/mcollective.log',
+ 'ttl': '4294957',
+ 'collectives': 'mcollective',
+ 'main_collective': 'mcollective',
+ 'securityprovider': 'psk',
+ 'daemonize': '1',
+ 'factsource': 'yaml',
+ 'direct_addressing': '1',
+ 'plugin.psk': 'unset',
+ 'libdir': '/usr/share/mcollective/plugins',
+ 'identity': '1',
+ }
+ self.assertEqual(expected, dict(contents))
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 3ae00fc6..41b9a6d0 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -6,6 +6,7 @@ from cloudinit.net import sysconfig
from cloudinit.sources.helpers import openstack
from cloudinit import util
+from .helpers import dir2dict
from .helpers import mock
from .helpers import TestCase
@@ -17,6 +18,8 @@ import json
import os
import shutil
import tempfile
+import textwrap
+import yaml
DHCP_CONTENT_1 = """
DEVICE='eth0'
@@ -141,6 +144,283 @@ nameserver 172.19.0.12
}
]
+EXAMPLE_ENI = """
+auto lo
+iface lo inet loopback
+ dns-nameservers 10.0.0.1
+ dns-search foo.com
+
+auto eth0
+iface eth0 inet static
+ address 1.2.3.12
+ netmask 255.255.255.248
+ broadcast 1.2.3.15
+ gateway 1.2.3.9
+ dns-nameservers 69.9.160.191 69.9.191.4
+auto eth1
+iface eth1 inet static
+ address 10.248.2.4
+ netmask 255.255.255.248
+ broadcast 10.248.2.7
+"""
+
+RENDERED_ENI = """
+auto lo
+iface lo inet loopback
+ dns-nameservers 10.0.0.1
+ dns-search foo.com
+
+auto eth0
+iface eth0 inet static
+ address 1.2.3.12
+ broadcast 1.2.3.15
+ dns-nameservers 69.9.160.191 69.9.191.4
+ gateway 1.2.3.9
+ netmask 255.255.255.248
+
+auto eth1
+iface eth1 inet static
+ address 10.248.2.4
+ broadcast 10.248.2.7
+ netmask 255.255.255.248
+""".lstrip()
+
+NETWORK_CONFIGS = {
+ 'small': {
+ 'expected_eni': textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+ dns-nameservers 1.2.3.4 5.6.7.8
+ dns-search wark.maas
+
+ iface eth1 inet manual
+
+ auto eth99
+ iface eth99 inet dhcp
+ post-up ifup eth99:1
+
+
+ auto eth99:1
+ iface eth99:1 inet static
+ 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
+ """).rstrip(' '),
+ 'yaml': textwrap.dedent("""
+ version: 1
+ config:
+ # Physical interfaces.
+ - type: physical
+ name: eth99
+ mac_address: "c0:d6:9f:2c:e8:80"
+ subnets:
+ - type: dhcp4
+ - type: static
+ address: 192.168.21.3/24
+ dns_nameservers:
+ - 8.8.8.8
+ - 8.8.4.4
+ dns_search: barley.maas sach.maas
+ routes:
+ - gateway: 65.61.151.37
+ netmask: 0.0.0.0
+ network: 0.0.0.0
+ metric: 2
+ - type: physical
+ name: eth1
+ mac_address: "cf:d6:af:48:e8:80"
+ - type: nameserver
+ address:
+ - 1.2.3.4
+ - 5.6.7.8
+ search:
+ - wark.maas
+ """),
+ },
+ 'all': {
+ 'expected_eni': ("""\
+auto lo
+iface lo inet loopback
+ dns-nameservers 8.8.8.8 4.4.4.4 8.8.4.4
+ dns-search barley.maas wark.maas foobar.maas
+
+iface eth0 inet manual
+
+auto eth1
+iface eth1 inet manual
+ bond-master bond0
+ bond-mode active-backup
+
+auto eth2
+iface eth2 inet manual
+ bond-master bond0
+ bond-mode active-backup
+
+iface eth3 inet manual
+
+iface eth4 inet manual
+
+# control-manual eth5
+iface eth5 inet dhcp
+
+auto bond0
+iface bond0 inet6 dhcp
+ bond-mode active-backup
+ bond-slaves none
+ hwaddress aa:bb:cc:dd:ee:ff
+
+auto br0
+iface br0 inet static
+ address 192.168.14.2/24
+ bridge_ports eth3 eth4
+ bridge_stp off
+ post-up ifup br0:1
+
+
+auto br0:1
+iface br0:1 inet6 static
+ address 2001:1::1/64
+
+auto bond0.200
+iface bond0.200 inet dhcp
+ vlan-raw-device bond0
+ vlan_id 200
+
+auto eth0.101
+iface eth0.101 inet static
+ address 192.168.0.2/24
+ dns-nameservers 192.168.0.10 10.23.23.134
+ dns-search barley.maas sacchromyces.maas brettanomyces.maas
+ gateway 192.168.0.1
+ mtu 1500
+ vlan-raw-device eth0
+ vlan_id 101
+ post-up ifup eth0.101:1
+
+
+auto eth0.101:1
+iface eth0.101:1 inet static
+ address 192.168.2.10/24
+
+post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
+pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
+"""),
+ 'yaml': textwrap.dedent("""
+ version: 1
+ config:
+ # Physical interfaces.
+ - type: physical
+ name: eth0
+ mac_address: "c0:d6:9f:2c:e8:80"
+ - type: physical
+ name: eth1
+ mac_address: "aa:d6:9f:2c:e8:80"
+ - type: physical
+ name: eth2
+ mac_address: "c0:bb:9f:2c:e8:80"
+ - type: physical
+ name: eth3
+ mac_address: "66:bb:9f:2c:e8:80"
+ - type: physical
+ name: eth4
+ mac_address: "98:bb:9f:2c:e8:80"
+ # specify how ifupdown should treat iface
+ # control is one of ['auto', 'hotplug', 'manual']
+ # with manual meaning ifup/ifdown should not affect the iface
+ # useful for things like iscsi root + dhcp
+ - type: physical
+ name: eth5
+ mac_address: "98:bb:9f:2c:e8:8a"
+ subnets:
+ - type: dhcp
+ control: manual
+ # VLAN interface.
+ - type: vlan
+ name: eth0.101
+ vlan_link: eth0
+ vlan_id: 101
+ mtu: 1500
+ subnets:
+ - type: static
+ address: 192.168.0.2/24
+ gateway: 192.168.0.1
+ dns_nameservers:
+ - 192.168.0.10
+ - 10.23.23.134
+ dns_search:
+ - barley.maas
+ - sacchromyces.maas
+ - brettanomyces.maas
+ - type: static
+ address: 192.168.2.10/24
+ # Bond.
+ - type: bond
+ name: bond0
+ # if 'mac_address' is omitted, the MAC is taken from
+ # the first slave.
+ mac_address: "aa:bb:cc:dd:ee:ff"
+ bond_interfaces:
+ - eth1
+ - eth2
+ params:
+ bond-mode: active-backup
+ subnets:
+ - type: dhcp6
+ # A Bond VLAN.
+ - type: vlan
+ name: bond0.200
+ vlan_link: bond0
+ vlan_id: 200
+ subnets:
+ - type: dhcp4
+ # A bridge.
+ - type: bridge
+ name: br0
+ bridge_interfaces:
+ - eth3
+ - eth4
+ ipv4_conf:
+ rp_filter: 1
+ proxy_arp: 0
+ forwarding: 1
+ ipv6_conf:
+ autoconf: 1
+ disable_ipv6: 1
+ use_tempaddr: 1
+ forwarding: 1
+ # basically anything in /proc/sys/net/ipv6/conf/.../
+ params:
+ bridge_stp: 'off'
+ bridge_fd: 0
+ bridge_maxwait: 0
+ subnets:
+ - type: static
+ address: 192.168.14.2/24
+ - type: static
+ address: 2001:1::1/64 # default to /64
+ # A global nameserver.
+ - type: nameserver
+ address: 8.8.8.8
+ search: barley.maas
+ # global nameservers and search in list form
+ - type: nameserver
+ address:
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - wark.maas
+ - foobar.maas
+ # A global route.
+ - type: route
+ destination: 10.0.0.0/8
+ gateway: 11.0.0.1
+ metric: 3
+ """).lstrip(),
+ }
+}
+
def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info,
mock_sys_dev_path):
@@ -269,6 +549,37 @@ iface eth1000 inet dhcp
self.assertEqual(expected.lstrip(), contents.lstrip())
+class TestEniNetworkStateToEni(TestCase):
+ mycfg = {
+ 'config': [{"type": "physical", "name": "eth0",
+ "mac_address": "c0:d6:9f:2c:e8:80",
+ "subnets": [{"type": "dhcp"}]}],
+ 'version': 1}
+ my_mac = 'c0:d6:9f:2c:e8:80'
+
+ def test_no_header(self):
+ rendered = eni.network_state_to_eni(
+ network_state=network_state.parse_net_config_data(self.mycfg),
+ render_hwaddress=True)
+ self.assertIn(self.my_mac, rendered)
+ self.assertIn("hwaddress", rendered)
+
+ def test_with_header(self):
+ header = "# hello world\n"
+ rendered = eni.network_state_to_eni(
+ network_state=network_state.parse_net_config_data(self.mycfg),
+ header=header, render_hwaddress=True)
+ self.assertIn(header, rendered)
+ self.assertIn(self.my_mac, rendered)
+
+ def test_no_hwaddress(self):
+ rendered = eni.network_state_to_eni(
+ network_state=network_state.parse_net_config_data(self.mycfg),
+ render_hwaddress=False)
+ self.assertNotIn(self.my_mac, rendered)
+ self.assertNotIn("hwaddress", rendered)
+
+
class TestCmdlineConfigParsing(TestCase):
simple_cfg = {
'config': [{"type": "physical", "name": "eth0",
@@ -323,6 +634,53 @@ class TestCmdlineConfigParsing(TestCase):
self.assertEqual(found, self.simple_cfg)
+class TestEniRoundTrip(TestCase):
+ def setUp(self):
+ super(TestCase, self).setUp()
+ self.tmp_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.tmp_dir)
+
+ def _render_and_read(self, network_config=None, state=None, eni_path=None,
+ links_prefix=None, netrules_path=None):
+ if network_config:
+ ns = network_state.parse_net_config_data(network_config)
+ elif state:
+ ns = state
+ else:
+ raise ValueError("Expected data or state, got neither")
+
+ if eni_path is None:
+ eni_path = 'etc/network/interfaces'
+
+ renderer = eni.Renderer(
+ config={'eni_path': eni_path, 'links_path_prefix': links_prefix,
+ 'netrules_path': netrules_path})
+
+ renderer.render_network_state(self.tmp_dir, ns)
+ return dir2dict(self.tmp_dir)
+
+ def testsimple_convert_and_render(self):
+ network_config = eni.convert_eni_data(EXAMPLE_ENI)
+ files = self._render_and_read(network_config=network_config)
+ self.assertEqual(
+ RENDERED_ENI.splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+ def testsimple_render_all(self):
+ entry = NETWORK_CONFIGS['all']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+ def testsimple_render_small(self):
+ entry = NETWORK_CONFIGS['small']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+
def _gzip_data(data):
with io.BytesIO() as iobuf:
gzfp = gzip.GzipFile(mode="wb", fileobj=iobuf)