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):