cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #02902
[Merge] ~raharper/cloud-init:curtin-centos into cloud-init:master
Ryan Harper has proposed merging ~raharper/cloud-init:curtin-centos into cloud-init:master.
Requested reviews:
cloud-init commiters (cloud-init-dev)
Related bugs:
Bug #1687725 in cloud-init: "sysconfig render does not support type manual subnets"
https://bugs.launchpad.net/cloud-init/+bug/1687725
Bug #1694801 in cloud-init: "sysconfig needs fix for ipv6 gateway routes"
https://bugs.launchpad.net/cloud-init/+bug/1694801
Bug #1695092 in cloud-init: "sysconfig only applies subnet/route config to physical interfaces"
https://bugs.launchpad.net/cloud-init/+bug/1695092
Bug #1701097 in cloud-init: "eni rendering of ipv6 gateways fails"
https://bugs.launchpad.net/cloud-init/+bug/1701097
Bug #1701417 in cloud-init: "cloud-init fails to configure bonding on CentOS 7"
https://bugs.launchpad.net/cloud-init/+bug/1701417
Bug #1702513 in cloud-init: "sysconfig should render MTU values from subnets/routes including ipv6"
https://bugs.launchpad.net/cloud-init/+bug/1702513
For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/327648
WIP branch with changes for el6/el7 networking rendering
--
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:curtin-centos into cloud-init:master.
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index b707146..bb80ec0 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -355,7 +355,7 @@ class Renderer(renderer.Renderer):
default_gw = " default gw %s" % route['gateway']
content.append(up + default_gw + or_true)
content.append(down + default_gw + or_true)
- elif route['network'] == '::' and route['netmask'] == 0:
+ elif route['network'] == '::' and route['prefix'] == 0:
# ipv6!
default_gw = " -A inet6 default gw %s" % route['gateway']
content.append(up + default_gw + or_true)
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
index bba139e..57652e2 100644
--- a/cloudinit/net/renderer.py
+++ b/cloudinit/net/renderer.py
@@ -20,6 +20,10 @@ def filter_by_name(match_name):
return lambda iface: match_name == iface['name']
+def filter_by_attr(match_name):
+ return lambda iface: (match_name in iface and iface[match_name])
+
+
filter_by_physical = filter_by_type('physical')
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 7ed11d1..95324ea 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -151,9 +151,10 @@ class Route(ConfigMap):
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\n" % (address_value,
- netmask_value,
- gateway_value))
+ buf.write("%s/%s via %s dev %s\n" % (address_value,
+ netmask_value,
+ gateway_value,
+ self._route_name))
return buf.getvalue()
@@ -262,6 +263,9 @@ class Renderer(renderer.Renderer):
for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
old_value = iface.get(old_key)
if old_value is not None:
+ # only set HWADDR on physical interfaces
+ if old_key == 'mac_address' and iface['type'] != 'physical':
+ continue
iface_cfg[new_key] = old_value
@classmethod
@@ -271,6 +275,7 @@ class Renderer(renderer.Renderer):
# modifying base values according to subnets
for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
+ mtu_key = 'MTU'
subnet_type = subnet.get('type')
if subnet_type == 'dhcp6':
iface_cfg['IPV6INIT'] = True
@@ -290,7 +295,13 @@ class Renderer(renderer.Renderer):
# if iface_cfg['BOOTPROTO'] == 'none':
# iface_cfg['BOOTPROTO'] = 'static'
if subnet_is_ipv6(subnet):
+ mtu_key = 'IPV6_MTU'
iface_cfg['IPV6INIT'] = True
+ if 'mtu' in subnet:
+ iface_cfg[mtu_key] = subnet['mtu']
+
+ elif subnet_type == 'manual':
+ iface_cfg['ONBOOT'] = False
else:
raise ValueError("Unknown subnet type '%s' found"
" for interface '%s'" % (subnet_type,
@@ -311,7 +322,7 @@ class Renderer(renderer.Renderer):
if 'netmask' in subnet and str(subnet['netmask']) != "":
ipv6_cidr = (subnet['address'] +
'/' +
- str(subnet['netmask']))
+ str(subnet['prefix']))
else:
ipv6_cidr = subnet['address']
if ipv6_index == 0:
@@ -329,11 +340,18 @@ class Renderer(renderer.Renderer):
iface_cfg['NETMASK' + suff] = \
net_prefix_to_ipv4_mask(subnet['prefix'])
+ if 'gateway' in subnet:
+ iface_cfg['DEFROUTE'] = True
+ if ':' in subnet['gateway']:
+ iface_cfg['IPV6_DEFAULTGW'] = subnet['gateway']
+ else:
+ iface_cfg['GATEWAY'] = subnet['gateway']
+
@classmethod
def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
for route in subnet.get('routes', []):
- is_ipv6 = subnet.get('ipv6')
+ is_ipv6 = subnet.get('ipv6') or ':' in route['gateway']
if _is_default_route(route):
if (
@@ -355,7 +373,7 @@ class Renderer(renderer.Renderer):
# also provided the default route?
iface_cfg['DEFROUTE'] = True
if 'gateway' in route:
- if is_ipv6:
+ if is_ipv6 or ':' in route['gateway']:
iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
route_cfg.has_set_default_ipv6 = True
else:
@@ -406,24 +424,42 @@ class Renderer(renderer.Renderer):
@classmethod
def _render_bond_interfaces(cls, network_state, iface_contents):
bond_filter = renderer.filter_by_type('bond')
+ slave_filter = renderer.filter_by_attr('bond-master')
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)
+ master_cfgs = [iface_cfg]
+ master_cfgs.extend(iface_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):
+ if 'mac_address' in iface and iface.get('mac_address'):
+ iface_cfg['MACADDR'] = iface.get('mac_address')
+
+ iface_subnets = iface.get("subnets", [])
+ route_cfg = iface_cfg.routes
+ cls._render_subnets(iface_cfg, iface_subnets)
+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+
+ bond_slaves = sorted([slave_iface['name']
+ for slave_iface in
+ network_state.iter_interfaces(slave_filter)
+ if slave_iface['bond-master'] == iface_name])
+ for index, bond_slave in enumerate(bond_slaves):
+ slavestr = 'BONDING_SLAVE%s' % index
+ iface_cfg[slavestr] = bond_slave
+
+ slave_cfg = iface_contents[bond_slave]
+ slave_cfg['MASTER'] = iface_name
+ slave_cfg['SLAVE'] = True
+
+ @classmethod
+ def _render_vlan_interfaces(cls, network_state, iface_contents):
vlan_filter = renderer.filter_by_type('vlan')
for iface in network_state.iter_interfaces(vlan_filter):
iface_name = iface['name']
@@ -431,6 +467,11 @@ class Renderer(renderer.Renderer):
iface_cfg['VLAN'] = True
iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
+ iface_subnets = iface.get("subnets", [])
+ route_cfg = iface_cfg.routes
+ cls._render_subnets(iface_cfg, iface_subnets)
+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+
@staticmethod
def _render_dns(network_state, existing_dns_path=None):
content = resolv_conf.ResolvConf("")
@@ -467,6 +508,10 @@ class Renderer(renderer.Renderer):
for old_key, new_key in cls.bridge_opts_keys:
if old_key in iface:
iface_cfg[new_key] = iface[old_key]
+
+ if 'mac_address' in iface and iface.get('mac_address'):
+ iface_cfg['MACADDR'] = iface.get('mac_address')
+
# 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
@@ -477,6 +522,11 @@ class Renderer(renderer.Renderer):
for bridge_cfg in bridged_cfgs:
bridge_cfg['BRIDGE'] = iface_name
+ iface_subnets = iface.get("subnets", [])
+ route_cfg = iface_cfg.routes
+ cls._render_subnets(iface_cfg, iface_subnets)
+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+
@classmethod
def _render_sysconfig(cls, base_sysconf_dir, network_state):
'''Given state, return /etc/sysconfig files + contents'''
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index b5a95a1..e02d7a7 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -442,7 +442,7 @@ class DataSourceAzure(sources.DataSource):
self.ds_cfg['agent_command'])
try:
fabric_data = metadata_func()
- except Exception as exc:
+ except Exception:
LOG.warning(
"Error communicating with Azure fabric; You may experience."
"connectivity issues.", exc_info=True)
diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
index 9f75c4b..ab22e0f 100644
--- a/packages/redhat/cloud-init.spec.in
+++ b/packages/redhat/cloud-init.spec.in
@@ -117,7 +117,8 @@ mkdir -p $RPM_BUILD_ROOT/%{_libexecdir}/%{name}
%if "%{init_system}" == "systemd"
mkdir -p $RPM_BUILD_ROOT/%{_unitdir}
-cp -p systemd/* $RPM_BUILD_ROOT/%{_unitdir}
+# only copy targets, .service files are installed by setup.py
+cp -p systemd/*.target $RPM_BUILD_ROOT/%{_unitdir}
%endif
%clean
diff --git a/setup.py b/setup.py
index bce06ad..fbcad6f 100755
--- a/setup.py
+++ b/setup.py
@@ -110,14 +110,45 @@ def render_cloud_cfg():
relpath = os.path.join(os.path.basename(tmpd), 'cloud.cfg')
return relpath
+def render_systemd_unit(template):
+ """render systemd unit into a tmpdir under same dir as setup.py
+
+ This is rendered to a temporary directory under the top level
+ directory with the name 'systemd.cfg'. The reason for not just
+ rendering to systemd/{unit}.service is for a.) don't want to write
+ over contents in that file if user had something there. b.) debuild
+ will complain that files are different outside of the debian directory."""
+
+ # older versions of tox use bdist (xenial), and then install from there.
+ # newer versions just use install.
+ if not (sys.argv[1] == 'install' or sys.argv[1].startswith('bdist*')):
+ return template
+
+ # we may get passed a non-template file, just pass it back
+ if not template.endswith('.tmpl'):
+ return template
+
+ topdir = os.path.dirname(sys.argv[0])
+ tmpd = tempfile.mkdtemp(dir=topdir)
+ atexit.register(shutil.rmtree, tmpd)
+ unit_fname = os.path.basename(template.rstrip('.tmpl'))
+ fpath = os.path.join(tmpd, unit_fname)
+ tiny_p([sys.executable, './tools/render-cloudcfg',
+ template, fpath])
+ # relpath is relative to setup.py
+ relpath = os.path.join(os.path.basename(tmpd), unit_fname)
+ return relpath
+
INITSYS_FILES = {
'sysvinit': [f for f in glob('sysvinit/redhat/*') if is_f(f)],
'sysvinit_freebsd': [f for f in glob('sysvinit/freebsd/*') if is_f(f)],
'sysvinit_deb': [f for f in glob('sysvinit/debian/*') if is_f(f)],
'sysvinit_openrc': [f for f in glob('sysvinit/gentoo/*') if is_f(f)],
- 'systemd': [f for f in (glob('systemd/*.service') +
- glob('systemd/*.target')) if is_f(f)],
+ 'systemd': [render_systemd_unit(f)
+ for f in (glob('systemd/*.tmpl') +
+ glob('systemd/*.service') +
+ glob('systemd/*.target')) if is_f(f)],
'systemd.generators': [f for f in glob('systemd/*-generator') if is_f(f)],
'upstart': [f for f in glob('upstart/*') if is_f(f)],
}
diff --git a/systemd/cloud-config.service b/systemd/cloud-config.service
deleted file mode 100644
index 3309e08..0000000
--- a/systemd/cloud-config.service
+++ /dev/null
@@ -1,16 +0,0 @@
-[Unit]
-Description=Apply the settings specified in cloud-config
-After=network-online.target cloud-config.target
-Wants=network-online.target cloud-config.target
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init modules --mode=config
-RemainAfterExit=yes
-TimeoutSec=0
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-config.service.tmpl b/systemd/cloud-config.service.tmpl
new file mode 100644
index 0000000..bdee3ce
--- /dev/null
+++ b/systemd/cloud-config.service.tmpl
@@ -0,0 +1,17 @@
+## template:jinja
+[Unit]
+Description=Apply the settings specified in cloud-config
+After=network-online.target cloud-config.target
+Wants=network-online.target cloud-config.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init modules --mode=config
+RemainAfterExit=yes
+TimeoutSec=0
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/systemd/cloud-final.service b/systemd/cloud-final.service
deleted file mode 100644
index 66f5b8f..0000000
--- a/systemd/cloud-final.service
+++ /dev/null
@@ -1,18 +0,0 @@
-[Unit]
-Description=Execute cloud user/final scripts
-After=network-online.target cloud-config.service rc-local.service multi-user.target
-Wants=network-online.target cloud-config.service
-Before=apt-daily.service
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init modules --mode=final
-RemainAfterExit=yes
-TimeoutSec=0
-KillMode=process
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-final.service.tmpl b/systemd/cloud-final.service.tmpl
new file mode 100644
index 0000000..fc01b89
--- /dev/null
+++ b/systemd/cloud-final.service.tmpl
@@ -0,0 +1,22 @@
+## template:jinja
+[Unit]
+Description=Execute cloud user/final scripts
+After=network-online.target cloud-config.service rc-local.service
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+After=multi-user.target
+{% endif %}
+Wants=network-online.target cloud-config.service
+Before=apt-daily.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init modules --mode=final
+RemainAfterExit=yes
+TimeoutSec=0
+KillMode=process
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/systemd/cloud-init-local.service b/systemd/cloud-init-local.service
deleted file mode 100644
index 7ee43ed..0000000
--- a/systemd/cloud-init-local.service
+++ /dev/null
@@ -1,24 +0,0 @@
-[Unit]
-Description=Initial cloud-init job (pre-networking)
-DefaultDependencies=no
-Wants=network-pre.target
-After=systemd-remount-fs.service
-Before=NetworkManager.service
-Before=network-pre.target
-Before=shutdown.target
-Before=sysinit.target
-Conflicts=shutdown.target
-RequiresMountsFor=/var/lib/cloud
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init init --local
-ExecStart=/bin/touch /run/cloud-init/network-config-ready
-RemainAfterExit=yes
-TimeoutSec=0
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl
new file mode 100644
index 0000000..ff9c644
--- /dev/null
+++ b/systemd/cloud-init-local.service.tmpl
@@ -0,0 +1,29 @@
+## template:jinja
+[Unit]
+Description=Initial cloud-init job (pre-networking)
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+DefaultDependencies=no
+{% endif %}
+Wants=network-pre.target
+After=systemd-remount-fs.service
+Before=NetworkManager.service
+Before=network-pre.target
+Before=shutdown.target
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+Before=sysinit.target
+Conflicts=shutdown.target
+{% endif %}
+RequiresMountsFor=/var/lib/cloud
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init init --local
+ExecStart=/bin/touch /run/cloud-init/network-config-ready
+RemainAfterExit=yes
+TimeoutSec=0
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/systemd/cloud-init.service b/systemd/cloud-init.service
deleted file mode 100644
index 39acc20..0000000
--- a/systemd/cloud-init.service
+++ /dev/null
@@ -1,27 +0,0 @@
-[Unit]
-Description=Initial cloud-init job (metadata service crawler)
-DefaultDependencies=no
-Wants=cloud-init-local.service
-Wants=sshd-keygen.service
-Wants=sshd.service
-After=cloud-init-local.service
-After=systemd-networkd-wait-online.service
-After=networking.service
-Before=network-online.target
-Before=sshd-keygen.service
-Before=sshd.service
-Before=sysinit.target
-Before=systemd-user-sessions.service
-Conflicts=shutdown.target
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/cloud-init init
-RemainAfterExit=yes
-TimeoutSec=0
-
-# Output needs to appear in instance console output
-StandardOutput=journal+console
-
-[Install]
-WantedBy=cloud-init.target
diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl
new file mode 100644
index 0000000..2c71889
--- /dev/null
+++ b/systemd/cloud-init.service.tmpl
@@ -0,0 +1,35 @@
+## template:jinja
+[Unit]
+Description=Initial cloud-init job (metadata service crawler)
+DefaultDependencies=no
+Wants=cloud-init-local.service
+Wants=sshd-keygen.service
+Wants=sshd.service
+After=cloud-init-local.service
+After=systemd-networkd-wait-online.service
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+After=networking.service
+{% endif %}
+{% if variant in ["centos", "fedora", "redhat"] %}
+After=network.service
+{% endif %}
+Before=network-online.target
+Before=sshd-keygen.service
+Before=sshd.service
+{% if variant in ["ubuntu", "unknown", "debian"] %}
+Before=sysinit.target
+Conflicts=shutdown.target
+{% endif %}
+Before=systemd-user-sessions.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cloud-init init
+RemainAfterExit=yes
+TimeoutSec=0
+
+# Output needs to appear in instance console output
+StandardOutput=journal+console
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index 83580cc..2f505d9 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -476,7 +476,9 @@ NETWORKING=yes
# Created by cloud-init on instance boot automatically, do not edit.
#
BOOTPROTO=none
+DEFROUTE=yes
DEVICE=eth0
+GATEWAY=192.168.1.254
IPADDR=192.168.1.5
NETMASK=255.255.255.0
NM_CONTROLLED=no
@@ -625,9 +627,11 @@ IPV6_AUTOCONF=no
# Created by cloud-init on instance boot automatically, do not edit.
#
BOOTPROTO=none
+DEFROUTE=yes
DEVICE=eth0
IPV6ADDR=2607:f0d0:1002:0011::2/64
IPV6INIT=yes
+IPV6_DEFAULTGW=2607:f0d0:1002:0011::1
NM_CONTROLLED=no
ONBOOT=yes
TYPE=Ethernet
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 06e8f09..1c4f4de 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -422,6 +422,31 @@ NETWORK_CONFIGS = {
via: 65.61.151.37
set-name: eth99
""").rstrip(' '),
+ 'expected_sysconfig': {
+ 'ifcfg-eth1': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ DEVICE=eth1
+ HWADDR=cf:d6:af:48:e8:80
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no""").rstrip(' '),
+ 'ifcfg-eth99': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=dhcp
+ DEFROUTE=yes
+ DEVICE=eth99
+ GATEWAY=65.61.151.37
+ HWADDR=c0:d6:9f:2c:e8:80
+ IPADDR=192.168.21.3
+ NETMASK=255.255.255.0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no """).rstrip(' ')},
'yaml': textwrap.dedent("""
version: 1
config:
@@ -541,6 +566,8 @@ iface br0 inet static
# control-alias br0
iface br0 inet6 static
address 2001:1::1/64
+ post-up route add -A inet6 default gw 2001:4800:78ff:1b::1 || true
+ pre-down route del -A inet6 default gw 2001:4800:78ff:1b::1 || true
auto bond0.200
iface bond0.200 inet dhcp
@@ -675,6 +702,9 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
eth3: 50
eth4: 75
priority: 22
+ routes:
+ - to: ::/0
+ via: 2001:4800:78ff:1b::1
vlans:
bond0.200:
dhcp4: true
@@ -697,6 +727,146 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
- sacchromyces.maas
- brettanomyces.maas
""").rstrip(' '),
+ 'expected_sysconfig': {
+ 'ifcfg-bond0': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BONDING_MASTER=yes
+ BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100"
+ BONDING_SLAVE0=eth1
+ BONDING_SLAVE1=eth2
+ BOOTPROTO=dhcp
+ DEVICE=bond0
+ DHCPV6C=yes
+ IPV6INIT=yes
+ MACADDR=aa:bb:cc:dd:ee:ff
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Bond
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-bond0.200': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=dhcp
+ DEVICE=bond0.200
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ PHYSDEV=bond0
+ TYPE=Ethernet
+ USERCTL=no
+ VLAN=yes
+ """).rstrip(' '),
+ 'ifcfg-br0': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ AGEING=250
+ BOOTPROTO=none
+ DEFROUTE=yes
+ DEVICE=br0
+ IPADDR=192.168.14.2
+ IPV6ADDR=2001:1::1
+ IPV6INIT=yes
+ IPV6_DEFAULTGW=2001:4800:78ff:1b::1
+ NETMASK=255.255.255.0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ PRIO=22
+ STP=off
+ TYPE=Bridge
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-eth0': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ DEVICE=eth0
+ HWADDR=c0:d6:9f:2c:e8:80
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-eth0.101': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ DEFROUTE=yes
+ DEVICE=eth0.101
+ GATEWAY=192.168.0.1
+ IPADDR=192.168.0.2
+ IPADDR1=192.168.2.10
+ MTU=1500
+ NETMASK=255.255.255.0
+ NETMASK1=255.255.255.0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ PHYSDEV=eth0
+ TYPE=Ethernet
+ USERCTL=no
+ VLAN=yes
+ """).rstrip(' '),
+ 'ifcfg-eth1': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ DEVICE=eth1
+ HWADDR=aa:d6:9f:2c:e8:80
+ MASTER=bond0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ SLAVE=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-eth2': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ DEVICE=eth2
+ HWADDR=c0:bb:9f:2c:e8:80
+ MASTER=bond0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ SLAVE=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-eth3': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ BRIDGE=br0
+ DEVICE=eth3
+ HWADDR=66:bb:9f:2c:e8:80
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-eth4': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ BRIDGE=br0
+ DEVICE=eth4
+ HWADDR=98:bb:9f:2c:e8:80
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """).rstrip(' '),
+ 'ifcfg-eth5': textwrap.dedent("""\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=dhcp
+ DEVICE=eth5
+ HWADDR=98:bb:9f:2c:e8:8a
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """).rstrip(' ')},
'yaml': textwrap.dedent("""
version: 1
config:
@@ -807,6 +977,10 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
address: 192.168.14.2/24
- type: static
address: 2001:1::1/64 # default to /64
+ routes:
+ - gateway: 2001:4800:78ff:1b::1
+ netmask: '::'
+ network: '::'
# A global nameserver.
- type: nameserver
address: 8.8.8.8
@@ -828,6 +1002,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
}
}
+
CONFIG_V1_EXPLICIT_LOOPBACK = {
'version': 1,
'config': [{'name': 'eth0', 'type': 'physical',
@@ -836,6 +1011,18 @@ CONFIG_V1_EXPLICIT_LOOPBACK = {
'subnets': [{'control': 'auto', 'type': 'loopback'}]},
]}
+
+CONFIG_V1_SIMPLE_SUBNET = {
+ 'version': 1,
+ 'config': [{'mac_address': '52:54:00:12:34:00',
+ 'name': 'interface0',
+ 'subnets': [{'address': '10.0.2.15',
+ 'gateway': '10.0.2.2',
+ 'netmask': '255.255.255.0',
+ 'type': 'static'}],
+ 'type': 'physical'}]}
+
+
DEFAULT_DEV_ATTRS = {
'eth1000': {
"bridge": False,
@@ -1135,6 +1322,32 @@ USERCTL=no
with open(os.path.join(render_dir, fn)) as fh:
self.assertEqual(expected_content, fh.read())
+ def test_network_config_v1_samples(self):
+ ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET)
+ render_dir = self.tmp_path("render")
+ os.makedirs(render_dir)
+ renderer = sysconfig.Renderer()
+ renderer.render_network_state(ns, render_dir)
+ found = dir2dict(render_dir)
+ nspath = '/etc/sysconfig/network-scripts/'
+ self.assertNotIn(nspath + 'ifcfg-lo', found.keys())
+ expected = """\
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=none
+DEFROUTE=yes
+DEVICE=interface0
+GATEWAY=10.0.2.2
+HWADDR=52:54:00:12:34:00
+IPADDR=10.0.2.15
+NETMASK=255.255.255.0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+"""
+ self.assertEqual(expected, found[nspath + 'ifcfg-interface0'])
+
def test_config_with_explicit_loopback(self):
ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
render_dir = self.tmp_path("render")
@@ -1654,6 +1867,43 @@ class TestEniRoundTrip(CiTestCase):
expected, [line for line in found if line])
+class TestSysconfigRoundTrip(CiTestCase):
+ def _render_and_read(self, network_config=None, state=None, dir=None):
+ if dir is None:
+ dir = self.tmp_dir()
+
+ 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")
+
+ renderer = sysconfig.Renderer()
+ renderer.render_network_state(ns, dir)
+ return dir2dict(dir)
+
+ def testsimple_render_small(self):
+ entry = NETWORK_CONFIGS['small']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ expected_sysconfig = entry.get('expected_sysconfig')
+ for ifcfg_name in expected_sysconfig:
+ expected_ifcfg = expected_sysconfig.get(ifcfg_name)
+ ifcfg_path = '/etc/sysconfig/network-scripts/' + ifcfg_name
+ self.assertEqual(expected_ifcfg.splitlines(),
+ files[ifcfg_path].splitlines())
+
+ def testsimple_render_all(self):
+ entry = NETWORK_CONFIGS['all']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ expected_sysconfig = entry.get('expected_sysconfig')
+ for ifcfg_name in expected_sysconfig:
+ expected_ifcfg = expected_sysconfig.get(ifcfg_name)
+ ifcfg_path = '/etc/sysconfig/network-scripts/' + ifcfg_name
+ self.assertEqual(expected_ifcfg.splitlines(),
+ files[ifcfg_path].splitlines())
+
+
class TestNetRenderers(CiTestCase):
@mock.patch("cloudinit.net.renderers.sysconfig.available")
@mock.patch("cloudinit.net.renderers.eni.available")
References