cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #01687
Re: [Merge] ~raharper/cloud-init:netconfig-v2-passthrough into cloud-init:master
Thanks for the review
Diff comments:
> diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
> index 48ccec8..f77b1e3 100644
> --- a/cloudinit/distros/debian.py
> +++ b/cloudinit/distros/debian.py
> @@ -38,11 +39,16 @@ ENI_HEADER = """# This file is generated from information provided by
> # network: {config: disabled}
> """
>
> +NETPLAN_GENERATE = ['netplan', 'generate']
> +
>
> 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"
> + network_conf_fn = {
> + 1: "/etc/network/interfaces.d/50-cloud-init.cfg",
> + 2: "/etc/netplan/50-cloud-init.yaml"
> + }
integers map the the network config version. 1 == eni, 2 = netplan.
I may have in a previous iteration done something like:
conf_fn = network_conf_fn.get(config.get('version'))
I'm fine to change however is best;
>
> def __init__(self, name, cfg, paths):
> distros.Distro.__init__(self, name, cfg, paths)
> @@ -76,11 +77,40 @@ class Distro(distros.Distro):
> self.package_command('install', pkgs=pkglist)
>
> def _write_network(self, settings):
> - util.write_file(self.network_conf_fn, settings)
> + # this is always going to be eni based
> + util.write_file(self.network_conf_fn[1], settings)
> return ['all']
>
> + def _select_net_renderer(self, network_state):
> + # This method will encapsulate the policy
> + # by which $distro determines which render
> + # to use.
> +
> + # In Ubuntu, we only use v2 iff the target
> + # system has netplan and networkd available
> + netplan_support = _netplan_supported()
> +
> + if netplan_support is True:
ACK
> + LOG.debug('Selected network config renderer: netplan')
> + net_renderer = netplan.Renderer({
> + 'netplan_path': self.network_conf_fn[2],
> + 'netplan_header': ENI_HEADER,
> + 'postcmds': [NETPLAN_GENERATE]
> + })
> + else:
> + LOG.debug('Selected network config renderer: eni')
> + net_renderer = eni.Renderer({
> + 'eni_path': self.network_conf_fn[1],
> + 'eni_header': ENI_HEADER,
> + 'links_path_prefix': None,
> + 'netrules_path': None,
> + })
> +
> + return net_renderer
> +
> def _write_network_config(self, netconfig):
> ns = parse_net_config_data(netconfig)
> + self._net_renderer = self._select_net_renderer(ns)
> self._net_renderer.render_network_state("/", ns)
> _maybe_remove_legacy_eth0()
> return []
> diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
> index ea649cc..e2a50ad 100755
> --- a/cloudinit/net/__init__.py
> +++ b/cloudinit/net/__init__.py
> @@ -205,7 +206,11 @@ def _get_current_rename_info(check_downable=True):
> """Collect information necessary for rename_interfaces."""
> names = get_devicelist()
> bymac = {}
> + virtual = os.listdir(SYS_DEV_VIRT_NET)
> for n in names:
> + # do not attempt to rename virtual interfaces
> + if n in virtual:
> + continue
I've updated the bug as well; tl;dr is then we need to find the duplicates and have some way to determine which of the N interfaces sharing the MAC are rename'able.
> bymac[get_interface_mac(n)] = {
> 'name': n, 'up': is_up(n), 'downable': None}
>
> diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
> new file mode 100644
> index 0000000..80d5c5d
> --- /dev/null
> +++ b/cloudinit/net/netplan.py
> @@ -0,0 +1,343 @@
> +# vi: ts=4 expandtab
ACK
> +#
> +# 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 copy
> +import os
> +from textwrap import indent
> +
> +from . import renderer
> +from .network_state import subnet_is_ipv6
> +
> +from cloudinit import util
> +from cloudinit.net import SYS_CLASS_NET
> +
> +
> +NET_CONFIG_COMMANDS = [
> + "pre-up", "up", "post-up", "down", "pre-down", "post-down",
> +]
ACK; left over from copy of eni.py
> +
> +NET_CONFIG_BRIDGE_OPTIONS = [
> + "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
> + "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
> +]
> +
> +NET_CONFIG_OPTIONS = [
> + "address", "netmask", "broadcast", "network", "metric", "gateway",
> + "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
> + "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
> + "netnum", "endpoint", "local", "ttl",
> +]
> +
> +NET_CONFIG_TO_V2 = {
> + 'bridge': {
> + 'bridge_ageing': 'ageing-time',
> + 'bridge_bridgeprio': 'priority',
> + 'bridge_fd': 'forward-delay',
> + 'bridge_gcint': None,
> + 'bridge_hello': 'hello-time',
> + 'bridge_maxage': 'max-age',
> + 'bridge_maxwait': None,
> + 'bridge_pathcost': 'path-cost',
> + 'bridge_portprio': None,
> + 'bridge_waitport': None,
> + },
> + 'bond': {
> + 'bond-ad-select': 'ad-select',
> + 'bond-arp-interval': 'arp-interval',
> + 'bond-arp-ip-target': 'arp-ip-target',
> + 'bond-arp-validate': 'arp-validate',
> + 'bond-downdelay': 'down-delay',
> + 'bond-fail-over-mac': 'fail-over-mac-policy',
> + 'bond-lacp-rate': 'lacp-rate',
> + 'bond-miimon': 'mii-monitor-interval',
> + 'bond-min-links': 'min-links',
> + 'bond-mode': 'mode',
> + 'bond-num-grat-arp': 'gratuitious-arp',
> + 'bond-primary-reselect': 'primary-reselect-policy',
> + 'bond-updelay': 'up-delay',
> + 'bond-xmit_hash_policy': 'transmit_hash_policy',
> + },
> +}
> +
> +
> +def _get_params_dict_by_match(config, match):
> + return dict((key, value) for (key, value) in config.items()
> + if key.startswith(match))
> +
> +
> +def _extract_addresses(config, entry):
ACK
> + addresses = []
> + routes = []
> + nameservers = []
> + searchdomains = []
> + subnets = config.get('subnets', [])
> + if subnets is None:
> + subnets = []
> + for subnet in subnets:
> + sn_type = subnet.get('type')
> + if sn_type.startswith('dhcp'):
> + entry.update({sn_type: True})
> + elif sn_type in ['static']:
> + addr = "%s" % subnet.get('address')
> + if 'netmask' in subnet:
> + addr += "/%s" % subnet.get('netmask')
> + if 'gateway' in subnet and subnet.get('gateway'):
> + gateway = subnet.get('gateway')
> + if ":" in gateway:
> + entry.update({'gateway6': gateway})
> + else:
> + entry.update({'gateway4': gateway})
> + if 'dns_nameservers' in subnet:
> + nameservers += subnet.get('dns_nameservers', [])
> + if 'dns_search' in subnet:
> + searchdomains += subnet.get('dns_search', [])
> + if 'mtu' in subnet:
> + mtukey = 'mtu'
> + if _subnet_is_ipv6(subnet):
> + mtukey += '6'
> + entry.update({mtukey: subnet.get('mtu')})
> + for route in subnet.get('routes', []):
> + to_net = "%s/%s" % (route.get('network'),
> + route.get('netmask'))
> + route = {
> + 'via': route.get('gateway'),
> + 'to': to_net,
> + }
> + if 'metric' in route:
> + route.update({'metric': route.get('metric', 100)})
> + routes.append(route)
> +
> + addresses.append(addr)
> +
> + if len(addresses) > 0:
> + entry.update({'addresses': addresses})
> + if len(routes) > 0:
> + entry.update({'routes': routes})
> + if len(nameservers) > 0:
> + ns = {'addresses': nameservers}
> + entry.update({'nameservers': ns})
> + if len(searchdomains) > 0:
> + ns = entry.get('nameservers', {})
> + ns.update({'search': searchdomains})
> + entry.update({'nameservers': ns})
> +
> +
> +def _extract_bond_slaves_by_name(interfaces, entry, bond_master):
> + bond_slave_names = [name for (name, cfg) in interfaces.items()
> + if cfg.get('bond-master', None) == bond_master]
> + if len(bond_slave_names) > 0:
> + entry.update({'interfaces': bond_slave_names})
> +
> +
> +class Renderer(renderer.Renderer):
> + """Renders network information in a /etc/netplan/network.yaml format."""
> +
> + def __init__(self, config=None):
> + if not config:
> + config = {}
> + self.netplan_path = config.get('netplan_path',
> + 'etc/netplan/50-cloud-init.yaml')
> + self.netplan_header = config.get('netplan_header', None)
> + self.postcmds = config.get('postcmds', [])
I don't think we'll keep it. We'll have 'netplan generate' for sure
and likely some explicit kick for .link interfaces; note that 'netplan apply'
isn't want we want since that (at least for now) attempts to start networkd
itself and replug network devices (which we don't need).
> +
> + def render_network_state(self, target, network_state):
> + # check network state for version
> + # if v2, then extract network_state.config
> + # else render_v2_from_state
> + fpnplan = os.path.join(target, self.netplan_path)
> + util.ensure_dir(os.path.dirname(fpnplan))
> + header = self.netplan_header if self.netplan_header else ""
> +
> + if network_state.version > 1:
> + # pass-through original config
> + content = util.yaml_dumps({'network': network_state.config},
> + explicit_start=False,
> + explicit_end=False)
> + else:
> + # render from state
> + content = self._render_content(network_state)
> + # ensure we poke udev to run net_setup_link
> + if len(self.postcmds) > 0:
> + setup_lnk = ['udevadm', 'test-builtin', 'net_setup_link']
> + self.postcmds += [setup_lnk + [SYS_CLASS_NET + "/%s" % iface]
> + for iface in os.listdir(SYS_CLASS_NET) if
> + os.path.islink(SYS_CLASS_NET + "/%s" % iface)]
Yeah; mostly helpful for playing with it. I think we're close to
settling on fixed commands to run in the netplan path only.
> +
> + if not header.endswith("\n"):
> + header += "\n"
> + util.write_file(fpnplan, header + content)
> + for cmd in self.postcmds:
> + out, err = util.subp(cmd, capture=True)
> + print('WARK: %s: stdout:\n%s\nstderr:\n%s' % (cmd, out, err))
> +
> + def _render_content(self, network_state):
> + print('rendering v2 for victory!')
> + ethernets = {}
> + wifis = {}
> + bridges = {}
> + bonds = {}
> + vlans = {}
> + content = []
> +
> + interfaces = network_state._network_state.get('interfaces', [])
> + order = {
> + 'physical': 0,
> + 'bond': 1,
> + 'bridge': 2,
> + 'vlan': 3,
> + }
> +
> + nameservers = network_state.dns_nameservers
> + searchdomains = network_state.dns_searchdomains
> +
We can drop the order; this was related to eni rendering; as it
turns out netplan has it's own order that is needed (though hopefully
with the forward declaration fix in netplan, we can just do
util.write_file((util.yaml_dump()).
> + for config in sorted(network_state.iter_interfaces(),
> + key=lambda k: (order[k['type']], k['name'])):
> + ifname = config.get('name')
> + # filter None entries up front so we can do simple if key in dict
> + ifcfg = dict((key, value) for (key, value) in config.items()
> + if value)
> +
> + if_type = ifcfg.get('type')
> + if if_type == 'physical':
> + # required_keys = ['name', 'mac_address']
> + eth = {
> + 'set-name': ifname,
> + 'match': ifcfg.get('match', None),
> + }
> + if eth['match'] is None:
> + macaddr = ifcfg.get('mac_address', None)
> + if macaddr is not None:
> + eth['match'] = {'macaddress': macaddr.lower()}
> + else:
> + del eth['match']
> + if 'mtu' in ifcfg:
> + eth['mtu'] = ifcfg.get('mtu')
> +
> + _extract_addresses(ifcfg, eth)
> + ethernets.update({ifname: eth})
> +
> + elif if_type == 'bond':
> + # required_keys = ['name', 'bond_interfaces']
> + bond = {}
eni supports both '-' and '_' so v1 supported both; hence network state
may have one or the other. We could push a single state into network_state
and then render only _ or whatever is required in eni.
> + bond_config = {}
> + # extract bond params and drop the bond_ prefix as it's
> + # redundent in v2 yaml format
> + v2_bond_map = NET_CONFIG_TO_V2.get('bond')
> + for match in ['bond_', 'bond-']:
> + bond_params = _get_params_dict_by_match(ifcfg, match)
> + for (param, value) in bond_params.items():
> + newname = v2_bond_map.get(param)
> + if newname is None:
> + continue
> + bond_config.update({newname: value})
> +
> + if len(bond_config) > 0:
> + bond.update({'parameters': bond_config})
> + slave_interfaces = ifcfg.get('bond-slaves')
> + if slave_interfaces == 'none':
> + _extract_bond_slaves_by_name(interfaces, bond, ifname)
> + _extract_addresses(ifcfg, bond)
> + bonds.update({ifname: bond})
> +
> + elif if_type == 'bridge':
> + # required_keys = ['name', 'bridge_ports']
> + bridge = {
> + 'interfaces': copy.copy(ifcfg.get('bridge_ports')),
> + }
> + # extract bridge params and drop the bridge prefix as it's
> + # redundent in v2 yaml format
> + match_prefix = 'bridge_'
> + params = _get_params_dict_by_match(ifcfg, match_prefix)
> + br_config = {}
> +
> + # v2 yaml uses different names for the keys
> + # and at least one value format change
> + v2_bridge_map = NET_CONFIG_TO_V2.get('bridge')
> + for (param, value) in params.items():
> + newname = v2_bridge_map.get(param)
> + if newname is None:
> + continue
> + br_config.update({newname: value})
> + if newname == 'path-cost':
> + # <interface> <cost> -> <interface>: int(<cost>)
> + newvalue = {}
> + for costval in value:
> + (port, cost) = costval.split()
> + newvalue[port] = int(cost)
> + br_config.update({newname: newvalue})
> + if len(br_config) > 0:
> + bridge.update({'parameters': br_config})
> + _extract_addresses(ifcfg, bridge)
> + bridges.update({ifname: bridge})
> +
> + elif if_type == 'vlan':
> + # required_keys = ['name', 'vlan_id', 'vlan-raw-device']
> + vlan = {
> + 'id': ifcfg.get('vlan_id'),
> + 'link': ifcfg.get('vlan-raw-device')
> + }
> +
> + _extract_addresses(ifcfg, vlan)
> + vlans.update({ifname: vlan})
> +
> + # inject global nameserver values under each physical interface
> + if nameservers:
> + for eth, cfg in ethernets.items():
ACK
> + nscfg = cfg.get('nameservers', {})
> + addresses = nscfg.get('addresses', [])
> + addresses += nameservers
> + nscfg.update({'addresses': addresses})
> + cfg.update({'nameservers': nscfg})
> +
> + if searchdomains:
> + for eth, cfg in ethernets.items():
> + nscfg = cfg.get('nameservers', {})
> + search = nscfg.get('search', [])
> + search += searchdomains
> + nscfg.update({'search': search})
> + cfg.update({'nameservers': nscfg})
> +
You forgot:
::smoser the _render_section stuff could be added as a method
render_ordered_yaml or something.
render_ordered(indent=4,
(('ethernets', ethernets),
('wifis', wifis),
('bonds', bonds))
kind of seems generally useful.
To which I replied:
Well, we _plan_ to drop it; netplan should not require an order
for parsing it; that's supposedly fixed in 0.19; I've yet to
test it.
> + # workaround yaml dictionary key sorting when dumping
> + def _render_section(name, section):
> + if section:
> + dump = util.yaml_dumps({name: section},
> + explicit_start=False,
> + explicit_end=False)
> + txt = indent(dump, ' ' * 4)
> + return [txt]
> + return []
> +
> + content.append("network:\n version: 2\n")
> + content += _render_section('ethernets', ethernets)
> + content += _render_section('wifis', wifis)
> + content += _render_section('bonds', bonds)
> + content += _render_section('bridges', bridges)
> + content += _render_section('vlans', vlans)
> +
> + return "".join(content)
> +
> +
> +def network_state_to_netplan(network_state, header=None):
Well, we already have a render_network_state in the Renderer class; this is a non-class method for use outside of the Distro configured renderer object. We've a similar helper in eni.py; none in sysconfig.py
> + # render the provided network state, return a string of equivalent eni
> + netplan_path = 'etc/network/50-cloud-init.yaml'
> + renderer = Renderer({
> + 'netplan_path': netplan_path,
> + 'netplan_header': header,
> + })
> + if not header:
> + header = ""
> + if not header.endswith("\n"):
> + header += "\n"
> + contents = renderer._render_content(network_state)
> + return header + contents
> diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
> index 11ef585..844e36c 100644
> --- a/cloudinit/net/network_state.py
> +++ b/cloudinit/net/network_state.py
> @@ -403,6 +452,239 @@ class NetworkStateInterpreter(object):
> }
> routes.append(route)
>
> + # V2 handlers
> + def handle_bonds(self, command):
> + '''
> + v2_command = {
> + bond0: {
> + 'interfaces': ['interface0', 'interface1'],
> + 'miimon': 100,
> + 'mode': '802.3ad',
> + 'xmit_hash_policy': 'layer3+4'},
> + bond1: {
> + 'bond-slaves': ['interface2', 'interface7'],
> + 'mode': 1
> + }
> + }
> +
> + v1_command = {
> + 'type': 'bond'
> + 'name': 'bond0',
> + 'bond_interfaces': [interface0, interface1],
> + 'params': {
> + 'bond-mode': '802.3ad',
> + 'bond_miimon: 100,
> + 'bond_xmit_hash_policy': 'layer3+4',
> + }
> + }
> +
> + '''
> + self._handle_bond_bridge(command, cmd_type='bond')
> +
> + def handle_bridges(self, command):
> +
> + '''
> + v2_command = {
> + br0: {
> + 'interfaces': ['interface0', 'interface1'],
> + 'fd': 0,
> + 'stp': 'off',
> + 'maxwait': 0,
> + }
> + }
> +
> + v1_command = {
> + 'type': 'bridge'
> + 'name': 'br0',
> + 'bridge_interfaces': [interface0, interface1],
> + 'params': {
> + 'bridge_stp': 'off',
> + 'bridge_fd: 0,
> + 'bridge_maxwait': 0
> + }
> + }
> +
> + '''
> + self._handle_bond_bridge(command, cmd_type='bridge')
> +
> + def handle_ethernets(self, command):
> + '''
> + ethernets:
> + eno1:
> + match:
> + macaddress: 00:11:22:33:44:55
> + wakeonlan: true
> + dhcp4: true
> + dhcp6: false
> + addresses:
> + - 192.168.14.2/24
> + - 2001:1::1/64
> + gateway4: 192.168.14.1
> + gateway6: 2001:1::2
> + nameservers:
> + search: [foo.local, bar.local]
> + addresses: [8.8.8.8, 8.8.4.4]
> + lom:
> + match:
> + driver: ixgbe
> + set-name: lom1
> + dhcp6: true
> + switchports:
> + match:
> + name: enp2*
> + mtu: 1280
> +
> + command = {
> + 'type': 'physical',
> + 'mac_address': 'c0:d6:9f:2c:e8:80',
> + 'name': 'eth0',
> + 'subnets': [
> + {'type': 'dhcp4'}
> + ]
> + }
> + '''
> + for eth, cfg in command.items():
> + phy_cmd = {
> + 'type': 'physical',
> + 'name': cfg.get('set-name', eth),
> + }
> + mac_address = cfg.get('match', {}).get('macaddress', None)
> + if not mac_address:
> + LOG.warning('NetworkState Version2: missing macaddress')
> +
> + for key in ['mtu', 'match', 'wakeonlan']:
> + if key in cfg:
> + phy_cmd.update({key: cfg.get(key)})
> +
> + subnets = self._v2_to_v1_ipcfg(cfg)
> + if len(subnets) > 0:
> + phy_cmd.update({'subnets': subnets})
> +
> + LOG.debug('v2(ethernets) -> v1(physical):\n%s', phy_cmd)
> + self.handle_physical(phy_cmd)
> +
> + def handle_vlans(self, command):
> + '''
> + v2_vlans = {
> + 'eth0.123': {
> + 'id': 123,
> + 'link': 'eth0',
> + 'dhcp4': True,
> + }
> + }
> +
> + v1_command = {
> + 'type': 'vlan',
> + 'name': 'eth0.123',
> + 'vlan_link': 'eth0',
> + 'vlan_id': 123,
> + 'subnets': [{'type': 'dhcp4'}],
> + }
> + '''
> + for vlan, cfg in command.items():
> + vlan_cmd = {
> + 'type': 'vlan',
> + 'name': vlan,
> + 'vlan_id': cfg.get('id'),
> + 'vlan_link': cfg.get('link'),
> + }
> + subnets = self._v2_to_v1_ipcfg(cfg)
> + if len(subnets) > 0:
> + vlan_cmd.update({'subnets': subnets})
> + LOG.debug('v2(vlans) -> v1(vlan):\n%s', vlan_cmd)
> + self.handle_vlan(vlan_cmd)
> +
> + def handle_wifis(self, command):
> + LOG.warning('NetworkState V2: Skipping wifi configuration')
> + pass
ACK
> +
> + def _v2_common(self, cfg):
> + LOG.debug('v2_common: handling config:\n%s', cfg)
> + if 'nameservers' in cfg:
> + search = cfg.get('nameservers').get('search', [])
> + dns = cfg.get('nameservers').get('addresses', [])
> + name_cmd = {'type': 'nameserver'}
> + if len(search) > 0:
> + name_cmd.update({'search': search})
> + if len(dns) > 0:
> + name_cmd.update({'addresses': dns})
> + LOG.debug('v2(nameserver) -> v1(nameserver):\n%s', name_cmd)
> + self.handle_nameserver(name_cmd)
> +
> + def _handle_bond_bridge(self, command, cmd_type=None):
> + """Common handler for bond and bridge types"""
> + for item_name, item_cfg in command.items():
> + item_params = dict((key, value) for (key, value) in
> + item_cfg.items() if key not in
> + NETWORK_V2_KEY_FILTER)
> + v1_cmd = {
> + 'type': cmd_type,
> + 'name': item_name,
> + cmd_type + '_interfaces': item_cfg.get('interfaces'),
> + 'params': item_params,
> + }
> + subnets = self._v2_to_v1_ipcfg(item_cfg)
> + if len(subnets) > 0:
> + v1_cmd.update({'subnets': subnets})
> +
> + LOG.debug('v2(%ss) -> v1(%s):\n%s', cmd_type, cmd_type, v1_cmd)
> + self.handle_bridge(v1_cmd)
> +
> + def _v2_to_v1_ipcfg(self, cfg):
> + """Common ipconfig extraction from v2 to v1 subnets array."""
> +
> + subnets = []
> + if 'dhcp4' in cfg:
> + subnets.append({'type': 'dhcp4'})
> + if 'dhcp6' in cfg:
> + subnets.append({'type': 'dhcp6'})
> +
> + gateway4 = None
> + gateway6 = None
> + for address in cfg.get('addresses', []):
> + subnet = {
> + 'type': 'static',
> + 'address': address,
> + }
> +
> + routes = []
> + for route in cfg.get('routes', []):
> + route_addr = route.get('to')
> + if "/" in route_addr:
> + route_addr, route_cidr = route_addr.split("/")
> + route_netmask = cidr2mask(route_cidr)
> + subnet_route = {
> + 'address': route_addr,
> + 'netmask': route_netmask,
> + 'gateway': route.get('via')
> + }
> + routes.append(subnet_route)
> + if len(routes) > 0:
> + subnet.update({'routes': routes})
> +
> + if ":" in address:
> + if 'gateway6' in cfg and gateway6 is None:
> + gateway6 = cfg.get('gateway6')
> + subnet.update({'gateway': gateway6})
> + else:
> + if 'gateway4' in cfg and gateway4 is None:
> + gateway4 = cfg.get('gateway4')
> + subnet.update({'gateway': gateway4})
> +
> + subnets.append(subnet)
> + return subnets
> +
> +
> +def subnet_is_ipv6(subnet):
> + """ Common helper for checking network_state subnets for ipv6"""
> + # 'static6' or 'dhcp6'
> + if subnet['type'].endswith('6'):
> + # This is a request for DHCPv6.
> + return True
> + elif subnet['type'] == 'static' and ":" in subnet['address']:
> + return True
> + return False
> +
>
> def cidr2mask(cidr):
> mask = [0, 0, 0, 0]
> diff --git a/systemd/systemd-networkd-wait-online.path b/systemd/systemd-networkd-wait-online.path
> new file mode 100644
> index 0000000..64940b8
> --- /dev/null
> +++ b/systemd/systemd-networkd-wait-online.path
> @@ -0,0 +1,5 @@
> +[Unit]
> +Description=Trigger systemd-networkd-wait-online if netplan runs/updates
> +
> +[Path]
> +PathChanged=/run/systemd/generator/netplan.stamp
Maybe i'm just missing something, but I'm not sure how this works.
It seems that the .path entry here will start
systemd-networkd-wait-online.service when netplan.stamp is created or
modified.
No; we _always_ wait on systemd-networkd-wait-online.service; Without
a valid networkd config (nothing /etc/systemd/network or /run/systemd/network
the wait-online service is a noop.
But I'm not really sure how that works. I guess on creation of that
file, systemd must re-load its generators, and then decide that
cloud-init.service should wait on systemd-networkd-wait-online.
It's certianly fine to the service at any time; we don't care as it's a noop without
a networkd configuration file and networkd actually running.
What happens on a system with systemd-networkd and no netplan?
If a user has configured networkd; they they will have enabled systemd-networkd
via a .wants target somewhere; this means that networkd will run before
cloud-init.service, and so will systemd-networkd-wait-online.service.
With no netplan, the .path file is never triggered.
Maybe we need netplan to provide us with a correct analog to
networking.service.
I think we're fine here; but I'm willing to change if we find a
better way to handle all of our cases.
- i think we might want PathExists rather than PathChanged per:
https://www.freedesktop.org/software/systemd/man/systemd.path.html
We'd like to be called whenever it changes. For example, if there was a
built-in netplan config and then cloud-init generates an additional config
we'd want cloud-init's call to netplan-generate to update the .stampfile
and trigger the wait.
--
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/319259
Your team cloud init development team is requested to review the proposed merge of ~raharper/cloud-init:netconfig-v2-passthrough into cloud-init:master.
References