Skip to content

Instantly share code, notes, and snippets.

@mgagne
Created April 12, 2016 17:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgagne/46748012efa1ff3389b380a25bedb14d to your computer and use it in GitHub Desktop.
Save mgagne/46748012efa1ff3389b380a25bedb14d to your computer and use it in GitHub Desktop.
cloud-init network_info.json
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