diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 55d6bcb..6e58edd 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):
@@ -115,6 +121,12 @@ class Distro(object):
return _get_package_mirror_info(availability_zone=availability_zone,
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..f2216ba 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,91 @@ class Distro(distros.Distro):
self.update_package_sources()
self.package_command('install', pkgs=pkglist)
+ def _debian_network_json(self, settings):
+ 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 = []
+ 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.append(bond['id'])
+ devs.extend(slaves)
+ chunk.append("auto {0}".format(bond['id']))
+ chunk.append("iface {0} inet manual".format(bond['id']))
+ if bond.has_key('bond_mode'):
+ chunk.append(' bond-mode {0}'.format(bond['bond_mode']))
+ if bond.has_key('bond_xmit_hash_policy'):
+ chunk.append(' bond_xmit_hash_policy {0}'.format(bond['bond_xmit_hash_policy']))
+ if bond.has_key('bond_miimon'):
+ chunk.append(' bond-miimon {0}'.format(bond['bond_miimon']))
+ 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 link.has_key('mtu'):
+ 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..ca7672e 100644
--- a/cloudinit/distros/net_util.py
+++ b/cloudinit/distros/net_util.py
@@ -21,6 +21,8 @@
# along with this program. If not, see .
+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":
+ 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/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 .
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\w+).*?(?:HWaddr|ether) (?P[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 0c35f83..6ff5d68 100644
--- a/cloudinit/sources/DataSourceConfigDrive.py
+++ b/cloudinit/sources/DataSourceConfigDrive.py
@@ -19,6 +19,7 @@
# along with this program. If not, see .
import os
+import json
from cloudinit import log as logging
from cloudinit import sources
@@ -125,7 +126,19 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
self.userdata_raw = results.get('userdata')
self.version = results['version']
self.files.update(results.get('files', {}))
- self.vendordata_raw = results.get('vendordata')
+
+ # if vendordata includes 'cloud-init', then read that explicitly
+ # for cloud-init (for namespacing).
+ vd = results.get('vendordata')
+ if isinstance(vd, dict):
+ if 'cloud-init' in vd:
+ self.vendordata_raw = vd['cloud-init']
+ else:
+ # TODO(pquerna): this is so wrong.
+ self.vendordata_raw = json.dumps(vd)
+ else:
+ self.vendordata_raw = vd
+
return True
@@ -160,7 +173,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="2013-10-17"):
reader = openstack.ConfigDriveReader(source_dir)
finders = [
(reader.read_v2, [], {'version': version}),
@@ -191,10 +204,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('vendordata', {}).get('network_info')
+ 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 0970d07..615455e 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
import time
+import json
from cloudinit import log as logging
from cloudinit import sources
@@ -146,8 +147,12 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
# if vendordata includes 'cloud-init', then read that explicitly
# for cloud-init (for namespacing).
vd = results.get('vendordata')
- if isinstance(vd, dict) and 'cloud-init' in vd:
- self.vendordata_raw = vd['cloud-init']
+ if isinstance(vd, dict):
+ if 'cloud-init' in vd:
+ self.vendordata_raw = vd['cloud-init']
+ else:
+ # TODO(pquerna): this is so wrong.
+ self.vendordata_raw = json.dumps(vd)
else:
self.vendordata_raw = vd
diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py
index 3a64430..dd2bcbc 100644
--- a/tests/unittests/test_datasource/test_openstack.py
+++ b/tests/unittests/test_datasource/test_openstack.py
@@ -241,7 +241,7 @@ class TestOpenStackDataSource(test_helpers.TestCase):
self.assertEquals(EC2_META, ds_os.ec2_metadata)
self.assertEquals(USER_DATA, ds_os.userdata_raw)
self.assertEquals(2, len(ds_os.files))
- self.assertEquals(VENDOR_DATA, ds_os.vendordata_raw)
+ self.assertEquals(VENDOR_DATA, json.loads(ds_os.vendordata_raw))
@hp.activate
def test_bad_datasource_meta(self):