From 7479ffefc3c31805e2b96b94852610dd7211e8a8 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 10 Mar 2016 16:37:30 -0500 Subject: initial copy of curtin net just add curtin/net as cloudinit/net and then copy curtin/udev.py as cloudinit/net/udev.py --- cloudinit/net/network_state.py | 400 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 cloudinit/net/network_state.py (limited to 'cloudinit/net/network_state.py') diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py new file mode 100644 index 00000000..83303317 --- /dev/null +++ b/cloudinit/net/network_state.py @@ -0,0 +1,400 @@ +# Copyright (C) 2013-2014 Canonical Ltd. +# +# Author: Ryan Harper +# +# 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 . + +from curtin.log import LOG +import curtin.config as curtin_config + +NETWORK_STATE_VERSION = 1 +NETWORK_STATE_REQUIRED_KEYS = { + 1: ['version', 'config', 'network_state'], +} + + +def from_state_file(state_file): + network_state = None + state = curtin_config.load_config(state_file) + network_state = NetworkState() + network_state.load(state) + + return network_state + + +class NetworkState: + def __init__(self, version=NETWORK_STATE_VERSION, config=None): + self.version = version + self.config = config + self.network_state = { + 'interfaces': {}, + 'routes': [], + 'dns': { + 'nameservers': [], + 'search': [], + } + } + self.command_handlers = self.get_command_handlers() + + def get_command_handlers(self): + METHOD_PREFIX = 'handle_' + methods = filter(lambda x: callable(getattr(self, x)) and + x.startswith(METHOD_PREFIX), dir(self)) + handlers = {} + for m in methods: + key = m.replace(METHOD_PREFIX, '') + handlers[key] = getattr(self, m) + + return handlers + + def dump(self): + state = { + 'version': self.version, + 'config': self.config, + 'network_state': self.network_state, + } + return curtin_config.dump_config(state) + + def load(self, state): + if 'version' not in state: + LOG.error('Invalid state, missing version field') + raise Exception('Invalid state, missing version field') + + required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']] + if not self.valid_command(state, required_keys): + msg = 'Invalid state, missing keys: {}'.format(required_keys) + LOG.error(msg) + raise Exception(msg) + + # v1 - direct attr mapping, except version + for key in [k for k in required_keys if k not in ['version']]: + setattr(self, key, state[key]) + self.command_handlers = self.get_command_handlers() + + def dump_network_state(self): + return curtin_config.dump_config(self.network_state) + + def parse_config(self): + # rebuild network state + for command in self.config: + handler = self.command_handlers.get(command['type']) + handler(command) + + def valid_command(self, command, required_keys): + if not required_keys: + return False + + found_keys = [key for key in command.keys() if key in required_keys] + return len(found_keys) == len(required_keys) + + def handle_physical(self, command): + ''' + command = { + 'type': 'physical', + 'mac_address': 'c0:d6:9f:2c:e8:80', + 'name': 'eth0', + 'subnets': [ + {'type': 'dhcp4'} + ] + } + ''' + required_keys = [ + 'name', + ] + if not self.valid_command(command, required_keys): + LOG.warn('Skipping Invalid command: {}'.format(command)) + LOG.debug(self.dump_network_state()) + return + + interfaces = self.network_state.get('interfaces') + iface = interfaces.get(command['name'], {}) + for param, val in command.get('params', {}).items(): + iface.update({param: val}) + iface.update({ + 'name': command.get('name'), + 'type': command.get('type'), + 'mac_address': command.get('mac_address'), + 'inet': 'inet', + 'mode': 'manual', + 'mtu': command.get('mtu'), + 'address': None, + 'gateway': None, + 'subnets': command.get('subnets'), + }) + self.network_state['interfaces'].update({command.get('name'): iface}) + self.dump_network_state() + + def handle_vlan(self, command): + ''' + auto eth0.222 + iface eth0.222 inet static + address 10.10.10.1 + netmask 255.255.255.0 + vlan-raw-device eth0 + ''' + required_keys = [ + 'name', + 'vlan_link', + 'vlan_id', + ] + if not self.valid_command(command, required_keys): + print('Skipping Invalid command: {}'.format(command)) + print(self.dump_network_state()) + return + + interfaces = self.network_state.get('interfaces') + self.handle_physical(command) + iface = interfaces.get(command.get('name'), {}) + iface['vlan-raw-device'] = command.get('vlan_link') + iface['vlan_id'] = command.get('vlan_id') + interfaces.update({iface['name']: iface}) + + def handle_bond(self, command): + ''' + #/etc/network/interfaces + auto eth0 + iface eth0 inet manual + bond-master bond0 + bond-mode 802.3ad + + auto eth1 + iface eth1 inet manual + bond-master bond0 + bond-mode 802.3ad + + auto bond0 + iface bond0 inet static + address 192.168.0.10 + gateway 192.168.0.1 + netmask 255.255.255.0 + bond-slaves none + bond-mode 802.3ad + bond-miimon 100 + bond-downdelay 200 + bond-updelay 200 + bond-lacp-rate 4 + ''' + required_keys = [ + 'name', + 'bond_interfaces', + 'params', + ] + if not self.valid_command(command, required_keys): + print('Skipping Invalid command: {}'.format(command)) + print(self.dump_network_state()) + return + + self.handle_physical(command) + 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}) + + # handle bond slaves + for ifname in command.get('bond_interfaces'): + if ifname not in interfaces: + cmd = { + 'name': ifname, + 'type': 'bond', + } + # inject placeholder + self.handle_physical(cmd) + + 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}) + + def handle_bridge(self, command): + ''' + auto br0 + iface br0 inet static + address 10.10.10.1 + netmask 255.255.255.0 + bridge_ports eth0 eth1 + bridge_stp off + bridge_fd 0 + bridge_maxwait 0 + + bridge_params = [ + "bridge_ports", + "bridge_ageing", + "bridge_bridgeprio", + "bridge_fd", + "bridge_gcint", + "bridge_hello", + "bridge_hw", + "bridge_maxage", + "bridge_maxwait", + "bridge_pathcost", + "bridge_portprio", + "bridge_stp", + "bridge_waitport", + ] + ''' + required_keys = [ + 'name', + 'bridge_interfaces', + 'params', + ] + if not self.valid_command(command, required_keys): + print('Skipping Invalid command: {}'.format(command)) + print(self.dump_network_state()) + return + + # find one of the bridge port ifaces to get mac_addr + # handle bridge_slaves + interfaces = self.network_state.get('interfaces') + for ifname in command.get('bridge_interfaces'): + if ifname in interfaces: + continue + + cmd = { + 'name': ifname, + } + # inject placeholder + self.handle_physical(cmd) + + interfaces = self.network_state.get('interfaces') + self.handle_physical(command) + iface = interfaces.get(command.get('name'), {}) + iface['bridge_ports'] = command['bridge_interfaces'] + for param, val in command.get('params').items(): + iface.update({param: val}) + + interfaces.update({iface['name']: iface}) + + def handle_nameserver(self, command): + required_keys = [ + 'address', + ] + if not self.valid_command(command, required_keys): + print('Skipping Invalid command: {}'.format(command)) + print(self.dump_network_state()) + return + + dns = self.network_state.get('dns') + if 'address' in command: + addrs = command['address'] + if not type(addrs) == list: + addrs = [addrs] + for addr in addrs: + dns['nameservers'].append(addr) + if 'search' in command: + paths = command['search'] + if not isinstance(paths, list): + paths = [paths] + for path in paths: + dns['search'].append(path) + + def handle_route(self, command): + required_keys = [ + 'destination', + ] + if not self.valid_command(command, required_keys): + print('Skipping Invalid command: {}'.format(command)) + print(self.dump_network_state()) + return + + routes = self.network_state.get('routes') + network, cidr = command['destination'].split("/") + netmask = cidr2mask(int(cidr)) + route = { + 'network': network, + 'netmask': netmask, + 'gateway': command.get('gateway'), + 'metric': command.get('metric'), + } + routes.append(route) + + +def cidr2mask(cidr): + mask = [0, 0, 0, 0] + for i in list(range(0, cidr)): + idx = int(i / 8) + mask[idx] = mask[idx] + (1 << (7 - i % 8)) + return ".".join([str(x) for x in mask]) + + +if __name__ == '__main__': + import sys + import random + from curtin import net + + def load_config(nc): + version = nc.get('version') + config = nc.get('config') + return (version, config) + + def test_parse(network_config): + (version, config) = load_config(network_config) + ns1 = NetworkState(version=version, config=config) + ns1.parse_config() + random.shuffle(config) + ns2 = NetworkState(version=version, config=config) + ns2.parse_config() + print("----NS1-----") + print(ns1.dump_network_state()) + print() + print("----NS2-----") + print(ns2.dump_network_state()) + print("NS1 == NS2 ?=> {}".format( + ns1.network_state == ns2.network_state)) + eni = net.render_interfaces(ns2.network_state) + print(eni) + udev_rules = net.render_persistent_net(ns2.network_state) + print(udev_rules) + + def test_dump_and_load(network_config): + print("Loading network_config into NetworkState") + (version, config) = load_config(network_config) + ns1 = NetworkState(version=version, config=config) + ns1.parse_config() + print("Dumping state to file") + ns1_dump = ns1.dump() + ns1_state = "/tmp/ns1.state" + with open(ns1_state, "w+") as f: + f.write(ns1_dump) + + print("Loading state from file") + ns2 = from_state_file(ns1_state) + print("NS1 == NS2 ?=> {}".format( + ns1.network_state == ns2.network_state)) + + def test_output(network_config): + (version, config) = load_config(network_config) + ns1 = NetworkState(version=version, config=config) + ns1.parse_config() + random.shuffle(config) + ns2 = NetworkState(version=version, config=config) + ns2.parse_config() + print("NS1 == NS2 ?=> {}".format( + ns1.network_state == ns2.network_state)) + eni_1 = net.render_interfaces(ns1.network_state) + eni_2 = net.render_interfaces(ns2.network_state) + print(eni_1) + print(eni_2) + print("eni_1 == eni_2 ?=> {}".format( + eni_1 == eni_2)) + + y = curtin_config.load_config(sys.argv[1]) + network_config = y.get('network') + test_parse(network_config) + test_dump_and_load(network_config) + test_output(network_config) -- cgit v1.2.3 From 098d05b2f1253ba0808772851a1a8e85cb0108a9 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 10 Mar 2016 16:42:27 -0500 Subject: adjust net to fit with cloudinit at this point, this works: python -m cloudinit.net.network_state examples/network-all.yaml --- cloudinit/net/__init__.py | 11 ++++++----- cloudinit/net/network_state.py | 17 ++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) (limited to 'cloudinit/net/network_state.py') diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index e5ca050e..3cf99604 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -21,12 +21,13 @@ import glob import os import re -from curtin.log import LOG -from curtin.udev import generate_udev_rule -import curtin.util as util -import curtin.config as config +from cloudinit import log as logging +from cloudinit import util +from .udev import generate_udev_rule from . import network_state +LOG = logging.getLogger(__name__) + SYS_CLASS_NET = "/sys/class/net/" NET_CONFIG_OPTIONS = [ @@ -272,7 +273,7 @@ def parse_net_config(path): """Parses a curtin network configuration file and return network state""" ns = None - net_config = config.load_config(path) + net_config = util.read_conf(path) if 'network' in net_config: ns = parse_net_config_data(net_config.get('network')) diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index 83303317..df04c526 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -15,8 +15,11 @@ # You should have received a copy of the GNU Affero General Public License # along with Curtin. If not, see . -from curtin.log import LOG -import curtin.config as curtin_config +from cloudinit import log as logging +from cloudinit import util +from cloudinit.util import yaml_dumps as dump_config + +LOG = logging.getLogger(__name__) NETWORK_STATE_VERSION = 1 NETWORK_STATE_REQUIRED_KEYS = { @@ -26,7 +29,7 @@ NETWORK_STATE_REQUIRED_KEYS = { def from_state_file(state_file): network_state = None - state = curtin_config.load_config(state_file) + state = util.read_conf(state_file) network_state = NetworkState() network_state.load(state) @@ -64,7 +67,7 @@ class NetworkState: 'config': self.config, 'network_state': self.network_state, } - return curtin_config.dump_config(state) + return dump_config(state) def load(self, state): if 'version' not in state: @@ -83,7 +86,7 @@ class NetworkState: self.command_handlers = self.get_command_handlers() def dump_network_state(self): - return curtin_config.dump_config(self.network_state) + return dump_config(self.network_state) def parse_config(self): # rebuild network state @@ -335,7 +338,7 @@ def cidr2mask(cidr): if __name__ == '__main__': import sys import random - from curtin import net + from cloudinit import net def load_config(nc): version = nc.get('version') @@ -393,7 +396,7 @@ if __name__ == '__main__': print("eni_1 == eni_2 ?=> {}".format( eni_1 == eni_2)) - y = curtin_config.load_config(sys.argv[1]) + y = util.read_conf(sys.argv[1]) network_config = y.get('network') test_parse(network_config) test_dump_and_load(network_config) -- cgit v1.2.3 From 2b85dabb802766e0b3b1949d744c8860c0cb838a Mon Sep 17 00:00:00 2001 From: Ryan Harper Date: Wed, 23 Mar 2016 11:05:22 -0500 Subject: configdata: parse and convert openstack network_data json to network_config --- cloudinit/net/__init__.py | 34 +++-- cloudinit/net/network_state.py | 45 ++++++- cloudinit/sources/DataSourceConfigDrive.py | 137 +++++++++++++++++++++ cloudinit/sources/helpers/openstack.py | 34 ++++- .../unittests/test_datasource/test_configdrive.py | 50 +++++++- 5 files changed, 289 insertions(+), 11 deletions(-) (limited to 'cloudinit/net/network_state.py') diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index ae7b1c04..76cd4e8b 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -336,7 +336,7 @@ def iface_add_attrs(iface): 'index', 'subnets', ] - if iface['type'] not in ['bond', 'bridge']: + if iface['type'] not in ['bond', 'bridge', 'vlan']: ignore_map.append('mac_address') for key, value in iface.items(): @@ -348,19 +348,34 @@ def iface_add_attrs(iface): return content -def render_route(route): - content = "up route add" +def render_route(route, indent=""): + content = "" + up = indent + "post-up route add" + down = indent + "pre-down route del" + eol = " || true\n" mapping = { 'network': '-net', 'netmask': 'netmask', 'gateway': 'gw', 'metric': 'metric', } - for k in ['network', 'netmask', 'gateway', 'metric']: - if k in route: - content += " %s %s" % (mapping[k], route[k]) + 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 + 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 + 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 += '\n' return content @@ -384,6 +399,7 @@ def render_interfaces(network_state): if len(value): content += " dns-{} {}\n".format(dnskey, " ".join(value)) + content += "\n" for iface in sorted(interfaces.values(), key=lambda k: (order[k['type']], k['name'])): content += "auto {name}\n".format(**iface) @@ -409,6 +425,8 @@ def render_interfaces(network_state): content += iface_add_subnet(iface, subnet) content += iface_add_attrs(iface) + for route in subnet.get('routes', []): + content += render_route(route, indent=" ") content += "\n" else: content += "iface {name} {inet} {mode}\n".format(**iface) @@ -419,7 +437,7 @@ def render_interfaces(network_state): content += render_route(route) # global replacements until v2 format - content = content.replace('mac_address', 'hwaddress') + content = content.replace('mac_address', 'hwaddress ether') return content diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index df04c526..e32d2cdf 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -124,6 +124,17 @@ class NetworkState: iface = interfaces.get(command['name'], {}) for param, val in command.get('params', {}).items(): iface.update({param: val}) + + # convert subnet ipv6 netmask to cidr as needed + subnets = command.get('subnets') + if subnets: + for subnet in subnets: + if subnet['type'] == 'static': + if 'netmask' in subnet and ':' in subnet['address']: + subnet['netmask'] = mask2cidr(subnet['netmask']) + for route in subnet.get('routes', []): + if 'netmask' in route: + route['netmask'] = mask2cidr(route['netmask']) iface.update({ 'name': command.get('name'), 'type': command.get('type'), @@ -133,7 +144,7 @@ class NetworkState: 'mtu': command.get('mtu'), 'address': None, 'gateway': None, - 'subnets': command.get('subnets'), + 'subnets': subnets, }) self.network_state['interfaces'].update({command.get('name'): iface}) self.dump_network_state() @@ -144,6 +155,7 @@ class NetworkState: iface eth0.222 inet static address 10.10.10.1 netmask 255.255.255.0 + hwaddress ether BC:76:4E:06:96:B3 vlan-raw-device eth0 ''' required_keys = [ @@ -335,6 +347,37 @@ def cidr2mask(cidr): return ".".join([str(x) for x in mask]) +def ipv4mask2cidr(mask): + if '.' not in mask: + return mask + return sum([bin(int(x)).count('1') for x in mask.split('.')]) + + +def ipv6mask2cidr(mask): + if ':' not in mask: + return mask + + bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00, + 0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc, + 0xfffe, 0xffff] + cidr = 0 + for word in mask.split(':'): + if not word or int(word, 16) == 0: + break + cidr += bitCount.index(int(word, 16)) + + return cidr + + +def mask2cidr(mask): + if ':' in mask: + return ipv6mask2cidr(mask) + elif '.' in mask: + return ipv4mask2cidr(mask) + else: + return mask + + if __name__ == '__main__': import sys import random diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index 6fc9e05b..d84fab54 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy import os from cloudinit import log as logging @@ -50,6 +51,8 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource): self.seed_dir = os.path.join(paths.seed_dir, 'config_drive') self.version = None self.ec2_metadata = None + self._network_config = None + self.network_json = None self.files = {} def __str__(self): @@ -144,12 +147,27 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource): LOG.warn("Invalid content in vendor-data: %s", e) self.vendordata_raw = None + nd = results.get('networkdata') + self.networkdata_pure = nd + try: + self.network_json = openstack.convert_networkdata_json(nd) + except ValueError as e: + LOG.warn("Invalid content in network-data: %s", e) + self.network_json = None + + if self.network_json: + self._network_config = convert_network_data(self.network_json) + return True def check_instance_id(self): # quickly (local check only) if self.instance_id is still valid return sources.instance_id_matches_system_uuid(self.get_instance_id()) + @property + def network_config(self): + return self._network_config + class DataSourceConfigDriveNet(DataSourceConfigDrive): def __init__(self, sys_cfg, distro, paths): @@ -287,3 +305,122 @@ datasources = [ # Return a list of data sources that match this set of dependencies def get_datasource_list(depends): return sources.list_from_depends(depends, datasources) + + +# Convert OpenStack ConfigDrive NetworkData json to network_config yaml +def convert_network_data(network_json=None): + """Return a dictionary of network_config by parsing provided + OpenStack ConfigDrive NetworkData json format + + OpenStack network_data.json provides a 3 element dictionary + - "links" (links are network devices, physical or virtual) + - "networks" (networks are ip network configurations for one or more + links) + - services (non-ip services, like dns) + + networks and links are combined via network items referencing specific + links via a 'link_id' which maps to a links 'id' field. + + To convert this format to network_config yaml, we first iterate over the + links and then walk the network list to determine if any of the networks + utilize the current link; if so we generate a subnet entry for the device + + We also need to map network_data.json fields to network_config fields. For + example, the network_data links 'id' field is equivalent to network_config + 'name' field for devices. We apply more of this mapping to the various + link types that we encounter. + + There are additional fields that are populated in the network_data.json + from OpenStack that are not relevant to network_config yaml, so we + enumerate a dictionary of valid keys for network_yaml and apply filtering + to drop these superflous keys from the network_config yaml. + """ + if network_json is None: + return None + + # dict of network_config key for filtering network_json + valid_keys = { + 'physical': [ + 'name', + 'type', + 'mac_address', + 'subnets', + 'params', + ], + 'subnet': [ + 'type', + 'address', + 'netmask', + 'broadcast', + 'metric', + 'gateway', + 'pointopoint', + 'mtu', + 'scope', + 'dns_nameservers', + 'dns_search', + 'routes', + ], + } + + links = network_json.get('links', []) + networks = network_json.get('networks', []) + services = network_json.get('services', []) + + config = [] + for link in links: + subnets = [] + cfg = {k: v for k, v in link.items() + if k in valid_keys['physical']} + cfg.update({'name': link['id']}) + for network in [net for net in networks + if net['link'] == link['id']]: + subnet = {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({ + 'type': t, + }) + else: + subnet.update({ + 'type': 'static', + 'address': network.get('ip_address'), + }) + subnets.append(subnet) + cfg.update({'subnets': subnets}) + if link['type'] in ['ethernet', 'vif', 'ovs']: + cfg.update({ + 'type': 'physical', + 'mac_address': link['ethernet_mac_address']}) + elif link['type'] in ['bond']: + params = {} + for k, v in link.items(): + if k == 'bond_links': + continue + elif k.startswith('bond'): + params.update({k: v}) + cfg.update({ + 'bond_interfaces': copy.deepcopy(link['bond_links']), + 'params': params, + }) + elif link['type'] in ['vlan']: + cfg.update({ + 'name': "%s.%s" % (link['vlan_link'], + link['vlan_id']), + 'vlan_link': link['vlan_link'], + 'vlan_id': link['vlan_id'], + 'mac_address': link['vlan_mac_address'], + }) + else: + raise ValueError( + 'Unknown network_data link type: %s' % link['type']) + + config.append(cfg) + + for service in services: + cfg = service + cfg.update({'type': 'nameserver'}) + config.append(cfg) + + return {'version': 1, 'config': config} diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py index bd93d22f..eb50a7be 100644 --- a/cloudinit/sources/helpers/openstack.py +++ b/cloudinit/sources/helpers/openstack.py @@ -51,11 +51,13 @@ OS_LATEST = 'latest' OS_FOLSOM = '2012-08-10' OS_GRIZZLY = '2013-04-04' OS_HAVANA = '2013-10-17' +OS_KILO = '2015-10-15' # keep this in chronological order. new supported versions go at the end. OS_VERSIONS = ( OS_FOLSOM, OS_GRIZZLY, OS_HAVANA, + OS_KILO, ) @@ -229,6 +231,11 @@ class BaseReader(object): False, load_json_anytype, ) + files['networkdata'] = ( + self._path_join("openstack", version, 'network_data.json'), + False, + load_json_anytype, + ) return files results = { @@ -334,7 +341,7 @@ class ConfigDriveReader(BaseReader): path = self._path_join(self.base_path, 'openstack') found = [d for d in os.listdir(path) if os.path.isdir(os.path.join(path))] - self._versions = found + self._versions = sorted(found) return self._versions def _read_ec2_metadata(self): @@ -490,3 +497,28 @@ def convert_vendordata_json(data, recurse=True): recurse=False) raise ValueError("vendordata['cloud-init'] cannot be dict") raise ValueError("Unknown data type for vendordata: %s" % type(data)) + + +def convert_networkdata_json(data, recurse=True): + """ data: a loaded json *object* (strings, arrays, dicts). + return something suitable for cloudinit networkdata_raw. + + if data is: + None: return None + string: return string + list: return data + the list is then processed in UserDataProcessor + dict: return convert_networkdata_json(data.get('cloud-init')) + """ + if not data: + return None + if isinstance(data, six.string_types): + return data + if isinstance(data, list): + return copy.deepcopy(data) + if isinstance(data, dict): + if recurse is True: + return convert_networkdata_json(data.get('cloud-init'), + recurse=False) + raise ValueError("networkdata['cloud-init'] cannot be dict") + raise ValueError("Unknown data type for networkdata: %s" % type(data)) diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py index bfd787d1..01f8c5ce 100644 --- a/tests/unittests/test_datasource/test_configdrive.py +++ b/tests/unittests/test_datasource/test_configdrive.py @@ -59,6 +59,34 @@ OSTACK_META = { CONTENT_0 = b'This is contents of /etc/foo.cfg\n' CONTENT_1 = b'# this is /etc/bar/bar.cfg\n' +NETWORK_DATA = { + 'services': [ + {'type': 'dns', 'address': '199.204.44.24'}, + {'type': 'dns', 'address': '199.204.47.54'} + ], + 'links': [ + {'vif_id': '2ecc7709-b3f7-4448-9580-e1ec32d75bbd', + 'ethernet_mac_address': 'fa:16:3e:69:b0:58', + 'type': 'ovs', 'mtu': None, 'id': 'tap2ecc7709-b3'}, + {'vif_id': '2f88d109-5b57-40e6-af32-2472df09dc33', + 'ethernet_mac_address': 'fa:16:3e:d4:57:ad', + 'type': 'ovs', 'mtu': None, 'id': 'tap2f88d109-5b'}, + {'vif_id': '1a5382f8-04c5-4d75-ab98-d666c1ef52cc', + 'ethernet_mac_address': 'fa:16:3e:05:30:fe', + 'type': 'ovs', 'mtu': None, 'id': 'tap1a5382f8-04'} + ], + 'networks': [ + {'link': 'tap2ecc7709-b3', 'type': 'ipv4_dhcp', + 'network_id': '6d6357ac-0f70-4afa-8bd7-c274cc4ea235', + 'id': 'network0'}, + {'link': 'tap2f88d109-5b', 'type': 'ipv4_dhcp', + 'network_id': 'd227a9b3-6960-4d94-8976-ee5788b44f54', + 'id': 'network1'}, + {'link': 'tap1a5382f8-04', 'type': 'ipv4_dhcp', + 'network_id': 'dab2ba57-cae2-4311-a5ed-010b263891f5', + 'id': 'network2'} + ] +} CFG_DRIVE_FILES_V2 = { 'ec2/2009-04-04/meta-data.json': json.dumps(EC2_META), @@ -70,7 +98,11 @@ CFG_DRIVE_FILES_V2 = { 'openstack/content/0000': CONTENT_0, 'openstack/content/0001': CONTENT_1, 'openstack/latest/meta_data.json': json.dumps(OSTACK_META), - 'openstack/latest/user_data': USER_DATA} + 'openstack/latest/user_data': USER_DATA, + 'openstack/latest/network_data.json': json.dumps(NETWORK_DATA), + 'openstack/2015-10-15/meta_data.json': json.dumps(OSTACK_META), + 'openstack/2015-10-15/user_data': USER_DATA, + 'openstack/2015-10-15/network_data.json': json.dumps(NETWORK_DATA)} class TestConfigDriveDataSource(TestCase): @@ -225,6 +257,7 @@ class TestConfigDriveDataSource(TestCase): self.assertEqual(USER_DATA, found['userdata']) self.assertEqual(expected_md, found['metadata']) + self.assertEqual(NETWORK_DATA, found['networkdata']) self.assertEqual(found['files']['/etc/foo.cfg'], CONTENT_0) self.assertEqual(found['files']['/etc/bar/bar.cfg'], CONTENT_1) @@ -321,6 +354,19 @@ class TestConfigDriveDataSource(TestCase): self.assertEqual(myds.get_public_ssh_keys(), [OSTACK_META['public_keys']['mykey']]) + def test_network_data_is_found(self): + """Verify that network_data is present in ds in config-drive-v2.""" + populate_dir(self.tmp, CFG_DRIVE_FILES_V2) + myds = cfg_ds_from_dir(self.tmp) + self.assertEqual(myds.network_json, NETWORK_DATA) + + def test_network_config_is_converted(self): + """Verify that network_data is converted and present on ds object.""" + populate_dir(self.tmp, CFG_DRIVE_FILES_V2) + myds = cfg_ds_from_dir(self.tmp) + network_config = ds.convert_network_data(NETWORK_DATA) + self.assertEqual(myds.network_config, network_config) + def cfg_ds_from_dir(seed_d): found = ds.read_config_drive(seed_d) @@ -339,6 +385,8 @@ def populate_ds_from_read_config(cfg_ds, source, results): cfg_ds.ec2_metadata = results.get('ec2-metadata') cfg_ds.userdata_raw = results.get('userdata') cfg_ds.version = results.get('version') + cfg_ds.network_json = results.get('networkdata') + cfg_ds._network_config = ds.convert_network_data(cfg_ds.network_json) def populate_dir(seed_dir, files): -- cgit v1.2.3