| From: =?utf-8?q?Mathieu_Gagne=CC=81?= <mgagne@iweb.com> | |
| Date: Wed, 4 Nov 2015 13:37:57 -0500 | |
| Subject: Add support for network_data.json from configdrive | |
| --- | |
| cloudinit/distros/__init__.py | 12 +++ | |
| cloudinit/distros/debian.py | 92 ++++++++++++++++++++- | |
| cloudinit/distros/net_util.py | 34 ++++++++ | |
| cloudinit/distros/rhel.py | 127 +++++++++++++++++++++++++++++ | |
| cloudinit/distros/ubuntu.py | 5 +- | |
| cloudinit/netinfo.py | 16 ++++ | |
| cloudinit/sources/DataSourceConfigDrive.py | 18 +++- | |
| cloudinit/sources/DataSourceOpenStack.py | 1 + | |
| cloudinit/sources/helpers/openstack.py | 5 ++ | |
| 9 files changed, 306 insertions(+), 4 deletions(-) | |
| diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py | |
| index 90dee75..cff4185 100644 | |
| --- a/cloudinit/distros/__init__.py | |
| +++ b/cloudinit/distros/__init__.py | |
| @@ -72,6 +72,12 @@ class Distro(object): | |
| # to write this blob out in a distro format | |
| raise NotImplementedError() | |
| + #@abc.abstractmethod | |
| + def _write_network_json(self, settings): | |
| + # In the future use the http://fedorahosted.org/netcf/ | |
| + # to write this blob out in a distro format | |
| + raise NotImplementedError() | |
| + | |
| def _find_tz_file(self, tz): | |
| tz_file = os.path.join(self.tz_zone_dir, str(tz)) | |
| if not os.path.isfile(tz_file): | |
| @@ -114,6 +120,12 @@ class Distro(object): | |
| return _get_package_mirror_info(data_source=data_source, | |
| mirror_info=arch_info) | |
| + def apply_network_json(self, settings, bring_up=True): | |
| + dev_names = self._write_network_json(settings) | |
| + if bring_up: | |
| + return self._bring_up_interfaces(dev_names) | |
| + return False | |
| + | |
| def apply_network(self, settings, bring_up=True): | |
| # Write it out | |
| dev_names = self._write_network(settings) | |
| diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py | |
| index 1ae232f..b8e5798 100644 | |
| --- a/cloudinit/distros/debian.py | |
| +++ b/cloudinit/distros/debian.py | |
| @@ -28,7 +28,7 @@ from cloudinit import log as logging | |
| from cloudinit import util | |
| from cloudinit.distros.parsers.hostname import HostnameConf | |
| - | |
| +from cloudinit.distros.net_util import NetConfHelper | |
| from cloudinit.settings import PER_INSTANCE | |
| LOG = logging.getLogger(__name__) | |
| @@ -74,6 +74,96 @@ class Distro(distros.Distro): | |
| self.update_package_sources() | |
| self.package_command('install', pkgs=pkglist) | |
| + def _debian_network_json(self, settings, ubuntu_compat=False): | |
| + devs = [] | |
| + nc = NetConfHelper(settings) | |
| + lines = [] | |
| + | |
| + lines.append("# Created by cloud-init on instance boot.") | |
| + lines.append("#") | |
| + lines.append("# This file describes the network interfaces available on your system") | |
| + lines.append("# and how to activate them. For more information, see interfaces(5).") | |
| + lines.append("") | |
| + lines.append("# The loopback network interface") | |
| + lines.append("auto lo") | |
| + lines.append("iface lo inet loopback") | |
| + lines.append("") | |
| + | |
| + bonds = nc.get_links_by_type('bond') | |
| + for bond in bonds: | |
| + chunk = [] | |
| + if ubuntu_compat: | |
| + slaves = [nc.get_link_devname(nc.get_link_by_name(x)) | |
| + for x in bond['bond_links']] | |
| + for slave in slaves: | |
| + chunk.append("auto {0}".format(slave)) | |
| + chunk.append("iface {0} inet manual".format(slave)) | |
| + chunk.append(" bond-master {0}".format(bond['id'])) | |
| + chunk.append("") | |
| + devs.extend(slaves) | |
| + devs.append(bond['id']) | |
| + chunk.append("auto {0}".format(bond['id'])) | |
| + chunk.append("iface {0} inet manual".format(bond['id'])) | |
| + if 'bond_mode' in bond: | |
| + chunk.append(' bond-mode {0}'.format(bond['bond_mode'])) | |
| + if 'bond_xmit_hash_policy' in bond: | |
| + chunk.append(' bond_xmit_hash_policy {0}'.format(bond['bond_xmit_hash_policy'])) | |
| + if 'bond_miimon' in bond: | |
| + chunk.append(' bond-miimon {0}'.format(bond['bond_miimon'])) | |
| + if ubuntu_compat: | |
| + chunk.append(' bond-slaves none') | |
| + else: | |
| + chunk.append(' bond-slaves {0}'.format(' '.join(slaves))) | |
| + chunk.append("") | |
| + lines.extend(chunk) | |
| + | |
| + dns = nc.get_dns_servers() | |
| + networks = nc.get_networks() | |
| + for net in networks: | |
| + # only have support for ipv4 so far. | |
| + if net['type'] != "ipv4": | |
| + continue | |
| + | |
| + link = nc.get_link_by_name(net['link']) | |
| + devname = nc.get_link_devname(link) | |
| + chunk = [] | |
| + chunk.append("# network: {0}".format(net['id'])) | |
| + chunk.append("# network_id: {0}".format(net['network_id'])) | |
| + chunk.append("auto {0}".format(devname)) | |
| + chunk.append("iface {0} inet static".format(devname)) | |
| + | |
| + devs.append(devname) | |
| + if link['type'] == "vlan": | |
| + chunk.append(" vlan_raw_device {0}".format(devname[:devname.rfind('.')])) | |
| + chunk.append(" hwaddress ether {0}".format(link['ethernet_mac_address'])) | |
| + if 'mtu' in link: | |
| + chunk.append(' mtu {0}'.format(link['mtu'])) | |
| + | |
| + chunk.append(" address {0}".format(net['ip_address'])) | |
| + chunk.append(" netmask {0}".format(net['netmask'])) | |
| + gwroute = [route for route in net['routes'] if route['network'] == '0.0.0.0'] | |
| + # TODO: hmmm | |
| + if len(gwroute) == 1: | |
| + chunk.append(" gateway {0}".format(gwroute[0]['gateway'])) | |
| + chunk.append(" dns-nameservers {0}".format(" ".join(dns))) | |
| + | |
| + for route in net['routes']: | |
| + if route['network'] == '0.0.0.0': | |
| + continue | |
| + chunk.append(" post-up route add -net {0} netmask {1} gw {2} || true".format(route['network'], | |
| + route['netmask'], route['gateway'])) | |
| + chunk.append(" pre-down route del -net {0} netmask {1} gw {2} || true".format(route['network'], | |
| + route['netmask'], route['gateway'])) | |
| + chunk.append("") | |
| + lines.extend(chunk) | |
| + return {'/etc/network/interfaces': "\n".join(lines)}, devs | |
| + | |
| + def _write_network_json(self, settings): | |
| + files, devs = self._debian_network_json(settings) | |
| + for (fn, data) in files.iteritems(): | |
| + util.write_file(fn, data) | |
| + return devs | |
| + | |
| def _write_network(self, settings): | |
| util.write_file(self.network_conf_fn, settings) | |
| return ['all'] | |
| diff --git a/cloudinit/distros/net_util.py b/cloudinit/distros/net_util.py | |
| index b9bcfd8..1774527 100644 | |
| --- a/cloudinit/distros/net_util.py | |
| +++ b/cloudinit/distros/net_util.py | |
| @@ -21,6 +21,8 @@ | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| +from cloudinit.netinfo import find_mac_addresses | |
| + | |
| # This is a util function to translate debian based distro interface blobs as | |
| # given in /etc/network/interfaces to an *somewhat* agnostic format for | |
| # distributions that use other formats. | |
| @@ -161,3 +163,35 @@ def translate_network(settings): | |
| if dev_name in real_ifaces: | |
| real_ifaces[dev_name]['auto'] = True | |
| return real_ifaces | |
| + | |
| +class NetConfHelper(object): | |
| + def __init__(self, settings): | |
| + self._settings = settings | |
| + | |
| + def get_link_by_name(self, name): | |
| + return [x for x in self._settings['links'] if x['id'] == name][0] | |
| + | |
| + def get_links_by_type(self, t): | |
| + return [x for x in self._settings['links'] if x['type'] == t] | |
| + | |
| + def get_link_devname(self, link): | |
| + # TODO: chase vlans/bonds/etc | |
| + if link['type'] == "vlan": | |
| + return "{0}.{1}".format( | |
| + self.get_link_devname( | |
| + self.get_link_by_name(link['vlan_link'])), | |
| + link['vlan_id']) | |
| + if link['type'] == "ethernet" or link['type'] == "phy": | |
| + devs = find_mac_addresses() | |
| + for (dev, mac) in devs.iteritems(): | |
| + if mac == link['ethernet_mac_address']: | |
| + return dev | |
| + raise Exception("Device not found: {0}".format(link)) | |
| + | |
| + return link['id'] | |
| + | |
| + def get_networks(self): | |
| + return self._settings['networks'] | |
| + | |
| + def get_dns_servers(self): | |
| + return [x['address'] for x in self._settings['services'] if x['type'] == "dns"] | |
| diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py | |
| index e8abf11..0806463 100644 | |
| --- a/cloudinit/distros/rhel.py | |
| +++ b/cloudinit/distros/rhel.py | |
| @@ -28,6 +28,7 @@ from cloudinit import util | |
| from cloudinit.distros import net_util | |
| from cloudinit.distros import rhel_util | |
| from cloudinit.settings import PER_INSTANCE | |
| +from cloudinit.distros.net_util import NetConfHelper | |
| LOG = logging.getLogger(__name__) | |
| @@ -62,6 +63,129 @@ class Distro(distros.Distro): | |
| def install_packages(self, pkglist): | |
| self.package_command('install', pkgs=pkglist) | |
| + def _rhel_network_json(self, settings): | |
| + devs = [] | |
| + # depends add redhat-lsb-core | |
| + nc = NetConfHelper(settings) | |
| + iffn = '/etc/sysconfig/network-scripts/ifcfg-{0}' | |
| + routefn = '/etc/sysconfig/network-scripts/route-{0}' | |
| + files = {} | |
| + | |
| + bonds = nc.get_links_by_type('bond') | |
| + for bond in bonds: | |
| + chunk = [] | |
| + fn = iffn.format(bond['id']) | |
| + lines = [] | |
| + lines.append("# Created by cloud-init on instance boot.") | |
| + lines.append("#") | |
| + lines.append("") | |
| + lines.append("DEVICE={0}".format(bond['id'])) | |
| + devs.append(bond['id']) | |
| + lines.append("ONBOOT=yes") | |
| + lines.append("BOOTPROTO=none") | |
| + lines.append("USERCTL=no") | |
| + lines.append("NM_CONTROLLED=no") | |
| + lines.append("TYPE=Ethernet") | |
| + | |
| + opts = [] | |
| + if bond.has_key('bond_mode'): | |
| + opts.append('mode={0}'.format(bond['bond_mode'])) | |
| + if bond.has_key('bond_xmit_hash_policy'): | |
| + opts.append('xmit_hash_policy={0}'.format(bond['bond_xmit_hash_policy'])) | |
| + if bond.has_key('bond_miimon'): | |
| + opts.append('miimon={0}'.format(bond['bond_miimon'])) | |
| + lines.append("BONDING_OPTS=\"{0}\"".format(" ".join(opts))) | |
| + files[fn] = "\n".join(lines) | |
| + | |
| + | |
| + for slave in bond['bond_links']: | |
| + slavelink = nc.get_link_by_name(slave) | |
| + slavedev = nc.get_link_devname(slavelink) | |
| + fn = iffn.format(slavedev) | |
| + lines = [] | |
| + lines.append("# Created by cloud-init on instance boot.") | |
| + lines.append("#") | |
| + lines.append("") | |
| + lines.append("DEVICE={0}".format(slavedev)) | |
| + devs.append(slavedev) | |
| + lines.append("ONBOOT=yes") | |
| + lines.append("BOOTPROTO=none") | |
| + lines.append("USERCTL=no") | |
| + lines.append("NM_CONTROLLED=no") | |
| + lines.append("TYPE=Ethernet") | |
| + lines.append("MASTER={0}".format(bond['id'])) | |
| + lines.append("SLAVE=yes") | |
| + files[fn] = "\n".join(lines) | |
| + | |
| + dns = nc.get_dns_servers() | |
| + networks = nc.get_networks() | |
| + for net in networks: | |
| + # only have support for ipv4 so far. | |
| + if net['type'] != "ipv4": | |
| + continue | |
| + | |
| + link = nc.get_link_by_name(net['link']) | |
| + devname = nc.get_link_devname(link) | |
| + fn = iffn.format(devname) | |
| + | |
| + lines = [] | |
| + lines.append("# Created by cloud-init on instance boot.") | |
| + lines.append("#") | |
| + lines.append("# network: {0}".format(net['id'])) | |
| + lines.append("# network_id: {0}".format(net['network_id'])) | |
| + lines.append("") | |
| + lines.append("DEVICE={0}".format(devname)) | |
| + devs.append(devname) | |
| + if link['type'] == "vlan": | |
| + lines.append("VLAN=yes") | |
| + lines.append("PHYSDEV={0}".format(devname[:devname.rfind('.')])) | |
| + lines.append("MACADDR={0}".format(link['ethernet_mac_address'])) | |
| + if link.has_key('mtu'): | |
| + chunk.append('MTU={0}'.format(link['mtu'])) | |
| + | |
| + lines.append("ONBOOT=yes") | |
| + lines.append("BOOTPROTO=static") | |
| + lines.append("USERCTL=no") | |
| + lines.append("NM_CONTROLLED=no") | |
| + lines.append("TYPE=Ethernet") | |
| + lines.append("IPADDR={0}".format(net['ip_address'])) | |
| + lines.append("NETMASK={0}".format(net['netmask'])) | |
| + | |
| + gwroute = [route for route in net['routes'] if route['network'] == '0.0.0.0'] | |
| + # TODO: hmmm | |
| + if len(gwroute) == 1: | |
| + lines.append("GATEWAY={0}".format(gwroute[0]['gateway'])) | |
| + i = 1 | |
| + for server in dns: | |
| + lines.append("DNS{0}={1}".format(i, server)) | |
| + i += 1 | |
| + | |
| + files[fn] = "\n".join(lines) | |
| + | |
| + i = 0 | |
| + fn = routefn.format(devname) | |
| + lines = [] | |
| + for route in net['routes']: | |
| + if route['network'] == '0.0.0.0': | |
| + continue | |
| + lines.append("ADDRESS{0}={1}".format(i, route['network'])) | |
| + lines.append("NETMASK{0}={1}".format(i, route['netmask'])) | |
| + lines.append("GATEWAY{0}={1}".format(i, route['gateway'])) | |
| + i += 1 | |
| + | |
| + if len(lines) > 0: | |
| + lines.insert(0, "#") | |
| + lines.insert(0, "# Created by cloud-init on instance boot.") | |
| + files[fn] = "\n".join(lines) | |
| + | |
| + return files, devs | |
| + | |
| + def _write_network_json(self, settings): | |
| + files, devs = self._rhel_network_json(settings) | |
| + for (fn, data) in files.iteritems(): | |
| + util.write_file(fn, data) | |
| + return devs | |
| + | |
| def _write_network(self, settings): | |
| # TODO(harlowja) fix this... since this is the ubuntu format | |
| entries = net_util.translate_network(settings) | |
| @@ -99,10 +223,13 @@ class Distro(distros.Distro): | |
| return dev_names | |
| def _dist_uses_systemd(self): | |
| + # TODO(pquerna): Figure out a more portable way of detecting systemd | |
| + # as the active init system. There are other distros out there. | |
| # Fedora 18 and RHEL 7 were the first adopters in their series | |
| (dist, vers) = util.system_info()['dist'][:2] | |
| major = (int)(vers.split('.')[0]) | |
| return ((dist.startswith('Red Hat Enterprise Linux') and major >= 7) | |
| + or (dist.startswith('CentOS Linux') and major >= 7) | |
| or (dist.startswith('Fedora') and major >= 18)) | |
| def apply_locale(self, locale, out_fn=None): | |
| diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py | |
| index c527f24..f2824aa 100644 | |
| --- a/cloudinit/distros/ubuntu.py | |
| +++ b/cloudinit/distros/ubuntu.py | |
| @@ -28,4 +28,7 @@ LOG = logging.getLogger(__name__) | |
| class Distro(debian.Distro): | |
| - pass | |
| + | |
| + def _debian_network_json(self, settings, ubuntu_compat=True): | |
| + return super(Distro, self)._debian_network_json( | |
| + settings, ubuntu_compat) | |
| diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py | |
| index 30b6f3b..85ed30c 100644 | |
| --- a/cloudinit/netinfo.py | |
| +++ b/cloudinit/netinfo.py | |
| @@ -21,6 +21,7 @@ | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| import cloudinit.util as util | |
| +import subprocess | |
| import re | |
| from prettytable import PrettyTable | |
| @@ -187,6 +188,21 @@ def route_pformat(): | |
| return "\n".join(lines) | |
| +_SECTIONS_RE = re.compile(r"\n(?=\w)") | |
| +_IFCONFIG_RE = re.compile(r"^(?P<name>\w+).*?(?:HWaddr|ether) (?P<mac>[a-fA-F0-9:]+)", re.DOTALL) | |
| + | |
| +def _parse_ifconfig_output(stdout): | |
| + result = {} | |
| + for section in _SECTIONS_RE.split(stdout): | |
| + match = _IFCONFIG_RE.match(section) | |
| + if match: | |
| + result[match.group("name")] = match.group("mac").lower() | |
| + return result | |
| + | |
| +def find_mac_addresses(): | |
| + (output, err) = util.subp(["ifconfig", "-a"]) | |
| + return _parse_ifconfig_output(output) | |
| + | |
| def debug_info(prefix='ci-info: '): | |
| lines = [] | |
| netdev_lines = netdev_pformat().splitlines() | |
| diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py | |
| index af21441..a03887b 100644 | |
| --- a/cloudinit/sources/DataSourceConfigDrive.py | |
| +++ b/cloudinit/sources/DataSourceConfigDrive.py | |
| @@ -19,6 +19,7 @@ | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| import os | |
| +import json | |
| from cloudinit import log as logging | |
| from cloudinit import sources | |
| @@ -167,7 +168,7 @@ def get_ds_mode(cfgdrv_ver, ds_cfg=None, user=None): | |
| return "net" | |
| -def read_config_drive(source_dir, version="2012-08-10"): | |
| +def read_config_drive(source_dir, version="2015-10-15"): | |
| reader = openstack.ConfigDriveReader(source_dir) | |
| finders = [ | |
| (reader.read_v2, [], {'version': version}), | |
| @@ -198,10 +199,23 @@ def on_first_boot(data, distro=None): | |
| if not isinstance(data, dict): | |
| raise TypeError("Config-drive data expected to be a dict; not %s" | |
| % (type(data))) | |
| + | |
| + networkapplied = False | |
| + jsonnet_conf = data.get('networkdata', {}) | |
| + if jsonnet_conf: | |
| + try: | |
| + LOG.debug("Updating network interfaces from JSON in config drive") | |
| + distro_user_config = distro.apply_network_json(jsonnet_conf) | |
| + networkapplied = True | |
| + except NotImplementedError: | |
| + LOG.debug("Distro does not implement networking setup via Vendor JSON.") | |
| + pass | |
| + | |
| net_conf = data.get("network_config", '') | |
| - if net_conf and distro: | |
| + if networkapplied is False and net_conf and distro: | |
| LOG.debug("Updating network interfaces from config drive") | |
| distro.apply_network(net_conf) | |
| + | |
| files = data.get('files', {}) | |
| if files: | |
| LOG.debug("Writing %s injected files", len(files)) | |
| diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py | |
| index 27a534c..2fe7496 100644 | |
| --- a/cloudinit/sources/DataSourceOpenStack.py | |
| +++ b/cloudinit/sources/DataSourceOpenStack.py | |
| @@ -17,6 +17,7 @@ | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| import time | |
| +import json | |
| from cloudinit import log as logging | |
| from cloudinit import sources | |
| diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py | |
| index 1ca6695..3495bbc 100644 | |
| --- a/cloudinit/sources/helpers/openstack.py | |
| +++ b/cloudinit/sources/helpers/openstack.py | |
| @@ -220,6 +220,11 @@ class BaseReader(object): | |
| False, | |
| load_json_anytype, | |
| ) | |
| + files['networkdata'] = ( | |
| + self._path_join("openstack", version, 'network_data.json'), | |
| + False, | |
| + util.load_json, | |
| + ) | |
| return files | |
| version = self._find_working_version(version) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment