Created
April 12, 2016 17:16
-
-
Save mgagne/46748012efa1ff3389b380a25bedb14d to your computer and use it in GitHub Desktop.
cloud-init network_info.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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