summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/distros/debian.py12
-rw-r--r--cloudinit/distros/rhel.py8
-rw-r--r--cloudinit/net/__init__.py1
-rw-r--r--cloudinit/net/eni.py69
-rw-r--r--cloudinit/net/network_state.py104
-rw-r--r--cloudinit/net/renderer.py48
-rw-r--r--cloudinit/net/sysconfig.py400
-rw-r--r--tests/unittests/test_net.py182
8 files changed, 722 insertions, 102 deletions
diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index 603b0b61..53f3aa4d 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -57,7 +57,11 @@ class Distro(distros.Distro):
# should only happen say once per instance...)
self._runner = helpers.Runners(paths)
self.osfamily = 'debian'
- self._net_renderer = eni.Renderer()
+ self._net_renderer = eni.Renderer({
+ 'eni_path': self.network_conf_fn,
+ 'links_prefix_path': self.links_prefix,
+ 'netrules_path': None,
+ })
def apply_locale(self, locale, out_fn=None):
if not out_fn:
@@ -82,12 +86,8 @@ class Distro(distros.Distro):
def _write_network_config(self, netconfig):
ns = parse_net_config_data(netconfig)
- self._net_renderer.render_network_state(
- target="/", network_state=ns,
- eni=self.network_conf_fn, links_prefix=self.links_prefix,
- netrules=None)
+ self._net_renderer.render_network_state("/", ns)
_maybe_remove_legacy_eth0()
-
return []
def _bring_up_interfaces(self, device_names):
diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py
index 812e7002..1aa42d75 100644
--- a/cloudinit/distros/rhel.py
+++ b/cloudinit/distros/rhel.py
@@ -23,6 +23,8 @@
from cloudinit import distros
from cloudinit import helpers
from cloudinit import log as logging
+from cloudinit.net.network_state import parse_net_config_data
+from cloudinit.net import sysconfig
from cloudinit import util
from cloudinit.distros import net_util
@@ -59,10 +61,16 @@ class Distro(distros.Distro):
# should only happen say once per instance...)
self._runner = helpers.Runners(paths)
self.osfamily = 'redhat'
+ self._net_renderer = sysconfig.Renderer()
def install_packages(self, pkglist):
self.package_command('install', pkgs=pkglist)
+ def _write_network_config(self, netconfig):
+ ns = parse_net_config_data(netconfig)
+ self._net_renderer.render_network_state("/", ns)
+ return []
+
def _write_network(self, settings):
# TODO(harlowja) fix this... since this is the ubuntu format
entries = net_util.translate_network(settings)
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index f5668fff..6959ad34 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -26,7 +26,6 @@ from cloudinit import util
LOG = logging.getLogger(__name__)
SYS_CLASS_NET = "/sys/class/net/"
DEFAULT_PRIMARY_INTERFACE = 'eth0'
-LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-"
def sys_dev_path(devname, path=""):
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index 5a90eb32..ccd16ba7 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -16,10 +16,9 @@ import glob
import os
import re
-from . import LINKS_FNAME_PREFIX
from . import ParserError
-from .udev import generate_udev_rule
+from . import renderer
from cloudinit import util
@@ -297,21 +296,17 @@ def _ifaces_to_net_config_data(ifaces):
'config': [devs[d] for d in sorted(devs)]}
-class Renderer(object):
+class Renderer(renderer.Renderer):
"""Renders network information in a /etc/network/interfaces format."""
- def _render_persistent_net(self, network_state):
- """Given state, emit udev rules to map mac to ifname."""
- content = ""
- interfaces = network_state.get('interfaces')
- for iface in interfaces.values():
- # for physical interfaces write out a persist net udev rule
- if iface['type'] == 'physical' and \
- 'name' in iface and iface.get('mac_address'):
- content += generate_udev_rule(iface['name'],
- iface['mac_address'])
-
- return content
+ def __init__(self, config=None):
+ if not config:
+ config = {}
+ self.eni_path = config.get('eni_path', 'etc/network/interfaces')
+ self.links_path_prefix = config.get(
+ 'links_path_prefix', 'etc/systemd/network/50-cloud-init-')
+ self.netrules_path = config.get(
+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
def _render_route(self, route, indent=""):
"""When rendering routes for an iface, in some cases applying a route
@@ -360,7 +355,15 @@ class Renderer(object):
'''Given state, emit etc/network/interfaces content.'''
content = ""
- interfaces = network_state.get('interfaces')
+ content += "auto lo\niface lo inet loopback\n"
+
+ nameservers = network_state.dns_nameservers
+ if nameservers:
+ content += " dns-nameservers %s\n" % (" ".join(nameservers))
+ searchdomains = network_state.dns_searchdomains
+ if searchdomains:
+ content += " dns-search %s\n" % (" ".join(searchdomains))
+
''' Apply a sort order to ensure that we write out
the physical interfaces first; this is critical for
bonding
@@ -371,12 +374,7 @@ class Renderer(object):
'bridge': 2,
'vlan': 3,
}
- content += "auto lo\niface lo inet loopback\n"
- for dnskey, value in network_state.get('dns', {}).items():
- if len(value):
- content += " dns-{} {}\n".format(dnskey, " ".join(value))
-
- for iface in sorted(interfaces.values(),
+ for iface in sorted(network_state.iter_interfaces(),
key=lambda k: (order[k['type']], k['name'])):
if content[-2:] != "\n\n":
@@ -409,40 +407,33 @@ class Renderer(object):
content += "iface {name} {inet} {mode}\n".format(**iface)
content += _iface_add_attrs(iface)
- for route in network_state.get('routes'):
+ for route in network_state.iter_routes():
content += self._render_route(route)
# global replacements until v2 format
content = content.replace('mac_address', 'hwaddress')
return content
- def render_network_state(
- self, target, network_state, eni="etc/network/interfaces",
- links_prefix=LINKS_FNAME_PREFIX,
- netrules='etc/udev/rules.d/70-persistent-net.rules',
- writer=None):
-
- fpeni = os.path.sep.join((target, eni,))
+ def render_network_state(self, target, network_state):
+ fpeni = os.path.join(target, self.eni_path)
util.ensure_dir(os.path.dirname(fpeni))
util.write_file(fpeni, self._render_interfaces(network_state))
- if netrules:
- netrules = os.path.sep.join((target, netrules,))
+ if self.netrules_path:
+ netrules = os.path.join(target, self.netrules_path)
util.ensure_dir(os.path.dirname(netrules))
util.write_file(netrules,
self._render_persistent_net(network_state))
- if links_prefix:
+ if self.links_path_prefix:
self._render_systemd_links(target, network_state,
- links_prefix=links_prefix)
+ links_prefix=self.links_path_prefix)
- def _render_systemd_links(self, target, network_state,
- links_prefix=LINKS_FNAME_PREFIX):
- fp_prefix = os.path.sep.join((target, links_prefix))
+ def _render_systemd_links(self, target, network_state, links_prefix):
+ fp_prefix = os.path.join(target, links_prefix)
for f in glob.glob(fp_prefix + "*"):
os.unlink(f)
- interfaces = network_state.get('interfaces')
- for iface in interfaces.values():
+ for iface in network_state.iter_interfaces():
if (iface['type'] == 'physical' and 'name' in iface and
iface.get('mac_address')):
fname = fp_prefix + iface['name'] + ".link"
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index a8be5e26..8ca5106f 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -38,10 +38,10 @@ def parse_net_config_data(net_config, skip_broken=True):
"""
state = None
if 'version' in net_config and 'config' in net_config:
- ns = NetworkState(version=net_config.get('version'),
- config=net_config.get('config'))
- ns.parse_config(skip_broken=skip_broken)
- state = ns.network_state
+ nsi = NetworkStateInterpreter(version=net_config.get('version'),
+ config=net_config.get('config'))
+ nsi.parse_config(skip_broken=skip_broken)
+ state = nsi.network_state
return state
@@ -57,11 +57,10 @@ def parse_net_config(path, skip_broken=True):
def from_state_file(state_file):
- network_state = None
state = util.read_conf(state_file)
- network_state = NetworkState()
- network_state.load(state)
- return network_state
+ nsi = NetworkStateInterpreter()
+ nsi.load(state)
+ return nsi
def diff_keys(expected, actual):
@@ -113,9 +112,51 @@ class CommandHandlerMeta(type):
parents, dct)
-@six.add_metaclass(CommandHandlerMeta)
class NetworkState(object):
+ def __init__(self, network_state, version=NETWORK_STATE_VERSION):
+ self._network_state = copy.deepcopy(network_state)
+ self._version = version
+
+ @property
+ def version(self):
+ return self._version
+
+ def iter_routes(self, filter_func=None):
+ for route in self._network_state.get('routes', []):
+ if filter_func is not None:
+ if filter_func(route):
+ yield route
+ else:
+ yield route
+
+ @property
+ def dns_nameservers(self):
+ try:
+ return self._network_state['dns']['nameservers']
+ except KeyError:
+ return []
+
+ @property
+ def dns_searchdomains(self):
+ try:
+ return self._network_state['dns']['search']
+ except KeyError:
+ return []
+
+ def iter_interfaces(self, filter_func=None):
+ ifaces = self._network_state.get('interfaces', {})
+ for iface in six.itervalues(ifaces):
+ if filter_func is None:
+ yield iface
+ else:
+ if filter_func(iface):
+ yield iface
+
+
+@six.add_metaclass(CommandHandlerMeta)
+class NetworkStateInterpreter(object):
+
initial_network_state = {
'interfaces': {},
'routes': [],
@@ -126,22 +167,27 @@ class NetworkState(object):
}
def __init__(self, version=NETWORK_STATE_VERSION, config=None):
- self.version = version
- self.config = config
- self.network_state = copy.deepcopy(self.initial_network_state)
+ self._version = version
+ self._config = config
+ self._network_state = copy.deepcopy(self.initial_network_state)
+ self._parsed = False
+
+ @property
+ def network_state(self):
+ return NetworkState(self._network_state, version=self._version)
def dump(self):
state = {
- 'version': self.version,
- 'config': self.config,
- 'network_state': self.network_state,
+ 'version': self._version,
+ 'config': self._config,
+ 'network_state': self._network_state,
}
return util.yaml_dumps(state)
def load(self, state):
if 'version' not in state:
LOG.error('Invalid state, missing version field')
- raise Exception('Invalid state, missing version field')
+ raise ValueError('Invalid state, missing version field')
required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]
missing_keys = diff_keys(required_keys, state)
@@ -155,11 +201,11 @@ class NetworkState(object):
setattr(self, key, state[key])
def dump_network_state(self):
- return util.yaml_dumps(self.network_state)
+ return util.yaml_dumps(self._network_state)
def parse_config(self, skip_broken=True):
# rebuild network state
- for command in self.config:
+ for command in self._config:
command_type = command['type']
try:
handler = self.command_handlers[command_type]
@@ -189,7 +235,7 @@ class NetworkState(object):
}
'''
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
iface = interfaces.get(command['name'], {})
for param, val in command.get('params', {}).items():
iface.update({param: val})
@@ -215,7 +261,7 @@ class NetworkState(object):
'gateway': None,
'subnets': subnets,
})
- self.network_state['interfaces'].update({command.get('name'): iface})
+ self._network_state['interfaces'].update({command.get('name'): iface})
self.dump_network_state()
@ensure_command_keys(['name', 'vlan_id', 'vlan_link'])
@@ -228,7 +274,7 @@ class NetworkState(object):
hwaddress ether BC:76:4E:06:96:B3
vlan-raw-device eth0
'''
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
self.handle_physical(command)
iface = interfaces.get(command.get('name'), {})
iface['vlan-raw-device'] = command.get('vlan_link')
@@ -263,12 +309,12 @@ class NetworkState(object):
'''
self.handle_physical(command)
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces')
iface = interfaces.get(command.get('name'), {})
for param, val in command.get('params').items():
iface.update({param: val})
iface.update({'bond-slaves': 'none'})
- self.network_state['interfaces'].update({iface['name']: iface})
+ self._network_state['interfaces'].update({iface['name']: iface})
# handle bond slaves
for ifname in command.get('bond_interfaces'):
@@ -280,13 +326,13 @@ class NetworkState(object):
# inject placeholder
self.handle_physical(cmd)
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
bond_if = interfaces.get(ifname)
bond_if['bond-master'] = command.get('name')
# copy in bond config into slave
for param, val in command.get('params').items():
bond_if.update({param: val})
- self.network_state['interfaces'].update({ifname: bond_if})
+ self._network_state['interfaces'].update({ifname: bond_if})
@ensure_command_keys(['name', 'bridge_interfaces', 'params'])
def handle_bridge(self, command):
@@ -319,7 +365,7 @@ class NetworkState(object):
# find one of the bridge port ifaces to get mac_addr
# handle bridge_slaves
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
for ifname in command.get('bridge_interfaces'):
if ifname in interfaces:
continue
@@ -330,7 +376,7 @@ class NetworkState(object):
# inject placeholder
self.handle_physical(cmd)
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
self.handle_physical(command)
iface = interfaces.get(command.get('name'), {})
iface['bridge_ports'] = command['bridge_interfaces']
@@ -341,7 +387,7 @@ class NetworkState(object):
@ensure_command_keys(['address'])
def handle_nameserver(self, command):
- dns = self.network_state.get('dns')
+ dns = self._network_state.get('dns')
if 'address' in command:
addrs = command['address']
if not type(addrs) == list:
@@ -357,7 +403,7 @@ class NetworkState(object):
@ensure_command_keys(['destination'])
def handle_route(self, command):
- routes = self.network_state.get('routes')
+ routes = self._network_state.get('routes', [])
network, cidr = command['destination'].split("/")
netmask = cidr2mask(int(cidr))
route = {
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
new file mode 100644
index 00000000..310cbe0d
--- /dev/null
+++ b/cloudinit/net/renderer.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2013-2014 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Blake Rouse <blake.rouse@canonical.com>
+#
+# Curtin is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# Curtin 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 Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
+
+import six
+
+from .udev import generate_udev_rule
+
+
+def filter_by_type(match_type):
+ return lambda iface: match_type == iface['type']
+
+
+def filter_by_name(match_name):
+ return lambda iface: match_name == iface['name']
+
+
+filter_by_physical = filter_by_type('physical')
+
+
+class Renderer(object):
+
+ @staticmethod
+ def _render_persistent_net(network_state):
+ """Given state, emit udev rules to map mac to ifname."""
+ # TODO(harlowja): this seems shared between eni renderer and
+ # this, so move it to a shared location.
+ content = six.StringIO()
+ for iface in network_state.iter_interfaces(filter_by_physical):
+ # for physical interfaces write out a persist net udev rule
+ if 'name' in iface and iface.get('mac_address'):
+ content.write(generate_udev_rule(iface['name'],
+ iface['mac_address']))
+ return content.getvalue()
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
new file mode 100644
index 00000000..c53acf71
--- /dev/null
+++ b/cloudinit/net/sysconfig.py
@@ -0,0 +1,400 @@
+# vi: ts=4 expandtab
+#
+# 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/>.
+
+import os
+import re
+
+import six
+
+from cloudinit.distros.parsers import resolv_conf
+from cloudinit import util
+
+from . import renderer
+
+
+def _make_header(sep='#'):
+ lines = [
+ "Created by cloud-init on instance boot automatically, do not edit.",
+ "",
+ ]
+ for i in range(0, len(lines)):
+ if lines[i]:
+ lines[i] = sep + " " + lines[i]
+ else:
+ lines[i] = sep
+ return "\n".join(lines)
+
+
+def _is_default_route(route):
+ if route['network'] == '::' and route['netmask'] == 0:
+ return True
+ if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
+ return True
+ return False
+
+
+def _quote_value(value):
+ if re.search(r"\s", value):
+ # This doesn't handle complex cases...
+ if value.startswith('"') and value.endswith('"'):
+ return value
+ else:
+ return '"%s"' % value
+ else:
+ return value
+
+
+class ConfigMap(object):
+ """Sysconfig like dictionary object."""
+
+ # Why does redhat prefer yes/no to true/false??
+ _bool_map = {
+ True: 'yes',
+ False: 'no',
+ }
+
+ def __init__(self):
+ self._conf = {}
+
+ def __setitem__(self, key, value):
+ self._conf[key] = value
+
+ def drop(self, key):
+ self._conf.pop(key, None)
+
+ def __len__(self):
+ return len(self._conf)
+
+ def to_string(self):
+ buf = six.StringIO()
+ buf.write(_make_header())
+ if self._conf:
+ buf.write("\n")
+ for key in sorted(self._conf.keys()):
+ value = self._conf[key]
+ if isinstance(value, bool):
+ value = self._bool_map[value]
+ if not isinstance(value, six.string_types):
+ value = str(value)
+ buf.write("%s=%s\n" % (key, _quote_value(value)))
+ return buf.getvalue()
+
+
+class Route(ConfigMap):
+ """Represents a route configuration."""
+
+ route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
+
+ def __init__(self, route_name, base_sysconf_dir):
+ super(Route, self).__init__()
+ self.last_idx = 1
+ self.has_set_default = False
+ self._route_name = route_name
+ self._base_sysconf_dir = base_sysconf_dir
+
+ def copy(self):
+ r = Route(self._route_name, self._base_sysconf_dir)
+ r._conf = self._conf.copy()
+ r.last_idx = self.last_idx
+ r.has_set_default = self.has_set_default
+ return r
+
+ @property
+ def path(self):
+ return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
+ 'name': self._route_name})
+
+
+class NetInterface(ConfigMap):
+ """Represents a sysconfig/networking-script (and its config + children)."""
+
+ iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s'
+
+ iface_types = {
+ 'ethernet': 'Ethernet',
+ 'bond': 'Bond',
+ 'bridge': 'Bridge',
+ }
+
+ def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'):
+ super(NetInterface, self).__init__()
+ self.children = []
+ self.routes = Route(iface_name, base_sysconf_dir)
+ self._kind = kind
+ self._iface_name = iface_name
+ self._conf['DEVICE'] = iface_name
+ self._conf['TYPE'] = self.iface_types[kind]
+ self._base_sysconf_dir = base_sysconf_dir
+
+ @property
+ def name(self):
+ return self._iface_name
+
+ @name.setter
+ def name(self, iface_name):
+ self._iface_name = iface_name
+ self._conf['DEVICE'] = iface_name
+
+ @property
+ def kind(self):
+ return self._kind
+
+ @kind.setter
+ def kind(self, kind):
+ self._kind = kind
+ self._conf['TYPE'] = self.iface_types[kind]
+
+ @property
+ def path(self):
+ return self.iface_fn_tpl % ({'base': self._base_sysconf_dir,
+ 'name': self.name})
+
+ def copy(self, copy_children=False, copy_routes=False):
+ c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind)
+ c._conf = self._conf.copy()
+ if copy_children:
+ c.children = list(self.children)
+ if copy_routes:
+ c.routes = self.routes.copy()
+ return c
+
+
+class Renderer(renderer.Renderer):
+ """Renders network information in a /etc/sysconfig format."""
+
+ # See: https://access.redhat.com/documentation/en-US/\
+ # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
+ # s1-networkscripts-interfaces.html (or other docs for
+ # details about this)
+
+ iface_defaults = tuple([
+ ('ONBOOT', True),
+ ('USERCTL', False),
+ ('NM_CONTROLLED', False),
+ ('BOOTPROTO', 'none'),
+ ])
+
+ # If these keys exist, then there values will be used to form
+ # a BONDING_OPTS grouping; otherwise no grouping will be set.
+ bond_tpl_opts = tuple([
+ ('bond_mode', "mode=%s"),
+ ('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
+ ('bond_miimon', "miimon=%s"),
+ ])
+
+ bridge_opts_keys = tuple([
+ ('bridge_stp', 'STP'),
+ ('bridge_ageing', 'AGEING'),
+ ('bridge_bridgeprio', 'PRIO'),
+ ])
+
+ def __init__(self, config=None):
+ if not config:
+ config = {}
+ self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/')
+ self.netrules_path = config.get(
+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
+ self.dns_path = config.get('dns_path', 'etc/resolv.conf')
+
+ @classmethod
+ def _render_iface_shared(cls, iface, iface_cfg):
+ for k, v in cls.iface_defaults:
+ iface_cfg[k] = v
+ for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
+ old_value = iface.get(old_key)
+ if old_value is not None:
+ iface_cfg[new_key] = old_value
+
+ @classmethod
+ def _render_subnet(cls, iface_cfg, route_cfg, subnet):
+ subnet_type = subnet.get('type')
+ if subnet_type == 'dhcp6':
+ iface_cfg['DHCPV6C'] = True
+ iface_cfg['IPV6INIT'] = True
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ elif subnet_type in ['dhcp4', 'dhcp']:
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ elif subnet_type == 'static':
+ iface_cfg['BOOTPROTO'] = 'static'
+ if subnet.get('ipv6'):
+ iface_cfg['IPV6ADDR'] = subnet['address']
+ iface_cfg['IPV6INIT'] = True
+ else:
+ iface_cfg['IPADDR'] = subnet['address']
+ else:
+ raise ValueError("Unknown subnet type '%s' found"
+ " for interface '%s'" % (subnet_type,
+ iface_cfg.name))
+ if 'netmask' in subnet:
+ iface_cfg['NETMASK'] = subnet['netmask']
+ for route in subnet.get('routes', []):
+ if _is_default_route(route):
+ if route_cfg.has_set_default:
+ raise ValueError("Duplicate declaration of default"
+ " route found for interface '%s'"
+ % (iface_cfg.name))
+ # NOTE(harlowja): ipv6 and ipv4 default gateways
+ gw_key = 'GATEWAY0'
+ nm_key = 'NETMASK0'
+ addr_key = 'ADDRESS0'
+ # The owning interface provides the default route.
+ #
+ # TODO(harlowja): add validation that no other iface has
+ # also provided the default route?
+ iface_cfg['DEFROUTE'] = True
+ if 'gateway' in route:
+ iface_cfg['GATEWAY'] = route['gateway']
+ route_cfg.has_set_default = True
+ 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
+ route_cfg.last_idx += 1
+ for (old_key, new_key) in [('gateway', gw_key),
+ ('netmask', nm_key),
+ ('network', addr_key)]:
+ if old_key in route:
+ route_cfg[new_key] = route[old_key]
+
+ @classmethod
+ def _render_bonding_opts(cls, iface_cfg, iface):
+ bond_opts = []
+ for (bond_key, value_tpl) in cls.bond_tpl_opts:
+ # Seems like either dash or underscore is possible?
+ bond_keys = [bond_key, bond_key.replace("_", "-")]
+ for bond_key in bond_keys:
+ if bond_key in iface:
+ bond_value = iface[bond_key]
+ if isinstance(bond_value, (tuple, list)):
+ bond_value = " ".join(bond_value)
+ bond_opts.append(value_tpl % (bond_value))
+ break
+ if bond_opts:
+ iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
+
+ @classmethod
+ def _render_physical_interfaces(cls, network_state, iface_contents):
+ physical_filter = renderer.filter_by_physical
+ for iface in network_state.iter_interfaces(physical_filter):
+ iface_name = iface['name']
+ iface_subnets = iface.get("subnets", [])
+ iface_cfg = iface_contents[iface_name]
+ route_cfg = iface_cfg.routes
+ if len(iface_subnets) == 1:
+ cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
+ elif len(iface_subnets) > 1:
+ for i, iface_subnet in enumerate(iface_subnets,
+ start=len(iface.children)):
+ iface_sub_cfg = iface_cfg.copy()
+ iface_sub_cfg.name = "%s:%s" % (iface_name, i)
+ iface.children.append(iface_sub_cfg)
+ cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
+
+ @classmethod
+ def _render_bond_interfaces(cls, network_state, iface_contents):
+ bond_filter = renderer.filter_by_type('bond')
+ for iface in network_state.iter_interfaces(bond_filter):
+ iface_name = iface['name']
+ iface_cfg = iface_contents[iface_name]
+ cls._render_bonding_opts(iface_cfg, iface)
+ iface_master_name = iface['bond-master']
+ iface_cfg['MASTER'] = iface_master_name
+ iface_cfg['SLAVE'] = True
+ # Ensure that the master interface (and any of its children)
+ # are actually marked as being bond types...
+ master_cfg = iface_contents[iface_master_name]
+ master_cfgs = [master_cfg]
+ master_cfgs.extend(master_cfg.children)
+ for master_cfg in master_cfgs:
+ master_cfg['BONDING_MASTER'] = True
+ master_cfg.kind = 'bond'
+
+ @staticmethod
+ def _render_vlan_interfaces(network_state, iface_contents):
+ vlan_filter = renderer.filter_by_type('vlan')
+ for iface in network_state.iter_interfaces(vlan_filter):
+ iface_name = iface['name']
+ iface_cfg = iface_contents[iface_name]
+ iface_cfg['VLAN'] = True
+ iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
+
+ @staticmethod
+ def _render_dns(network_state, existing_dns_path=None):
+ content = resolv_conf.ResolvConf("")
+ if existing_dns_path and os.path.isfile(existing_dns_path):
+ content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
+ for nameserver in network_state.dns_nameservers:
+ content.add_nameserver(nameserver)
+ for searchdomain in network_state.dns_searchdomains:
+ content.add_search_domain(searchdomain)
+ return "\n".join([_make_header(';'), str(content)])
+
+ @classmethod
+ def _render_bridge_interfaces(cls, network_state, iface_contents):
+ bridge_filter = renderer.filter_by_type('bridge')
+ for iface in network_state.iter_interfaces(bridge_filter):
+ iface_name = iface['name']
+ iface_cfg = iface_contents[iface_name]
+ iface_cfg.kind = 'bridge'
+ for old_key, new_key in cls.bridge_opts_keys:
+ if old_key in iface:
+ iface_cfg[new_key] = iface[old_key]
+ # Is this the right key to get all the connected interfaces?
+ for bridged_iface_name in iface.get('bridge_ports', []):
+ # Ensure all bridged interfaces are correctly tagged
+ # as being bridged to this interface.
+ bridged_cfg = iface_contents[bridged_iface_name]
+ bridged_cfgs = [bridged_cfg]
+ bridged_cfgs.extend(bridged_cfg.children)
+ for bridge_cfg in bridged_cfgs:
+ bridge_cfg['BRIDGE'] = iface_name
+
+ @classmethod
+ def _render_sysconfig(cls, base_sysconf_dir, network_state):
+ '''Given state, return /etc/sysconfig files + contents'''
+ iface_contents = {}
+ for iface in network_state.iter_interfaces():
+ iface_name = iface['name']
+ iface_cfg = NetInterface(iface_name, base_sysconf_dir)
+ cls._render_iface_shared(iface, iface_cfg)
+ iface_contents[iface_name] = iface_cfg
+ cls._render_physical_interfaces(network_state, iface_contents)
+ cls._render_bond_interfaces(network_state, iface_contents)
+ cls._render_vlan_interfaces(network_state, iface_contents)
+ cls._render_bridge_interfaces(network_state, iface_contents)
+ contents = {}
+ for iface_name, iface_cfg in iface_contents.items():
+ if iface_cfg or iface_cfg.children:
+ contents[iface_cfg.path] = iface_cfg.to_string()
+ for iface_cfg in iface_cfg.children:
+ if iface_cfg:
+ contents[iface_cfg.path] = iface_cfg.to_string()
+ if iface_cfg.routes:
+ contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
+ return contents
+
+ def render_network_state(self, target, network_state):
+ base_sysconf_dir = os.path.join(target, self.sysconf_dir)
+ for path, data in self._render_sysconfig(base_sysconf_dir,
+ network_state).items():
+ util.write_file(path, data)
+ if self.dns_path:
+ dns_path = os.path.join(target, self.dns_path)
+ resolv_content = self._render_dns(network_state,
+ existing_dns_path=dns_path)
+ util.write_file(dns_path, resolv_content)
+ if self.netrules_path:
+ netrules_content = self._render_persistent_net(network_state)
+ netrules_path = os.path.join(target, self.netrules_path)
+ util.write_file(netrules_path, netrules_content)
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 7998111a..3ae00fc6 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -2,6 +2,8 @@ from cloudinit import net
from cloudinit.net import cmdline
from cloudinit.net import eni
from cloudinit.net import network_state
+from cloudinit.net import sysconfig
+from cloudinit.sources.helpers import openstack
from cloudinit import util
from .helpers import mock
@@ -74,8 +76,102 @@ STATIC_EXPECTED_1 = {
'dns_nameservers': ['10.0.1.1']}],
}
+# Examples (and expected outputs for various renderers).
+OS_SAMPLES = [
+ {
+ 'in_data': {
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
+ "networks": [{
+ "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4",
+ "type": "ipv4", "netmask": "255.255.252.0",
+ "link": "tap1a81968a-79",
+ "routes": [{
+ "netmask": "0.0.0.0",
+ "network": "0.0.0.0",
+ "gateway": "172.19.3.254",
+ }],
+ "ip_address": "172.19.1.34", "id": "network0"
+ }],
+ "links": [
+ {
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
+ "mtu": None, "type": "bridge", "id":
+ "tap1a81968a-79",
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
+ },
+ ],
+ },
+ 'in_macs': {
+ 'fa:16:3e:ed:9a:59': 'eth0',
+ },
+ 'out_sysconfig': [
+ ('etc/sysconfig/network-scripts/ifcfg-eth0',
+ """
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=static
+DEFROUTE=yes
+DEVICE=eth0
+GATEWAY=172.19.3.254
+HWADDR=fa:16:3e:ed:9a:59
+IPADDR=172.19.1.34
+NETMASK=255.255.252.0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+""".lstrip()),
+ ('etc/sysconfig/network-scripts/route-eth0',
+ """
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+ADDRESS0=0.0.0.0
+GATEWAY0=172.19.3.254
+NETMASK0=0.0.0.0
+""".lstrip()),
+ ('etc/resolv.conf',
+ """
+; Created by cloud-init on instance boot automatically, do not edit.
+;
+nameserver 172.19.0.12
+""".lstrip()),
+ ('etc/udev/rules.d/70-persistent-net.rules',
+ "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
+ 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
+ }
+]
+
+
+def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info,
+ mock_sys_dev_path):
+ mock_get_devicelist.return_value = ['eth1000']
+ dev_characteristics = {
+ 'eth1000': {
+ "bridge": False,
+ "carrier": False,
+ "dormant": False,
+ "operstate": "down",
+ "address": "07-1C-C6-75-A4-BE",
+ }
+ }
+
+ def netdev_info(name, field):
+ return dev_characteristics[name][field]
+
+ mock_sys_netdev_info.side_effect = netdev_info
+
+ def sys_dev_path(devname, path=""):
+ return tmp_dir + devname + "/" + path
+
+ for dev in dev_characteristics:
+ os.makedirs(os.path.join(tmp_dir, dev))
+ with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
+ fh.write("down")
+
+ mock_sys_dev_path.side_effect = sys_dev_path
-class TestEniNetRendering(TestCase):
+
+class TestSysConfigRendering(TestCase):
@mock.patch("cloudinit.net.sys_dev_path")
@mock.patch("cloudinit.net.sys_netdev_info")
@@ -83,35 +179,67 @@ class TestEniNetRendering(TestCase):
def test_default_generation(self, mock_get_devicelist,
mock_sys_netdev_info,
mock_sys_dev_path):
- mock_get_devicelist.return_value = ['eth1000', 'lo']
-
- dev_characteristics = {
- 'eth1000': {
- "bridge": False,
- "carrier": False,
- "dormant": False,
- "operstate": "down",
- "address": "07-1C-C6-75-A4-BE",
- }
- }
+ tmp_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, tmp_dir)
+ _setup_test(tmp_dir, mock_get_devicelist,
+ mock_sys_netdev_info, mock_sys_dev_path)
- def netdev_info(name, field):
- return dev_characteristics[name][field]
+ network_cfg = net.generate_fallback_config()
+ ns = network_state.parse_net_config_data(network_cfg,
+ skip_broken=False)
- mock_sys_netdev_info.side_effect = netdev_info
+ render_dir = os.path.join(tmp_dir, "render")
+ os.makedirs(render_dir)
+ renderer = sysconfig.Renderer()
+ renderer.render_network_state(render_dir, ns)
+
+ render_file = 'etc/sysconfig/network-scripts/ifcfg-eth1000'
+ with open(os.path.join(render_dir, render_file)) as fh:
+ content = fh.read()
+ expected_content = """
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=dhcp
+DEVICE=eth1000
+HWADDR=07-1C-C6-75-A4-BE
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+""".lstrip()
+ self.assertEqual(expected_content, content)
+
+ def test_openstack_rendering_samples(self):
tmp_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmp_dir)
+ render_dir = os.path.join(tmp_dir, "render")
+ for os_sample in OS_SAMPLES:
+ ex_input = os_sample['in_data']
+ ex_mac_addrs = os_sample['in_macs']
+ network_cfg = openstack.convert_net_json(
+ ex_input, known_macs=ex_mac_addrs)
+ ns = network_state.parse_net_config_data(network_cfg,
+ skip_broken=False)
+ renderer = sysconfig.Renderer()
+ renderer.render_network_state(render_dir, ns)
+ for fn, expected_content in os_sample.get('out_sysconfig', []):
+ with open(os.path.join(render_dir, fn)) as fh:
+ self.assertEqual(expected_content, fh.read())
- def sys_dev_path(devname, path=""):
- return tmp_dir + devname + "/" + path
- for dev in dev_characteristics:
- os.makedirs(os.path.join(tmp_dir, dev))
- with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
- fh.write("down")
+class TestEniNetRendering(TestCase):
- mock_sys_dev_path.side_effect = sys_dev_path
+ @mock.patch("cloudinit.net.sys_dev_path")
+ @mock.patch("cloudinit.net.sys_netdev_info")
+ @mock.patch("cloudinit.net.get_devicelist")
+ def test_default_generation(self, mock_get_devicelist,
+ mock_sys_netdev_info,
+ mock_sys_dev_path):
+ tmp_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, tmp_dir)
+ _setup_test(tmp_dir, mock_get_devicelist,
+ mock_sys_netdev_info, mock_sys_dev_path)
network_cfg = net.generate_fallback_config()
ns = network_state.parse_net_config_data(network_cfg,
@@ -120,11 +248,11 @@ class TestEniNetRendering(TestCase):
render_dir = os.path.join(tmp_dir, "render")
os.makedirs(render_dir)
- renderer = eni.Renderer()
- renderer.render_network_state(render_dir, ns,
- eni="interfaces",
- links_prefix=None,
- netrules=None)
+ renderer = eni.Renderer(
+ {'links_path_prefix': None,
+ 'eni_path': 'interfaces', 'netrules_path': None,
+ })
+ renderer.render_network_state(render_dir, ns)
self.assertTrue(os.path.exists(os.path.join(render_dir,
'interfaces')))