From a2249942522d140a063acdc007aced991d4f0588 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Thu, 5 May 2016 14:57:48 -0700
Subject: Work on refactoring (and adding) network conversion tests

---
 cloudinit/net/network_state.py             |  68 ---------------
 cloudinit/sources/DataSourceConfigDrive.py | 127 ++---------------------------
 cloudinit/sources/helpers/openstack.py     | 116 ++++++++++++++++++++++++++
 3 files changed, 121 insertions(+), 190 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index e32d2cdf..d08e94fe 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -376,71 +376,3 @@ def mask2cidr(mask):
         return ipv4mask2cidr(mask)
     else:
         return mask
-
-
-if __name__ == '__main__':
-    import sys
-    import random
-    from cloudinit import net
-
-    def load_config(nc):
-        version = nc.get('version')
-        config = nc.get('config')
-        return (version, config)
-
-    def test_parse(network_config):
-        (version, config) = load_config(network_config)
-        ns1 = NetworkState(version=version, config=config)
-        ns1.parse_config()
-        random.shuffle(config)
-        ns2 = NetworkState(version=version, config=config)
-        ns2.parse_config()
-        print("----NS1-----")
-        print(ns1.dump_network_state())
-        print()
-        print("----NS2-----")
-        print(ns2.dump_network_state())
-        print("NS1 == NS2 ?=> {}".format(
-            ns1.network_state == ns2.network_state))
-        eni = net.render_interfaces(ns2.network_state)
-        print(eni)
-        udev_rules = net.render_persistent_net(ns2.network_state)
-        print(udev_rules)
-
-    def test_dump_and_load(network_config):
-        print("Loading network_config into NetworkState")
-        (version, config) = load_config(network_config)
-        ns1 = NetworkState(version=version, config=config)
-        ns1.parse_config()
-        print("Dumping state to file")
-        ns1_dump = ns1.dump()
-        ns1_state = "/tmp/ns1.state"
-        with open(ns1_state, "w+") as f:
-            f.write(ns1_dump)
-
-        print("Loading state from file")
-        ns2 = from_state_file(ns1_state)
-        print("NS1 == NS2 ?=> {}".format(
-            ns1.network_state == ns2.network_state))
-
-    def test_output(network_config):
-        (version, config) = load_config(network_config)
-        ns1 = NetworkState(version=version, config=config)
-        ns1.parse_config()
-        random.shuffle(config)
-        ns2 = NetworkState(version=version, config=config)
-        ns2.parse_config()
-        print("NS1 == NS2 ?=> {}".format(
-            ns1.network_state == ns2.network_state))
-        eni_1 = net.render_interfaces(ns1.network_state)
-        eni_2 = net.render_interfaces(ns2.network_state)
-        print(eni_1)
-        print(eni_2)
-        print("eni_1 == eni_2 ?=> {}".format(
-            eni_1 == eni_2))
-
-    y = util.read_conf(sys.argv[1])
-    network_config = y.get('network')
-    test_parse(network_config)
-    test_dump_and_load(network_config)
-    test_output(network_config)
diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py
index 52a9f543..70373b43 100644
--- a/cloudinit/sources/DataSourceConfigDrive.py
+++ b/cloudinit/sources/DataSourceConfigDrive.py
@@ -61,7 +61,7 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
         mstr += "[source=%s]" % (self.source)
         return mstr
 
-    def get_data(self):
+    def get_data(self, skip_first_boot=False):
         found = None
         md = {}
         results = {}
@@ -119,7 +119,8 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
         # instance-id
         prev_iid = get_previous_iid(self.paths)
         cur_iid = md['instance-id']
-        if prev_iid != cur_iid and self.dsmode == "local":
+        if prev_iid != cur_iid and \
+           self.dsmode == "local" and not skip_first_boot:
             on_first_boot(results, distro=self.distro)
 
         # dsmode != self.dsmode here if:
@@ -163,7 +164,8 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
     def network_config(self):
         if self._network_config is None:
             if self.network_json is not None:
-                self._network_config = convert_network_data(self.network_json)
+                self._network_config = openstack.convert_net_json(
+                    self.network_json)
         return self._network_config
 
 
@@ -303,122 +305,3 @@ datasources = [
 # Return a list of data sources that match this set of dependencies
 def get_datasource_list(depends):
     return sources.list_from_depends(depends, datasources)
-
-
-# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
-def convert_network_data(network_json=None):
-    """Return a dictionary of network_config by parsing provided
-       OpenStack ConfigDrive NetworkData json format
-
-    OpenStack network_data.json provides a 3 element dictionary
-      - "links" (links are network devices, physical or virtual)
-      - "networks" (networks are ip network configurations for one or more
-                    links)
-      -  services (non-ip services, like dns)
-
-    networks and links are combined via network items referencing specific
-    links via a 'link_id' which maps to a links 'id' field.
-
-    To convert this format to network_config yaml, we first iterate over the
-    links and then walk the network list to determine if any of the networks
-    utilize the current link; if so we generate a subnet entry for the device
-
-    We also need to map network_data.json fields to network_config fields. For
-    example, the network_data links 'id' field is equivalent to network_config
-    'name' field for devices.  We apply more of this mapping to the various
-    link types that we encounter.
-
-    There are additional fields that are populated in the network_data.json
-    from OpenStack that are not relevant to network_config yaml, so we
-    enumerate a dictionary of valid keys for network_yaml and apply filtering
-    to drop these superflous keys from the network_config yaml.
-    """
-    if network_json is None:
-        return None
-
-    # dict of network_config key for filtering network_json
-    valid_keys = {
-        'physical': [
-            'name',
-            'type',
-            'mac_address',
-            'subnets',
-            'params',
-        ],
-        'subnet': [
-            'type',
-            'address',
-            'netmask',
-            'broadcast',
-            'metric',
-            'gateway',
-            'pointopoint',
-            'mtu',
-            'scope',
-            'dns_nameservers',
-            'dns_search',
-            'routes',
-        ],
-    }
-
-    links = network_json.get('links', [])
-    networks = network_json.get('networks', [])
-    services = network_json.get('services', [])
-
-    config = []
-    for link in links:
-        subnets = []
-        cfg = {k: v for k, v in link.items()
-               if k in valid_keys['physical']}
-        cfg.update({'name': link['id']})
-        for network in [net for net in networks
-                        if net['link'] == link['id']]:
-            subnet = {k: v for k, v in network.items()
-                      if k in valid_keys['subnet']}
-            if 'dhcp' in network['type']:
-                t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
-                subnet.update({
-                    'type': t,
-                })
-            else:
-                subnet.update({
-                    'type': 'static',
-                    'address': network.get('ip_address'),
-                })
-            subnets.append(subnet)
-        cfg.update({'subnets': subnets})
-        if link['type'] in ['ethernet', 'vif', 'ovs', 'phy']:
-            cfg.update({
-                'type': 'physical',
-                'mac_address': link['ethernet_mac_address']})
-        elif link['type'] in ['bond']:
-            params = {}
-            for k, v in link.items():
-                if k == 'bond_links':
-                    continue
-                elif k.startswith('bond'):
-                    params.update({k: v})
-            cfg.update({
-                'bond_interfaces': copy.deepcopy(link['bond_links']),
-                'params': params,
-            })
-        elif link['type'] in ['vlan']:
-            cfg.update({
-                'name': "%s.%s" % (link['vlan_link'],
-                                   link['vlan_id']),
-                'vlan_link': link['vlan_link'],
-                'vlan_id': link['vlan_id'],
-                'mac_address': link['vlan_mac_address'],
-            })
-        else:
-            raise ValueError(
-                'Unknown network_data link type: %s' % link['type'])
-
-        config.append(cfg)
-
-    for service in services:
-        cfg = service
-        cfg.update({'type': 'nameserver'})
-        config.append(cfg)
-
-    return {'version': 1, 'config': config}
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 1aa6bbae..475ccab3 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -474,6 +474,122 @@ class MetadataReader(BaseReader):
                                                retries=self.retries)
 
 
+def convert_net_json(network_json):
+    """Return a dictionary of network_config by parsing provided
+       OpenStack ConfigDrive NetworkData json format
+
+    OpenStack network_data.json provides a 3 element dictionary
+      - "links" (links are network devices, physical or virtual)
+      - "networks" (networks are ip network configurations for one or more
+                    links)
+      -  services (non-ip services, like dns)
+
+    networks and links are combined via network items referencing specific
+    links via a 'link_id' which maps to a links 'id' field.
+
+    To convert this format to network_config yaml, we first iterate over the
+    links and then walk the network list to determine if any of the networks
+    utilize the current link; if so we generate a subnet entry for the device
+
+    We also need to map network_data.json fields to network_config fields. For
+    example, the network_data links 'id' field is equivalent to network_config
+    'name' field for devices.  We apply more of this mapping to the various
+    link types that we encounter.
+
+    There are additional fields that are populated in the network_data.json
+    from OpenStack that are not relevant to network_config yaml, so we
+    enumerate a dictionary of valid keys for network_yaml and apply filtering
+    to drop these superflous keys from the network_config yaml.
+    """
+
+    # Dict of network_config key for filtering network_json
+    valid_keys = {
+        'physical': [
+            'name',
+            'type',
+            'mac_address',
+            'subnets',
+            'params',
+        ],
+        'subnet': [
+            'type',
+            'address',
+            'netmask',
+            'broadcast',
+            'metric',
+            'gateway',
+            'pointopoint',
+            'mtu',
+            'scope',
+            'dns_nameservers',
+            'dns_search',
+            'routes',
+        ],
+    }
+
+    links = network_json.get('links', [])
+    networks = network_json.get('networks', [])
+    services = network_json.get('services', [])
+
+    config = []
+    for link in links:
+        subnets = []
+        cfg = {k: v for k, v in link.items()
+               if k in valid_keys['physical']}
+        cfg.update({'name': link['id']})
+        for network in [net for net in networks
+                        if net['link'] == link['id']]:
+            subnet = {k: v for k, v in network.items()
+                      if k in valid_keys['subnet']}
+            if 'dhcp' in network['type']:
+                t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
+                subnet.update({
+                    'type': t,
+                })
+            else:
+                subnet.update({
+                    'type': 'static',
+                    'address': network.get('ip_address'),
+                })
+            subnets.append(subnet)
+        cfg.update({'subnets': subnets})
+        if link['type'] in ['ethernet', 'vif', 'ovs', 'phy']:
+            cfg.update({
+                'type': 'physical',
+                'mac_address': link['ethernet_mac_address']})
+        elif link['type'] in ['bond']:
+            params = {}
+            for k, v in link.items():
+                if k == 'bond_links':
+                    continue
+                elif k.startswith('bond'):
+                    params.update({k: v})
+            cfg.update({
+                'bond_interfaces': copy.deepcopy(link['bond_links']),
+                'params': params,
+            })
+        elif link['type'] in ['vlan']:
+            cfg.update({
+                'name': "%s.%s" % (link['vlan_link'],
+                                   link['vlan_id']),
+                'vlan_link': link['vlan_link'],
+                'vlan_id': link['vlan_id'],
+                'mac_address': link['vlan_mac_address'],
+            })
+        else:
+            raise ValueError(
+                'Unknown network_data link type: %s' % link['type'])
+
+        config.append(cfg)
+
+    for service in services:
+        cfg = copy.deepcopy(service)
+        cfg.update({'type': 'nameserver'})
+        config.append(cfg)
+
+    return {'version': 1, 'config': config}
+
+
 def convert_vendordata_json(data, recurse=True):
     """ data: a loaded json *object* (strings, arrays, dicts).
     return something suitable for cloudinit vendordata_raw.
-- 
cgit v1.2.3


From db9d958a0e76c2c59298041a1355aba97fda000f Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Thu, 5 May 2016 16:38:53 -0700
Subject: Add the bridge net type

---
 cloudinit/net/__init__.py              |  2 +-
 cloudinit/net/network_state.py         | 39 +++++++++++++++++++++-------------
 cloudinit/sources/helpers/openstack.py |  5 +++++
 3 files changed, 30 insertions(+), 16 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 31544fd8..cc154c57 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -262,7 +262,7 @@ def parse_deb_config(path):
 
 
 def parse_net_config_data(net_config):
-    """Parses the config, returns NetworkState dictionary
+    """Parses the config, returns NetworkState object
 
     :param net_config: curtin network config dict
     """
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index d08e94fe..27d35256 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -15,6 +15,8 @@
 #   You should have received a copy of the GNU Affero General Public License
 #   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
 
+import six
+
 from cloudinit import log as logging
 from cloudinit import util
 from cloudinit.util import yaml_dumps as dump_config
@@ -32,11 +34,31 @@ def from_state_file(state_file):
     state = util.read_conf(state_file)
     network_state = NetworkState()
     network_state.load(state)
-
     return network_state
 
 
-class NetworkState:
+class CommandHandlerMeta(type):
+    """Metaclass that dynamically creates a 'command_handlers' attribute.
+
+    This will scan the to-be-created class for methods that start with
+    'handle_' and on finding those will populate a class attribute mapping
+    so that those methods can be quickly located and called.
+    """
+    def __new__(cls, name, parents, dct):
+        command_handlers = {}
+        for attr_name, attr in six.iteritems(dct):
+            if six.callable(attr) and attr_name.startswith('handle_'):
+                handles_what = attr_name[len('handle_'):]
+                if handles_what:
+                    command_handlers[handles_what] = attr
+        dct['command_handlers'] = command_handlers
+        return super(CommandHandlerMeta, cls).__new__(cls, name,
+                                                      parents, dct)
+
+
+@six.add_metaclass(CommandHandlerMeta)
+class NetworkState(object):
+
     def __init__(self, version=NETWORK_STATE_VERSION, config=None):
         self.version = version
         self.config = config
@@ -48,18 +70,6 @@ class NetworkState:
                 'search': [],
             }
         }
-        self.command_handlers = self.get_command_handlers()
-
-    def get_command_handlers(self):
-        METHOD_PREFIX = 'handle_'
-        methods = filter(lambda x: callable(getattr(self, x)) and
-                         x.startswith(METHOD_PREFIX),  dir(self))
-        handlers = {}
-        for m in methods:
-            key = m.replace(METHOD_PREFIX, '')
-            handlers[key] = getattr(self, m)
-
-        return handlers
 
     def dump(self):
         state = {
@@ -83,7 +93,6 @@ class NetworkState:
         # v1 - direct attr mapping, except version
         for key in [k for k in required_keys if k not in ['version']]:
             setattr(self, key, state[key])
-        self.command_handlers = self.get_command_handlers()
 
     def dump_network_state(self):
         return dump_config(self.network_state)
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 475ccab3..845ea971 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -576,6 +576,11 @@ def convert_net_json(network_json):
                 'vlan_id': link['vlan_id'],
                 'mac_address': link['vlan_mac_address'],
             })
+        elif link['type'] in ['bridge']:
+            cfg.update({
+                'type': 'bridge',
+                'mac_address': link['ethernet_mac_address'],
+                'mtu': link['mtu']})
         else:
             raise ValueError(
                 'Unknown network_data link type: %s' % link['type'])
-- 
cgit v1.2.3


From 2c32c855278c22b77b6ac3bfca23fa2fa6bc6330 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Thu, 5 May 2016 17:03:08 -0700
Subject: Use a decorator vs repeated key checks.

---
 cloudinit/net/network_state.py | 136 ++++++++++++++++++++---------------------
 1 file changed, 66 insertions(+), 70 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 27d35256..73be84e1 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -15,6 +15,8 @@
 #   You should have received a copy of the GNU Affero General Public License
 #   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
 
+import copy
+
 import six
 
 from cloudinit import log as logging
@@ -37,6 +39,37 @@ def from_state_file(state_file):
     return network_state
 
 
+class InvalidCommand(Exception):
+    pass
+
+
+def ensure_command_keys(required_keys):
+    required_keys = frozenset(required_keys)
+
+    def extract_missing(command):
+        missing_keys = set()
+        for key in required_keys:
+            if key not in command:
+                missing_keys.add(key)
+        return missing_keys
+
+    def wrapper(func):
+
+        @six.wraps(func)
+        def decorator(self, command, *args, **kwargs):
+            if required_keys:
+                missing_keys = extract_missing(command)
+                if missing_keys:
+                    raise InvalidCommand("Command missing %s of required"
+                                         " keys %s" % (missing_keys,
+                                                       required_keys))
+            return func(self, command, *args, **kwargs)
+
+        return decorator
+
+    return wrapper
+
+
 class CommandHandlerMeta(type):
     """Metaclass that dynamically creates a 'command_handlers' attribute.
 
@@ -59,17 +92,19 @@ class CommandHandlerMeta(type):
 @six.add_metaclass(CommandHandlerMeta)
 class NetworkState(object):
 
+    initial_network_state = {
+        'interfaces': {},
+        'routes': [],
+        'dns': {
+            'nameservers': [],
+            'search': [],
+        }
+    }
+
     def __init__(self, version=NETWORK_STATE_VERSION, config=None):
         self.version = version
         self.config = config
-        self.network_state = {
-            'interfaces': {},
-            'routes': [],
-            'dns': {
-                'nameservers': [],
-                'search': [],
-            }
-        }
+        self.network_state = copy.deepcopy(self.initial_network_state)
 
     def dump(self):
         state = {
@@ -97,19 +132,26 @@ class NetworkState(object):
     def dump_network_state(self):
         return dump_config(self.network_state)
 
-    def parse_config(self):
+    def parse_config(self, skip_broken=True):
         # rebuild network state
         for command in self.config:
-            handler = self.command_handlers.get(command['type'])
-            handler(command)
-
-    def valid_command(self, command, required_keys):
-        if not required_keys:
-            return False
-
-        found_keys = [key for key in command.keys() if key in required_keys]
-        return len(found_keys) == len(required_keys)
-
+            command_type = command['type']
+            try:
+                handler = self.command_handlers[command_type]
+            except KeyError:
+                raise RuntimeError("No handler found for"
+                                   " command '%s'" % command_type)
+            try:
+                handler(command)
+            except InvalidCommand:
+                if not skip_broken:
+                    raise
+                else:
+                    LOG.warn("Skipping invalid command: %s", command,
+                             exc_info=True)
+                    LOG.debug(self.dump_network_state())
+
+    @ensure_command_keys(['name'])
     def handle_physical(self, command):
         '''
         command = {
@@ -121,13 +163,6 @@ class NetworkState(object):
              ]
         }
         '''
-        required_keys = [
-            'name',
-        ]
-        if not self.valid_command(command, required_keys):
-            LOG.warn('Skipping Invalid command: {}'.format(command))
-            LOG.debug(self.dump_network_state())
-            return
 
         interfaces = self.network_state.get('interfaces')
         iface = interfaces.get(command['name'], {})
@@ -158,6 +193,7 @@ class NetworkState(object):
         self.network_state['interfaces'].update({command.get('name'): iface})
         self.dump_network_state()
 
+    @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])
     def handle_vlan(self, command):
         '''
             auto eth0.222
@@ -167,16 +203,6 @@ class NetworkState(object):
                     hwaddress ether BC:76:4E:06:96:B3
                     vlan-raw-device eth0
         '''
-        required_keys = [
-            'name',
-            'vlan_link',
-            'vlan_id',
-        ]
-        if not self.valid_command(command, required_keys):
-            print('Skipping Invalid command: {}'.format(command))
-            print(self.dump_network_state())
-            return
-
         interfaces = self.network_state.get('interfaces')
         self.handle_physical(command)
         iface = interfaces.get(command.get('name'), {})
@@ -184,6 +210,7 @@ class NetworkState(object):
         iface['vlan_id'] = command.get('vlan_id')
         interfaces.update({iface['name']: iface})
 
+    @ensure_command_keys(['name', 'bond_interfaces', 'params'])
     def handle_bond(self, command):
         '''
     #/etc/network/interfaces
@@ -209,15 +236,6 @@ class NetworkState(object):
          bond-updelay 200
          bond-lacp-rate 4
         '''
-        required_keys = [
-            'name',
-            'bond_interfaces',
-            'params',
-        ]
-        if not self.valid_command(command, required_keys):
-            print('Skipping Invalid command: {}'.format(command))
-            print(self.dump_network_state())
-            return
 
         self.handle_physical(command)
         interfaces = self.network_state.get('interfaces')
@@ -245,6 +263,7 @@ class NetworkState(object):
                 bond_if.update({param: val})
             self.network_state['interfaces'].update({ifname: bond_if})
 
+    @ensure_command_keys(['name', 'bridge_interfaces', 'params'])
     def handle_bridge(self, command):
         '''
             auto br0
@@ -272,15 +291,6 @@ class NetworkState(object):
             "bridge_waitport",
         ]
         '''
-        required_keys = [
-            'name',
-            'bridge_interfaces',
-            'params',
-        ]
-        if not self.valid_command(command, required_keys):
-            print('Skipping Invalid command: {}'.format(command))
-            print(self.dump_network_state())
-            return
 
         # find one of the bridge port ifaces to get mac_addr
         # handle bridge_slaves
@@ -304,15 +314,8 @@ class NetworkState(object):
 
         interfaces.update({iface['name']: iface})
 
+    @ensure_command_keys(['address'])
     def handle_nameserver(self, command):
-        required_keys = [
-            'address',
-        ]
-        if not self.valid_command(command, required_keys):
-            print('Skipping Invalid command: {}'.format(command))
-            print(self.dump_network_state())
-            return
-
         dns = self.network_state.get('dns')
         if 'address' in command:
             addrs = command['address']
@@ -327,15 +330,8 @@ class NetworkState(object):
             for path in paths:
                 dns['search'].append(path)
 
+    @ensure_command_keys(['destination'])
     def handle_route(self, command):
-        required_keys = [
-            'destination',
-        ]
-        if not self.valid_command(command, required_keys):
-            print('Skipping Invalid command: {}'.format(command))
-            print(self.dump_network_state())
-            return
-
         routes = self.network_state.get('routes')
         network, cidr = command['destination'].split("/")
         netmask = cidr2mask(int(cidr))
-- 
cgit v1.2.3


From 247fbc9ce9ac6f47d670a19f073bda0a1f746669 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Mon, 9 May 2016 16:27:59 -0700
Subject: Move the current rendering to a debian distro file

This format allows for rendering to work in other distros
and clearly separates the API needed to do this (it also
moves the klibc parsing into its own module so that the
leftover code in net/__init__.py is smaller and only focused
on util code).
---
 cloudinit/net/__init__.py         | 553 +-------------------------------------
 cloudinit/net/distros/__init__.py |   0
 cloudinit/net/distros/debian.py   | 401 +++++++++++++++++++++++++++
 cloudinit/net/klibc.py            | 191 +++++++++++++
 4 files changed, 595 insertions(+), 550 deletions(-)
 create mode 100644 cloudinit/net/distros/__init__.py
 create mode 100644 cloudinit/net/distros/debian.py
 create mode 100644 cloudinit/net/klibc.py

(limited to 'cloudinit')

diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index cc154c57..e911ed0c 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -16,41 +16,18 @@
 #   You should have received a copy of the GNU Affero General Public License
 #   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
 
-import base64
 import errno
-import glob
-import gzip
-import io
 import os
-import re
-import shlex
+
 
 from cloudinit import log as logging
+from cloudinit.net import network_state
 from cloudinit import util
-from .udev import generate_udev_rule
-from . import network_state
 
-LOG = logging.getLogger(__name__)
 
+LOG = logging.getLogger(__name__)
 SYS_CLASS_NET = "/sys/class/net/"
 LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-"
-
-NET_CONFIG_OPTIONS = [
-    "address", "netmask", "broadcast", "network", "metric", "gateway",
-    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
-    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
-    "netnum", "endpoint", "local", "ttl",
-    ]
-
-NET_CONFIG_COMMANDS = [
-    "pre-up", "up", "post-up", "down", "pre-down", "post-down",
-    ]
-
-NET_CONFIG_BRIDGE_OPTIONS = [
-    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
-    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
-    ]
-
 DEFAULT_PRIMARY_INTERFACE = 'eth0'
 
 
@@ -130,137 +107,6 @@ class ParserError(Exception):
     """Raised when parser has issue parsing the interfaces file."""
 
 
-def parse_deb_config_data(ifaces, contents, src_dir, src_path):
-    """Parses the file contents, placing result into ifaces.
-
-    '_source_path' is added to every dictionary entry to define which file
-    the configration information came from.
-
-    :param ifaces: interface dictionary
-    :param contents: contents of interfaces file
-    :param src_dir: directory interfaces file was located
-    :param src_path: file path the `contents` was read
-    """
-    currif = None
-    for line in contents.splitlines():
-        line = line.strip()
-        if line.startswith('#'):
-            continue
-        split = line.split(' ')
-        option = split[0]
-        if option == "source-directory":
-            parsed_src_dir = split[1]
-            if not parsed_src_dir.startswith("/"):
-                parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
-            for expanded_path in glob.glob(parsed_src_dir):
-                dir_contents = os.listdir(expanded_path)
-                dir_contents = [
-                    os.path.join(expanded_path, path)
-                    for path in dir_contents
-                    if (os.path.isfile(os.path.join(expanded_path, path)) and
-                        re.match("^[a-zA-Z0-9_-]+$", path) is not None)
-                ]
-                for entry in dir_contents:
-                    with open(entry, "r") as fp:
-                        src_data = fp.read().strip()
-                    abs_entry = os.path.abspath(entry)
-                    parse_deb_config_data(
-                        ifaces, src_data,
-                        os.path.dirname(abs_entry), abs_entry)
-        elif option == "source":
-            new_src_path = split[1]
-            if not new_src_path.startswith("/"):
-                new_src_path = os.path.join(src_dir, new_src_path)
-            for expanded_path in glob.glob(new_src_path):
-                with open(expanded_path, "r") as fp:
-                    src_data = fp.read().strip()
-                abs_path = os.path.abspath(expanded_path)
-                parse_deb_config_data(
-                    ifaces, src_data,
-                    os.path.dirname(abs_path), abs_path)
-        elif option == "auto":
-            for iface in split[1:]:
-                if iface not in ifaces:
-                    ifaces[iface] = {
-                        # Include the source path this interface was found in.
-                        "_source_path": src_path
-                    }
-                ifaces[iface]['auto'] = True
-        elif option == "iface":
-            iface, family, method = split[1:4]
-            if iface not in ifaces:
-                ifaces[iface] = {
-                    # Include the source path this interface was found in.
-                    "_source_path": src_path
-                }
-            elif 'family' in ifaces[iface]:
-                raise ParserError(
-                    "Interface %s can only be defined once. "
-                    "Re-defined in '%s'." % (iface, src_path))
-            ifaces[iface]['family'] = family
-            ifaces[iface]['method'] = method
-            currif = iface
-        elif option == "hwaddress":
-            ifaces[currif]['hwaddress'] = split[1]
-        elif option in NET_CONFIG_OPTIONS:
-            ifaces[currif][option] = split[1]
-        elif option in NET_CONFIG_COMMANDS:
-            if option not in ifaces[currif]:
-                ifaces[currif][option] = []
-            ifaces[currif][option].append(' '.join(split[1:]))
-        elif option.startswith('dns-'):
-            if 'dns' not in ifaces[currif]:
-                ifaces[currif]['dns'] = {}
-            if option == 'dns-search':
-                ifaces[currif]['dns']['search'] = []
-                for domain in split[1:]:
-                    ifaces[currif]['dns']['search'].append(domain)
-            elif option == 'dns-nameservers':
-                ifaces[currif]['dns']['nameservers'] = []
-                for server in split[1:]:
-                    ifaces[currif]['dns']['nameservers'].append(server)
-        elif option.startswith('bridge_'):
-            if 'bridge' not in ifaces[currif]:
-                ifaces[currif]['bridge'] = {}
-            if option in NET_CONFIG_BRIDGE_OPTIONS:
-                bridge_option = option.replace('bridge_', '', 1)
-                ifaces[currif]['bridge'][bridge_option] = split[1]
-            elif option == "bridge_ports":
-                ifaces[currif]['bridge']['ports'] = []
-                for iface in split[1:]:
-                    ifaces[currif]['bridge']['ports'].append(iface)
-            elif option == "bridge_hw" and split[1].lower() == "mac":
-                ifaces[currif]['bridge']['mac'] = split[2]
-            elif option == "bridge_pathcost":
-                if 'pathcost' not in ifaces[currif]['bridge']:
-                    ifaces[currif]['bridge']['pathcost'] = {}
-                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
-            elif option == "bridge_portprio":
-                if 'portprio' not in ifaces[currif]['bridge']:
-                    ifaces[currif]['bridge']['portprio'] = {}
-                ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
-        elif option.startswith('bond-'):
-            if 'bond' not in ifaces[currif]:
-                ifaces[currif]['bond'] = {}
-            bond_option = option.replace('bond-', '', 1)
-            ifaces[currif]['bond'][bond_option] = split[1]
-    for iface in ifaces.keys():
-        if 'auto' not in ifaces[iface]:
-            ifaces[iface]['auto'] = False
-
-
-def parse_deb_config(path):
-    """Parses a debian network configuration file."""
-    ifaces = {}
-    with open(path, "r") as fp:
-        contents = fp.read().strip()
-    abs_path = os.path.abspath(path)
-    parse_deb_config_data(
-        ifaces, contents,
-        os.path.dirname(abs_path), abs_path)
-    return ifaces
-
-
 def parse_net_config_data(net_config):
     """Parses the config, returns NetworkState object
 
@@ -287,347 +133,6 @@ def parse_net_config(path):
     return ns
 
 
-def _load_shell_content(content, add_empty=False, empty_val=None):
-    """Given shell like syntax (key=value\nkey2=value2\n) in content
-       return the data in dictionary form.  If 'add_empty' is True
-       then add entries in to the returned dictionary for 'VAR='
-       variables.  Set their value to empty_val."""
-    data = {}
-    for line in shlex.split(content):
-        key, value = line.split("=", 1)
-        if not value:
-            value = empty_val
-        if add_empty or value:
-            data[key] = value
-
-    return data
-
-
-def _klibc_to_config_entry(content, mac_addrs=None):
-    """Convert a klibc writtent shell content file to a 'config' entry
-    When ip= is seen on the kernel command line in debian initramfs
-    and networking is brought up, ipconfig will populate
-    /run/net-<name>.cfg.
-
-    The files are shell style syntax, and examples are in the tests
-    provided here.  There is no good documentation on this unfortunately.
-
-    DEVICE=<name> is expected/required and PROTO should indicate if
-    this is 'static' or 'dhcp'.
-    """
-
-    if mac_addrs is None:
-        mac_addrs = {}
-
-    data = _load_shell_content(content)
-    try:
-        name = data['DEVICE']
-    except KeyError:
-        raise ValueError("no 'DEVICE' entry in data")
-
-    # ipconfig on precise does not write PROTO
-    proto = data.get('PROTO')
-    if not proto:
-        if data.get('filename'):
-            proto = 'dhcp'
-        else:
-            proto = 'static'
-
-    if proto not in ('static', 'dhcp'):
-        raise ValueError("Unexpected value for PROTO: %s" % proto)
-
-    iface = {
-        'type': 'physical',
-        'name': name,
-        'subnets': [],
-    }
-
-    if name in mac_addrs:
-        iface['mac_address'] = mac_addrs[name]
-
-    # originally believed there might be IPV6* values
-    for v, pre in (('ipv4', 'IPV4'),):
-        # if no IPV4ADDR or IPV6ADDR, then go on.
-        if pre + "ADDR" not in data:
-            continue
-        subnet = {'type': proto, 'control': 'manual'}
-
-        # these fields go right on the subnet
-        for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
-            if pre + key in data:
-                subnet[key.lower()] = data[pre + key]
-
-        dns = []
-        # handle IPV4DNS0 or IPV6DNS0
-        for nskey in ('DNS0', 'DNS1'):
-            ns = data.get(pre + nskey)
-            # verify it has something other than 0.0.0.0 (or ipv6)
-            if ns and len(ns.strip(":.0")):
-                dns.append(data[pre + nskey])
-        if dns:
-            subnet['dns_nameservers'] = dns
-            # add search to both ipv4 and ipv6, as it has no namespace
-            search = data.get('DOMAINSEARCH')
-            if search:
-                if ',' in search:
-                    subnet['dns_search'] = search.split(",")
-                else:
-                    subnet['dns_search'] = search.split()
-
-        iface['subnets'].append(subnet)
-
-    return name, iface
-
-
-def config_from_klibc_net_cfg(files=None, mac_addrs=None):
-    if files is None:
-        files = glob.glob('/run/net*.conf')
-
-    entries = []
-    names = {}
-    for cfg_file in files:
-        name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
-                                             mac_addrs=mac_addrs)
-        if name in names:
-            raise ValueError(
-                "device '%s' defined multiple times: %s and %s" % (
-                    name, names[name], cfg_file))
-
-        names[name] = cfg_file
-        entries.append(entry)
-    return {'config': entries, 'version': 1}
-
-
-def render_persistent_net(network_state):
-    ''' Given state, emit udev rules to map
-        mac to ifname
-    '''
-    content = ""
-    interfaces = network_state.get('interfaces')
-    for iface in interfaces.values():
-        # for physical interfaces write out a persist net udev rule
-        if iface['type'] == 'physical' and \
-           'name' in iface and iface.get('mac_address'):
-            content += generate_udev_rule(iface['name'],
-                                          iface['mac_address'])
-
-    return content
-
-
-# TODO: switch valid_map based on mode inet/inet6
-def iface_add_subnet(iface, subnet):
-    content = ""
-    valid_map = [
-        'address',
-        'netmask',
-        'broadcast',
-        'metric',
-        'gateway',
-        'pointopoint',
-        'mtu',
-        'scope',
-        'dns_search',
-        'dns_nameservers',
-    ]
-    for key, value in subnet.items():
-        if value and key in valid_map:
-            if type(value) == list:
-                value = " ".join(value)
-            if '_' in key:
-                key = key.replace('_', '-')
-            content += "    {} {}\n".format(key, value)
-
-    return content
-
-
-# TODO: switch to valid_map for attrs
-def iface_add_attrs(iface):
-    content = ""
-    ignore_map = [
-        'control',
-        'index',
-        'inet',
-        'mode',
-        'name',
-        'subnets',
-        'type',
-    ]
-    if iface['type'] not in ['bond', 'bridge', 'vlan']:
-        ignore_map.append('mac_address')
-
-    for key, value in iface.items():
-        if value and key not in ignore_map:
-            if type(value) == list:
-                value = " ".join(value)
-            content += "    {} {}\n".format(key, value)
-
-    return content
-
-
-def render_route(route, indent=""):
-    """ When rendering routes for an iface, in some cases applying a route
-    may result in the route command returning non-zero which produces
-    some confusing output for users manually using ifup/ifdown[1].  To
-    that end, we will optionally include an '|| true' postfix to each
-    route line allowing users to work with ifup/ifdown without using
-    --force option.
-
-    We may at somepoint not want to emit this additional postfix, and
-    add a 'strict' flag to this function.  When called with strict=True,
-    then we will not append the postfix.
-
-    1. http://askubuntu.com/questions/168033/
-             how-to-set-static-routes-in-ubuntu-server
-    """
-    content = ""
-    up = indent + "post-up route add"
-    down = indent + "pre-down route del"
-    eol = " || true\n"
-    mapping = {
-        'network': '-net',
-        'netmask': 'netmask',
-        'gateway': 'gw',
-        'metric': 'metric',
-    }
-    if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
-        default_gw = " default gw %s" % route['gateway']
-        content += up + default_gw + eol
-        content += down + default_gw + eol
-    elif route['network'] == '::' and route['netmask'] == 0:
-        # ipv6!
-        default_gw = " -A inet6 default gw %s" % route['gateway']
-        content += up + default_gw + eol
-        content += down + default_gw + eol
-    else:
-        route_line = ""
-        for k in ['network', 'netmask', 'gateway', 'metric']:
-            if k in route:
-                route_line += " %s %s" % (mapping[k], route[k])
-        content += up + route_line + eol
-        content += down + route_line + eol
-
-    return content
-
-
-def iface_start_entry(iface, index):
-    fullname = iface['name']
-    if index != 0:
-        fullname += ":%s" % index
-
-    control = iface['control']
-    if control == "auto":
-        cverb = "auto"
-    elif control in ("hotplug",):
-        cverb = "allow-" + control
-    else:
-        cverb = "# control-" + control
-
-    subst = iface.copy()
-    subst.update({'fullname': fullname, 'cverb': cverb})
-
-    return ("{cverb} {fullname}\n"
-            "iface {fullname} {inet} {mode}\n").format(**subst)
-
-
-def render_interfaces(network_state):
-    ''' Given state, emit etc/network/interfaces content '''
-
-    content = ""
-    interfaces = network_state.get('interfaces')
-    ''' Apply a sort order to ensure that we write out
-        the physical interfaces first; this is critical for
-        bonding
-    '''
-    order = {
-        'physical': 0,
-        'bond': 1,
-        'bridge': 2,
-        'vlan': 3,
-    }
-    content += "auto lo\niface lo inet loopback\n"
-    for dnskey, value in network_state.get('dns', {}).items():
-        if len(value):
-            content += "    dns-{} {}\n".format(dnskey, " ".join(value))
-
-    for iface in sorted(interfaces.values(),
-                        key=lambda k: (order[k['type']], k['name'])):
-
-        if content[-2:] != "\n\n":
-            content += "\n"
-        subnets = iface.get('subnets', {})
-        if subnets:
-            for index, subnet in zip(range(0, len(subnets)), subnets):
-                if content[-2:] != "\n\n":
-                    content += "\n"
-                iface['index'] = index
-                iface['mode'] = subnet['type']
-                iface['control'] = subnet.get('control', 'auto')
-                if iface['mode'].endswith('6'):
-                    iface['inet'] += '6'
-                elif iface['mode'] == 'static' and ":" in subnet['address']:
-                    iface['inet'] += '6'
-                if iface['mode'].startswith('dhcp'):
-                    iface['mode'] = 'dhcp'
-
-                content += iface_start_entry(iface, index)
-                content += iface_add_subnet(iface, subnet)
-                content += iface_add_attrs(iface)
-        else:
-            # ifenslave docs say to auto the slave devices
-            if 'bond-master' in iface:
-                content += "auto {name}\n".format(**iface)
-            content += "iface {name} {inet} {mode}\n".format(**iface)
-            content += iface_add_attrs(iface)
-
-    for route in network_state.get('routes'):
-        content += render_route(route)
-
-    # global replacements until v2 format
-    content = content.replace('mac_address', 'hwaddress')
-    return content
-
-
-def render_network_state(target, network_state, eni="etc/network/interfaces",
-                         links_prefix=LINKS_FNAME_PREFIX,
-                         netrules='etc/udev/rules.d/70-persistent-net.rules'):
-
-    fpeni = os.path.sep.join((target, eni,))
-    util.ensure_dir(os.path.dirname(fpeni))
-    with open(fpeni, 'w+') as f:
-        f.write(render_interfaces(network_state))
-
-    if netrules:
-        netrules = os.path.sep.join((target, netrules,))
-        util.ensure_dir(os.path.dirname(netrules))
-        with open(netrules, 'w+') as f:
-            f.write(render_persistent_net(network_state))
-
-    if links_prefix:
-        render_systemd_links(target, network_state, links_prefix)
-
-
-def render_systemd_links(target, network_state,
-                         links_prefix=LINKS_FNAME_PREFIX):
-    fp_prefix = os.path.sep.join((target, links_prefix))
-    for f in glob.glob(fp_prefix + "*"):
-        os.unlink(f)
-
-    interfaces = network_state.get('interfaces')
-    for iface in interfaces.values():
-        if (iface['type'] == 'physical' and 'name' in iface and
-                iface.get('mac_address')):
-            fname = fp_prefix + iface['name'] + ".link"
-            with open(fname, "w") as fp:
-                fp.write("\n".join([
-                    "[Match]",
-                    "MACAddress=" + iface['mac_address'],
-                    "",
-                    "[Link]",
-                    "Name=" + iface['name'],
-                    ""
-                ]))
-
-
 def is_disabled_cfg(cfg):
     if not cfg or not isinstance(cfg, dict):
         return False
@@ -718,56 +223,4 @@ def generate_fallback_config():
     return nconf
 
 
-def _decomp_gzip(blob, strict=True):
-    # decompress blob. raise exception if not compressed unless strict=False.
-    with io.BytesIO(blob) as iobuf:
-        gzfp = None
-        try:
-            gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
-            return gzfp.read()
-        except IOError:
-            if strict:
-                raise
-            return blob
-        finally:
-            if gzfp:
-                gzfp.close()
-
-
-def _b64dgz(b64str, gzipped="try"):
-    # decode a base64 string.  If gzipped is true, transparently uncompresss
-    # if gzipped is 'try', then try gunzip, returning the original on fail.
-    try:
-        blob = base64.b64decode(b64str)
-    except TypeError:
-        raise ValueError("Invalid base64 text: %s" % b64str)
-
-    if not gzipped:
-        return blob
-
-    return _decomp_gzip(blob, strict=gzipped != "try")
-
-
-def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
-    if cmdline is None:
-        cmdline = util.get_cmdline()
-
-    if 'network-config=' in cmdline:
-        data64 = None
-        for tok in cmdline.split():
-            if tok.startswith("network-config="):
-                data64 = tok.split("=", 1)[1]
-        if data64:
-            return util.load_yaml(_b64dgz(data64))
-
-    if 'ip=' not in cmdline:
-        return None
-
-    if mac_addrs is None:
-        mac_addrs = {k: sys_netdev_info(k, 'address')
-                     for k in get_devicelist()}
-
-    return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
-
-
 # vi: ts=4 expandtab syntax=python
diff --git a/cloudinit/net/distros/__init__.py b/cloudinit/net/distros/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cloudinit/net/distros/debian.py b/cloudinit/net/distros/debian.py
new file mode 100644
index 00000000..3ab0483e
--- /dev/null
+++ b/cloudinit/net/distros/debian.py
@@ -0,0 +1,401 @@
+# vi: ts=4 expandtab
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import glob
+import os
+import re
+
+from cloudinit.net import LINKS_FNAME_PREFIX
+from cloudinit.net import ParserError
+from cloudinit.net.udev import generate_udev_rule
+from cloudinit import util
+
+
+NET_CONFIG_COMMANDS = [
+    "pre-up", "up", "post-up", "down", "pre-down", "post-down",
+]
+
+NET_CONFIG_BRIDGE_OPTIONS = [
+    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
+    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
+]
+
+NET_CONFIG_OPTIONS = [
+    "address", "netmask", "broadcast", "network", "metric", "gateway",
+    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
+    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
+    "netnum", "endpoint", "local", "ttl",
+]
+
+
+# TODO: switch valid_map based on mode inet/inet6
+def _iface_add_subnet(iface, subnet):
+    content = ""
+    valid_map = [
+        'address',
+        'netmask',
+        'broadcast',
+        'metric',
+        'gateway',
+        'pointopoint',
+        'mtu',
+        'scope',
+        'dns_search',
+        'dns_nameservers',
+    ]
+    for key, value in subnet.items():
+        if value and key in valid_map:
+            if type(value) == list:
+                value = " ".join(value)
+            if '_' in key:
+                key = key.replace('_', '-')
+            content += "    {} {}\n".format(key, value)
+
+    return content
+
+
+# TODO: switch to valid_map for attrs
+def _iface_add_attrs(iface):
+    content = ""
+    ignore_map = [
+        'control',
+        'index',
+        'inet',
+        'mode',
+        'name',
+        'subnets',
+        'type',
+    ]
+    if iface['type'] not in ['bond', 'bridge', 'vlan']:
+        ignore_map.append('mac_address')
+
+    for key, value in iface.items():
+        if value and key not in ignore_map:
+            if type(value) == list:
+                value = " ".join(value)
+            content += "    {} {}\n".format(key, value)
+
+    return content
+
+
+def _iface_start_entry(iface, index):
+    fullname = iface['name']
+    if index != 0:
+        fullname += ":%s" % index
+
+    control = iface['control']
+    if control == "auto":
+        cverb = "auto"
+    elif control in ("hotplug",):
+        cverb = "allow-" + control
+    else:
+        cverb = "# control-" + control
+
+    subst = iface.copy()
+    subst.update({'fullname': fullname, 'cverb': cverb})
+
+    return ("{cverb} {fullname}\n"
+            "iface {fullname} {inet} {mode}\n").format(**subst)
+
+
+def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
+    """Parses the file contents, placing result into ifaces.
+
+    '_source_path' is added to every dictionary entry to define which file
+    the configration information came from.
+
+    :param ifaces: interface dictionary
+    :param contents: contents of interfaces file
+    :param src_dir: directory interfaces file was located
+    :param src_path: file path the `contents` was read
+    """
+    currif = None
+    for line in contents.splitlines():
+        line = line.strip()
+        if line.startswith('#'):
+            continue
+        split = line.split(' ')
+        option = split[0]
+        if option == "source-directory":
+            parsed_src_dir = split[1]
+            if not parsed_src_dir.startswith("/"):
+                parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
+            for expanded_path in glob.glob(parsed_src_dir):
+                dir_contents = os.listdir(expanded_path)
+                dir_contents = [
+                    os.path.join(expanded_path, path)
+                    for path in dir_contents
+                    if (os.path.isfile(os.path.join(expanded_path, path)) and
+                        re.match("^[a-zA-Z0-9_-]+$", path) is not None)
+                ]
+                for entry in dir_contents:
+                    with open(entry, "r") as fp:
+                        src_data = fp.read().strip()
+                    abs_entry = os.path.abspath(entry)
+                    _parse_deb_config_data(
+                        ifaces, src_data,
+                        os.path.dirname(abs_entry), abs_entry)
+        elif option == "source":
+            new_src_path = split[1]
+            if not new_src_path.startswith("/"):
+                new_src_path = os.path.join(src_dir, new_src_path)
+            for expanded_path in glob.glob(new_src_path):
+                with open(expanded_path, "r") as fp:
+                    src_data = fp.read().strip()
+                abs_path = os.path.abspath(expanded_path)
+                _parse_deb_config_data(
+                    ifaces, src_data,
+                    os.path.dirname(abs_path), abs_path)
+        elif option == "auto":
+            for iface in split[1:]:
+                if iface not in ifaces:
+                    ifaces[iface] = {
+                        # Include the source path this interface was found in.
+                        "_source_path": src_path
+                    }
+                ifaces[iface]['auto'] = True
+        elif option == "iface":
+            iface, family, method = split[1:4]
+            if iface not in ifaces:
+                ifaces[iface] = {
+                    # Include the source path this interface was found in.
+                    "_source_path": src_path
+                }
+            elif 'family' in ifaces[iface]:
+                raise ParserError(
+                    "Interface %s can only be defined once. "
+                    "Re-defined in '%s'." % (iface, src_path))
+            ifaces[iface]['family'] = family
+            ifaces[iface]['method'] = method
+            currif = iface
+        elif option == "hwaddress":
+            ifaces[currif]['hwaddress'] = split[1]
+        elif option in NET_CONFIG_OPTIONS:
+            ifaces[currif][option] = split[1]
+        elif option in NET_CONFIG_COMMANDS:
+            if option not in ifaces[currif]:
+                ifaces[currif][option] = []
+            ifaces[currif][option].append(' '.join(split[1:]))
+        elif option.startswith('dns-'):
+            if 'dns' not in ifaces[currif]:
+                ifaces[currif]['dns'] = {}
+            if option == 'dns-search':
+                ifaces[currif]['dns']['search'] = []
+                for domain in split[1:]:
+                    ifaces[currif]['dns']['search'].append(domain)
+            elif option == 'dns-nameservers':
+                ifaces[currif]['dns']['nameservers'] = []
+                for server in split[1:]:
+                    ifaces[currif]['dns']['nameservers'].append(server)
+        elif option.startswith('bridge_'):
+            if 'bridge' not in ifaces[currif]:
+                ifaces[currif]['bridge'] = {}
+            if option in NET_CONFIG_BRIDGE_OPTIONS:
+                bridge_option = option.replace('bridge_', '', 1)
+                ifaces[currif]['bridge'][bridge_option] = split[1]
+            elif option == "bridge_ports":
+                ifaces[currif]['bridge']['ports'] = []
+                for iface in split[1:]:
+                    ifaces[currif]['bridge']['ports'].append(iface)
+            elif option == "bridge_hw" and split[1].lower() == "mac":
+                ifaces[currif]['bridge']['mac'] = split[2]
+            elif option == "bridge_pathcost":
+                if 'pathcost' not in ifaces[currif]['bridge']:
+                    ifaces[currif]['bridge']['pathcost'] = {}
+                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
+            elif option == "bridge_portprio":
+                if 'portprio' not in ifaces[currif]['bridge']:
+                    ifaces[currif]['bridge']['portprio'] = {}
+                ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
+        elif option.startswith('bond-'):
+            if 'bond' not in ifaces[currif]:
+                ifaces[currif]['bond'] = {}
+            bond_option = option.replace('bond-', '', 1)
+            ifaces[currif]['bond'][bond_option] = split[1]
+    for iface in ifaces.keys():
+        if 'auto' not in ifaces[iface]:
+            ifaces[iface]['auto'] = False
+
+
+def _parse_deb_config(path):
+    """Parses a debian network configuration file."""
+    ifaces = {}
+    with open(path, "r") as fp:
+        contents = fp.read().strip()
+    abs_path = os.path.abspath(path)
+    _parse_deb_config_data(
+        ifaces, contents,
+        os.path.dirname(abs_path), abs_path)
+    return ifaces
+
+
+class Renderer(object):
+    """Renders network information in a debian format."""
+
+    def render_persistent_net(self, network_state):
+        ''' Given state, emit udev rules to map
+            mac to ifname
+        '''
+        content = ""
+        interfaces = network_state.get('interfaces')
+        for iface in interfaces.values():
+            # for physical interfaces write out a persist net udev rule
+            if iface['type'] == 'physical' and \
+               'name' in iface and iface.get('mac_address'):
+                content += generate_udev_rule(iface['name'],
+                                              iface['mac_address'])
+
+        return content
+
+    def render_routes(self, route, indent=""):
+        """ When rendering routes for an iface, in some cases applying a route
+        may result in the route command returning non-zero which produces
+        some confusing output for users manually using ifup/ifdown[1].  To
+        that end, we will optionally include an '|| true' postfix to each
+        route line allowing users to work with ifup/ifdown without using
+        --force option.
+
+        We may at somepoint not want to emit this additional postfix, and
+        add a 'strict' flag to this function.  When called with strict=True,
+        then we will not append the postfix.
+
+        1. http://askubuntu.com/questions/168033/
+                 how-to-set-static-routes-in-ubuntu-server
+        """
+        content = ""
+        up = indent + "post-up route add"
+        down = indent + "pre-down route del"
+        eol = " || true\n"
+        mapping = {
+            'network': '-net',
+            'netmask': 'netmask',
+            'gateway': 'gw',
+            'metric': 'metric',
+        }
+        if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
+            default_gw = " default gw %s" % route['gateway']
+            content += up + default_gw + eol
+            content += down + default_gw + eol
+        elif route['network'] == '::' and route['netmask'] == 0:
+            # ipv6!
+            default_gw = " -A inet6 default gw %s" % route['gateway']
+            content += up + default_gw + eol
+            content += down + default_gw + eol
+        else:
+            route_line = ""
+            for k in ['network', 'netmask', 'gateway', 'metric']:
+                if k in route:
+                    route_line += " %s %s" % (mapping[k], route[k])
+            content += up + route_line + eol
+            content += down + route_line + eol
+
+        return content
+
+    def render_interfaces(self, network_state):
+        ''' Given state, emit etc/network/interfaces content '''
+
+        content = ""
+        interfaces = network_state.get('interfaces')
+        ''' Apply a sort order to ensure that we write out
+            the physical interfaces first; this is critical for
+            bonding
+        '''
+        order = {
+            'physical': 0,
+            'bond': 1,
+            'bridge': 2,
+            'vlan': 3,
+        }
+        content += "auto lo\niface lo inet loopback\n"
+        for dnskey, value in network_state.get('dns', {}).items():
+            if len(value):
+                content += "    dns-{} {}\n".format(dnskey, " ".join(value))
+
+        for iface in sorted(interfaces.values(),
+                            key=lambda k: (order[k['type']], k['name'])):
+
+            if content[-2:] != "\n\n":
+                content += "\n"
+            subnets = iface.get('subnets', {})
+            if subnets:
+                for index, subnet in zip(range(0, len(subnets)), subnets):
+                    if content[-2:] != "\n\n":
+                        content += "\n"
+                    iface['index'] = index
+                    iface['mode'] = subnet['type']
+                    iface['control'] = subnet.get('control', 'auto')
+                    if iface['mode'].endswith('6'):
+                        iface['inet'] += '6'
+                    elif iface['mode'] == 'static' \
+                         and ":" in subnet['address']:
+                        iface['inet'] += '6'
+                    if iface['mode'].startswith('dhcp'):
+                        iface['mode'] = 'dhcp'
+
+                    content += _iface_start_entry(iface, index)
+                    content += _iface_add_subnet(iface, subnet)
+                    content += _iface_add_attrs(iface)
+            else:
+                # ifenslave docs say to auto the slave devices
+                if 'bond-master' in iface:
+                    content += "auto {name}\n".format(**iface)
+                content += "iface {name} {inet} {mode}\n".format(**iface)
+                content += _iface_add_attrs(iface)
+
+        for route in network_state.get('routes'):
+            content += self.render_route(route)
+
+        # global replacements until v2 format
+        content = content.replace('mac_address', 'hwaddress')
+        return content
+
+    def render_network_state(self,
+        target, network_state, eni="etc/network/interfaces",
+        links_prefix=LINKS_FNAME_PREFIX,
+        netrules='etc/udev/rules.d/70-persistent-net.rules'):
+
+        fpeni = os.path.sep.join((target, eni,))
+        util.ensure_dir(os.path.dirname(fpeni))
+        with open(fpeni, 'w+') as f:
+            f.write(self.render_interfaces(network_state))
+
+        if netrules:
+            netrules = os.path.sep.join((target, netrules,))
+            util.ensure_dir(os.path.dirname(netrules))
+            with open(netrules, 'w+') as f:
+                f.write(self.render_persistent_net(network_state))
+
+        if links_prefix:
+            self.render_systemd_links(target, network_state, links_prefix)
+
+    def render_systemd_links(self, target, network_state,
+                             links_prefix=LINKS_FNAME_PREFIX):
+        fp_prefix = os.path.sep.join((target, links_prefix))
+        for f in glob.glob(fp_prefix + "*"):
+            os.unlink(f)
+
+        interfaces = network_state.get('interfaces')
+        for iface in interfaces.values():
+            if (iface['type'] == 'physical' and 'name' in iface and
+                    iface.get('mac_address')):
+                fname = fp_prefix + iface['name'] + ".link"
+                with open(fname, "w") as fp:
+                    fp.write("\n".join([
+                        "[Match]",
+                        "MACAddress=" + iface['mac_address'],
+                        "",
+                        "[Link]",
+                        "Name=" + iface['name'],
+                        ""
+                    ]))
diff --git a/cloudinit/net/klibc.py b/cloudinit/net/klibc.py
new file mode 100644
index 00000000..958c264b
--- /dev/null
+++ b/cloudinit/net/klibc.py
@@ -0,0 +1,191 @@
+#   Copyright (C) 2013-2014 Canonical Ltd.
+#
+#   Author: Scott Moser <scott.moser@canonical.com>
+#   Author: Blake Rouse <blake.rouse@canonical.com>
+#
+#   Curtin is free software: you can redistribute it and/or modify it under
+#   the terms of the GNU Affero General Public License as published by the
+#   Free Software Foundation, either version 3 of the License, or (at your
+#   option) any later version.
+#
+#   Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
+#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+#   FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
+#   more details.
+#
+#   You should have received a copy of the GNU Affero General Public License
+#   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import glob
+import gzip
+import io
+import shlex
+
+from cloudinit.net import get_devicelist
+from cloudinit.net import sys_netdev_info
+
+from cloudinit import util
+
+
+def _load_shell_content(content, add_empty=False, empty_val=None):
+    """Given shell like syntax (key=value\nkey2=value2\n) in content
+       return the data in dictionary form.  If 'add_empty' is True
+       then add entries in to the returned dictionary for 'VAR='
+       variables.  Set their value to empty_val."""
+    data = {}
+    for line in shlex.split(content):
+        key, value = line.split("=", 1)
+        if not value:
+            value = empty_val
+        if add_empty or value:
+            data[key] = value
+
+    return data
+
+
+def _klibc_to_config_entry(content, mac_addrs=None):
+    """Convert a klibc writtent shell content file to a 'config' entry
+    When ip= is seen on the kernel command line in debian initramfs
+    and networking is brought up, ipconfig will populate
+    /run/net-<name>.cfg.
+
+    The files are shell style syntax, and examples are in the tests
+    provided here.  There is no good documentation on this unfortunately.
+
+    DEVICE=<name> is expected/required and PROTO should indicate if
+    this is 'static' or 'dhcp'.
+    """
+
+    if mac_addrs is None:
+        mac_addrs = {}
+
+    data = _load_shell_content(content)
+    try:
+        name = data['DEVICE']
+    except KeyError:
+        raise ValueError("no 'DEVICE' entry in data")
+
+    # ipconfig on precise does not write PROTO
+    proto = data.get('PROTO')
+    if not proto:
+        if data.get('filename'):
+            proto = 'dhcp'
+        else:
+            proto = 'static'
+
+    if proto not in ('static', 'dhcp'):
+        raise ValueError("Unexpected value for PROTO: %s" % proto)
+
+    iface = {
+        'type': 'physical',
+        'name': name,
+        'subnets': [],
+    }
+
+    if name in mac_addrs:
+        iface['mac_address'] = mac_addrs[name]
+
+    # originally believed there might be IPV6* values
+    for v, pre in (('ipv4', 'IPV4'),):
+        # if no IPV4ADDR or IPV6ADDR, then go on.
+        if pre + "ADDR" not in data:
+            continue
+        subnet = {'type': proto, 'control': 'manual'}
+
+        # these fields go right on the subnet
+        for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
+            if pre + key in data:
+                subnet[key.lower()] = data[pre + key]
+
+        dns = []
+        # handle IPV4DNS0 or IPV6DNS0
+        for nskey in ('DNS0', 'DNS1'):
+            ns = data.get(pre + nskey)
+            # verify it has something other than 0.0.0.0 (or ipv6)
+            if ns and len(ns.strip(":.0")):
+                dns.append(data[pre + nskey])
+        if dns:
+            subnet['dns_nameservers'] = dns
+            # add search to both ipv4 and ipv6, as it has no namespace
+            search = data.get('DOMAINSEARCH')
+            if search:
+                if ',' in search:
+                    subnet['dns_search'] = search.split(",")
+                else:
+                    subnet['dns_search'] = search.split()
+
+        iface['subnets'].append(subnet)
+
+    return name, iface
+
+
+def config_from_klibc_net_cfg(files=None, mac_addrs=None):
+    if files is None:
+        files = glob.glob('/run/net*.conf')
+
+    entries = []
+    names = {}
+    for cfg_file in files:
+        name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
+                                             mac_addrs=mac_addrs)
+        if name in names:
+            raise ValueError(
+                "device '%s' defined multiple times: %s and %s" % (
+                    name, names[name], cfg_file))
+
+        names[name] = cfg_file
+        entries.append(entry)
+    return {'config': entries, 'version': 1}
+
+
+def _decomp_gzip(blob, strict=True):
+    # decompress blob. raise exception if not compressed unless strict=False.
+    with io.BytesIO(blob) as iobuf:
+        gzfp = None
+        try:
+            gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
+            return gzfp.read()
+        except IOError:
+            if strict:
+                raise
+            return blob
+        finally:
+            if gzfp:
+                gzfp.close()
+
+
+def _b64dgz(b64str, gzipped="try"):
+    # decode a base64 string.  If gzipped is true, transparently uncompresss
+    # if gzipped is 'try', then try gunzip, returning the original on fail.
+    try:
+        blob = base64.b64decode(b64str)
+    except TypeError:
+        raise ValueError("Invalid base64 text: %s" % b64str)
+
+    if not gzipped:
+        return blob
+
+    return _decomp_gzip(blob, strict=gzipped != "try")
+
+
+def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
+    if cmdline is None:
+        cmdline = util.get_cmdline()
+
+    if 'network-config=' in cmdline:
+        data64 = None
+        for tok in cmdline.split():
+            if tok.startswith("network-config="):
+                data64 = tok.split("=", 1)[1]
+        if data64:
+            return util.load_yaml(_b64dgz(data64))
+
+    if 'ip=' not in cmdline:
+        return None
+
+    if mac_addrs is None:
+        mac_addrs = {k: sys_netdev_info(k, 'address')
+                     for k in get_devicelist()}
+
+    return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
-- 
cgit v1.2.3


From a800da2371b85b2ece1a30de00f988035819c958 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Mon, 9 May 2016 16:36:04 -0700
Subject: Use the new renderer in the debian and stages files

---
 cloudinit/distros/debian.py     | 10 ++++++----
 cloudinit/net/distros/debian.py |  2 +-
 cloudinit/stages.py             |  4 ++--
 3 files changed, 9 insertions(+), 7 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index 75ab340f..e8fc1df4 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -27,6 +27,7 @@ from cloudinit import helpers
 from cloudinit import log as logging
 from cloudinit import util
 from cloudinit import net
+from cloudinit.net.distros import debian
 
 from cloudinit.distros.parsers.hostname import HostnameConf
 
@@ -56,6 +57,7 @@ class Distro(distros.Distro):
         # should only happen say once per instance...)
         self._runner = helpers.Runners(paths)
         self.osfamily = 'debian'
+        self.renderer = debian.Renderer()
 
     def apply_locale(self, locale, out_fn=None):
         if not out_fn:
@@ -80,10 +82,10 @@ class Distro(distros.Distro):
 
     def _write_network_config(self, netconfig):
         ns = net.parse_net_config_data(netconfig)
-        net.render_network_state(target="/", network_state=ns,
-                                 eni=self.network_conf_fn,
-                                 links_prefix=self.links_prefix,
-                                 netrules=None)
+        self.renderer.render_network_state(
+            target="/", network_state=ns,
+            eni=self.network_conf_fn, links_prefix=self.links_prefix,
+            netrules=None)
         _maybe_remove_legacy_eth0()
 
         return []
diff --git a/cloudinit/net/distros/debian.py b/cloudinit/net/distros/debian.py
index 3ab0483e..4bf34fd7 100644
--- a/cloudinit/net/distros/debian.py
+++ b/cloudinit/net/distros/debian.py
@@ -258,7 +258,7 @@ class Renderer(object):
 
         return content
 
-    def render_routes(self, route, indent=""):
+    def render_route(self, route, indent=""):
         """ When rendering routes for an iface, in some cases applying a route
         may result in the route command returning non-zero which produces
         some confusing output for users manually using ifup/ifdown[1].  To
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index ffb15165..e6bd34fe 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -43,7 +43,7 @@ from cloudinit import distros
 from cloudinit import helpers
 from cloudinit import importer
 from cloudinit import log as logging
-from cloudinit import net
+from cloudinit.net import klibc
 from cloudinit import sources
 from cloudinit import type_utils
 from cloudinit import util
@@ -579,7 +579,7 @@ class Init(object):
         if os.path.exists(disable_file):
             return (None, disable_file)
 
-        cmdline_cfg = ('cmdline', net.read_kernel_cmdline_config())
+        cmdline_cfg = ('cmdline', klibc.read_kernel_cmdline_config())
         dscfg = ('ds', None)
         if self.datasource and hasattr(self.datasource, 'network_config'):
             dscfg = ('ds', self.datasource.network_config)
-- 
cgit v1.2.3


From e459e3525cf8321245a2f29b869e9978eb15f2c9 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Mon, 9 May 2016 16:50:17 -0700
Subject: Rename renderer attribute to _net_renderer

---
 cloudinit/distros/debian.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index e8fc1df4..21a4f805 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -57,7 +57,7 @@ class Distro(distros.Distro):
         # should only happen say once per instance...)
         self._runner = helpers.Runners(paths)
         self.osfamily = 'debian'
-        self.renderer = debian.Renderer()
+        self._net_renderer = debian.Renderer()
 
     def apply_locale(self, locale, out_fn=None):
         if not out_fn:
@@ -82,7 +82,7 @@ class Distro(distros.Distro):
 
     def _write_network_config(self, netconfig):
         ns = net.parse_net_config_data(netconfig)
-        self.renderer.render_network_state(
+        self._net_renderer.render_network_state(
             target="/", network_state=ns,
             eni=self.network_conf_fn, links_prefix=self.links_prefix,
             netrules=None)
-- 
cgit v1.2.3


From 483545957dca362452b94fc49064e298fc07dd71 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Tue, 10 May 2016 13:02:29 -0700
Subject: Rename to net/renderers and klibc -> cmdline.py

---
 cloudinit/net/cmdline.py            | 191 +++++++++++++++++
 cloudinit/net/distros/__init__.py   |   0
 cloudinit/net/distros/debian.py     | 401 ------------------------------------
 cloudinit/net/klibc.py              | 191 -----------------
 cloudinit/net/renderers/__init__.py |   0
 cloudinit/net/renderers/eni.py      | 399 +++++++++++++++++++++++++++++++++++
 cloudinit/stages.py                 |   4 +-
 7 files changed, 592 insertions(+), 594 deletions(-)
 create mode 100644 cloudinit/net/cmdline.py
 delete mode 100644 cloudinit/net/distros/__init__.py
 delete mode 100644 cloudinit/net/distros/debian.py
 delete mode 100644 cloudinit/net/klibc.py
 create mode 100644 cloudinit/net/renderers/__init__.py
 create mode 100644 cloudinit/net/renderers/eni.py

(limited to 'cloudinit')

diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
new file mode 100644
index 00000000..958c264b
--- /dev/null
+++ b/cloudinit/net/cmdline.py
@@ -0,0 +1,191 @@
+#   Copyright (C) 2013-2014 Canonical Ltd.
+#
+#   Author: Scott Moser <scott.moser@canonical.com>
+#   Author: Blake Rouse <blake.rouse@canonical.com>
+#
+#   Curtin is free software: you can redistribute it and/or modify it under
+#   the terms of the GNU Affero General Public License as published by the
+#   Free Software Foundation, either version 3 of the License, or (at your
+#   option) any later version.
+#
+#   Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
+#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+#   FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
+#   more details.
+#
+#   You should have received a copy of the GNU Affero General Public License
+#   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import glob
+import gzip
+import io
+import shlex
+
+from cloudinit.net import get_devicelist
+from cloudinit.net import sys_netdev_info
+
+from cloudinit import util
+
+
+def _load_shell_content(content, add_empty=False, empty_val=None):
+    """Given shell like syntax (key=value\nkey2=value2\n) in content
+       return the data in dictionary form.  If 'add_empty' is True
+       then add entries in to the returned dictionary for 'VAR='
+       variables.  Set their value to empty_val."""
+    data = {}
+    for line in shlex.split(content):
+        key, value = line.split("=", 1)
+        if not value:
+            value = empty_val
+        if add_empty or value:
+            data[key] = value
+
+    return data
+
+
+def _klibc_to_config_entry(content, mac_addrs=None):
+    """Convert a klibc writtent shell content file to a 'config' entry
+    When ip= is seen on the kernel command line in debian initramfs
+    and networking is brought up, ipconfig will populate
+    /run/net-<name>.cfg.
+
+    The files are shell style syntax, and examples are in the tests
+    provided here.  There is no good documentation on this unfortunately.
+
+    DEVICE=<name> is expected/required and PROTO should indicate if
+    this is 'static' or 'dhcp'.
+    """
+
+    if mac_addrs is None:
+        mac_addrs = {}
+
+    data = _load_shell_content(content)
+    try:
+        name = data['DEVICE']
+    except KeyError:
+        raise ValueError("no 'DEVICE' entry in data")
+
+    # ipconfig on precise does not write PROTO
+    proto = data.get('PROTO')
+    if not proto:
+        if data.get('filename'):
+            proto = 'dhcp'
+        else:
+            proto = 'static'
+
+    if proto not in ('static', 'dhcp'):
+        raise ValueError("Unexpected value for PROTO: %s" % proto)
+
+    iface = {
+        'type': 'physical',
+        'name': name,
+        'subnets': [],
+    }
+
+    if name in mac_addrs:
+        iface['mac_address'] = mac_addrs[name]
+
+    # originally believed there might be IPV6* values
+    for v, pre in (('ipv4', 'IPV4'),):
+        # if no IPV4ADDR or IPV6ADDR, then go on.
+        if pre + "ADDR" not in data:
+            continue
+        subnet = {'type': proto, 'control': 'manual'}
+
+        # these fields go right on the subnet
+        for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
+            if pre + key in data:
+                subnet[key.lower()] = data[pre + key]
+
+        dns = []
+        # handle IPV4DNS0 or IPV6DNS0
+        for nskey in ('DNS0', 'DNS1'):
+            ns = data.get(pre + nskey)
+            # verify it has something other than 0.0.0.0 (or ipv6)
+            if ns and len(ns.strip(":.0")):
+                dns.append(data[pre + nskey])
+        if dns:
+            subnet['dns_nameservers'] = dns
+            # add search to both ipv4 and ipv6, as it has no namespace
+            search = data.get('DOMAINSEARCH')
+            if search:
+                if ',' in search:
+                    subnet['dns_search'] = search.split(",")
+                else:
+                    subnet['dns_search'] = search.split()
+
+        iface['subnets'].append(subnet)
+
+    return name, iface
+
+
+def config_from_klibc_net_cfg(files=None, mac_addrs=None):
+    if files is None:
+        files = glob.glob('/run/net*.conf')
+
+    entries = []
+    names = {}
+    for cfg_file in files:
+        name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
+                                             mac_addrs=mac_addrs)
+        if name in names:
+            raise ValueError(
+                "device '%s' defined multiple times: %s and %s" % (
+                    name, names[name], cfg_file))
+
+        names[name] = cfg_file
+        entries.append(entry)
+    return {'config': entries, 'version': 1}
+
+
+def _decomp_gzip(blob, strict=True):
+    # decompress blob. raise exception if not compressed unless strict=False.
+    with io.BytesIO(blob) as iobuf:
+        gzfp = None
+        try:
+            gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
+            return gzfp.read()
+        except IOError:
+            if strict:
+                raise
+            return blob
+        finally:
+            if gzfp:
+                gzfp.close()
+
+
+def _b64dgz(b64str, gzipped="try"):
+    # decode a base64 string.  If gzipped is true, transparently uncompresss
+    # if gzipped is 'try', then try gunzip, returning the original on fail.
+    try:
+        blob = base64.b64decode(b64str)
+    except TypeError:
+        raise ValueError("Invalid base64 text: %s" % b64str)
+
+    if not gzipped:
+        return blob
+
+    return _decomp_gzip(blob, strict=gzipped != "try")
+
+
+def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
+    if cmdline is None:
+        cmdline = util.get_cmdline()
+
+    if 'network-config=' in cmdline:
+        data64 = None
+        for tok in cmdline.split():
+            if tok.startswith("network-config="):
+                data64 = tok.split("=", 1)[1]
+        if data64:
+            return util.load_yaml(_b64dgz(data64))
+
+    if 'ip=' not in cmdline:
+        return None
+
+    if mac_addrs is None:
+        mac_addrs = {k: sys_netdev_info(k, 'address')
+                     for k in get_devicelist()}
+
+    return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
diff --git a/cloudinit/net/distros/__init__.py b/cloudinit/net/distros/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/cloudinit/net/distros/debian.py b/cloudinit/net/distros/debian.py
deleted file mode 100644
index 4bf34fd7..00000000
--- a/cloudinit/net/distros/debian.py
+++ /dev/null
@@ -1,401 +0,0 @@
-# vi: ts=4 expandtab
-#
-#    This program is free software: you can redistribute it and/or modify
-#    it under the terms of the GNU General Public License version 3, as
-#    published by the Free Software Foundation.
-#
-#    This program is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU General Public License for more details.
-#
-#    You should have received a copy of the GNU General Public License
-#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import glob
-import os
-import re
-
-from cloudinit.net import LINKS_FNAME_PREFIX
-from cloudinit.net import ParserError
-from cloudinit.net.udev import generate_udev_rule
-from cloudinit import util
-
-
-NET_CONFIG_COMMANDS = [
-    "pre-up", "up", "post-up", "down", "pre-down", "post-down",
-]
-
-NET_CONFIG_BRIDGE_OPTIONS = [
-    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
-    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
-]
-
-NET_CONFIG_OPTIONS = [
-    "address", "netmask", "broadcast", "network", "metric", "gateway",
-    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
-    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
-    "netnum", "endpoint", "local", "ttl",
-]
-
-
-# TODO: switch valid_map based on mode inet/inet6
-def _iface_add_subnet(iface, subnet):
-    content = ""
-    valid_map = [
-        'address',
-        'netmask',
-        'broadcast',
-        'metric',
-        'gateway',
-        'pointopoint',
-        'mtu',
-        'scope',
-        'dns_search',
-        'dns_nameservers',
-    ]
-    for key, value in subnet.items():
-        if value and key in valid_map:
-            if type(value) == list:
-                value = " ".join(value)
-            if '_' in key:
-                key = key.replace('_', '-')
-            content += "    {} {}\n".format(key, value)
-
-    return content
-
-
-# TODO: switch to valid_map for attrs
-def _iface_add_attrs(iface):
-    content = ""
-    ignore_map = [
-        'control',
-        'index',
-        'inet',
-        'mode',
-        'name',
-        'subnets',
-        'type',
-    ]
-    if iface['type'] not in ['bond', 'bridge', 'vlan']:
-        ignore_map.append('mac_address')
-
-    for key, value in iface.items():
-        if value and key not in ignore_map:
-            if type(value) == list:
-                value = " ".join(value)
-            content += "    {} {}\n".format(key, value)
-
-    return content
-
-
-def _iface_start_entry(iface, index):
-    fullname = iface['name']
-    if index != 0:
-        fullname += ":%s" % index
-
-    control = iface['control']
-    if control == "auto":
-        cverb = "auto"
-    elif control in ("hotplug",):
-        cverb = "allow-" + control
-    else:
-        cverb = "# control-" + control
-
-    subst = iface.copy()
-    subst.update({'fullname': fullname, 'cverb': cverb})
-
-    return ("{cverb} {fullname}\n"
-            "iface {fullname} {inet} {mode}\n").format(**subst)
-
-
-def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
-    """Parses the file contents, placing result into ifaces.
-
-    '_source_path' is added to every dictionary entry to define which file
-    the configration information came from.
-
-    :param ifaces: interface dictionary
-    :param contents: contents of interfaces file
-    :param src_dir: directory interfaces file was located
-    :param src_path: file path the `contents` was read
-    """
-    currif = None
-    for line in contents.splitlines():
-        line = line.strip()
-        if line.startswith('#'):
-            continue
-        split = line.split(' ')
-        option = split[0]
-        if option == "source-directory":
-            parsed_src_dir = split[1]
-            if not parsed_src_dir.startswith("/"):
-                parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
-            for expanded_path in glob.glob(parsed_src_dir):
-                dir_contents = os.listdir(expanded_path)
-                dir_contents = [
-                    os.path.join(expanded_path, path)
-                    for path in dir_contents
-                    if (os.path.isfile(os.path.join(expanded_path, path)) and
-                        re.match("^[a-zA-Z0-9_-]+$", path) is not None)
-                ]
-                for entry in dir_contents:
-                    with open(entry, "r") as fp:
-                        src_data = fp.read().strip()
-                    abs_entry = os.path.abspath(entry)
-                    _parse_deb_config_data(
-                        ifaces, src_data,
-                        os.path.dirname(abs_entry), abs_entry)
-        elif option == "source":
-            new_src_path = split[1]
-            if not new_src_path.startswith("/"):
-                new_src_path = os.path.join(src_dir, new_src_path)
-            for expanded_path in glob.glob(new_src_path):
-                with open(expanded_path, "r") as fp:
-                    src_data = fp.read().strip()
-                abs_path = os.path.abspath(expanded_path)
-                _parse_deb_config_data(
-                    ifaces, src_data,
-                    os.path.dirname(abs_path), abs_path)
-        elif option == "auto":
-            for iface in split[1:]:
-                if iface not in ifaces:
-                    ifaces[iface] = {
-                        # Include the source path this interface was found in.
-                        "_source_path": src_path
-                    }
-                ifaces[iface]['auto'] = True
-        elif option == "iface":
-            iface, family, method = split[1:4]
-            if iface not in ifaces:
-                ifaces[iface] = {
-                    # Include the source path this interface was found in.
-                    "_source_path": src_path
-                }
-            elif 'family' in ifaces[iface]:
-                raise ParserError(
-                    "Interface %s can only be defined once. "
-                    "Re-defined in '%s'." % (iface, src_path))
-            ifaces[iface]['family'] = family
-            ifaces[iface]['method'] = method
-            currif = iface
-        elif option == "hwaddress":
-            ifaces[currif]['hwaddress'] = split[1]
-        elif option in NET_CONFIG_OPTIONS:
-            ifaces[currif][option] = split[1]
-        elif option in NET_CONFIG_COMMANDS:
-            if option not in ifaces[currif]:
-                ifaces[currif][option] = []
-            ifaces[currif][option].append(' '.join(split[1:]))
-        elif option.startswith('dns-'):
-            if 'dns' not in ifaces[currif]:
-                ifaces[currif]['dns'] = {}
-            if option == 'dns-search':
-                ifaces[currif]['dns']['search'] = []
-                for domain in split[1:]:
-                    ifaces[currif]['dns']['search'].append(domain)
-            elif option == 'dns-nameservers':
-                ifaces[currif]['dns']['nameservers'] = []
-                for server in split[1:]:
-                    ifaces[currif]['dns']['nameservers'].append(server)
-        elif option.startswith('bridge_'):
-            if 'bridge' not in ifaces[currif]:
-                ifaces[currif]['bridge'] = {}
-            if option in NET_CONFIG_BRIDGE_OPTIONS:
-                bridge_option = option.replace('bridge_', '', 1)
-                ifaces[currif]['bridge'][bridge_option] = split[1]
-            elif option == "bridge_ports":
-                ifaces[currif]['bridge']['ports'] = []
-                for iface in split[1:]:
-                    ifaces[currif]['bridge']['ports'].append(iface)
-            elif option == "bridge_hw" and split[1].lower() == "mac":
-                ifaces[currif]['bridge']['mac'] = split[2]
-            elif option == "bridge_pathcost":
-                if 'pathcost' not in ifaces[currif]['bridge']:
-                    ifaces[currif]['bridge']['pathcost'] = {}
-                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
-            elif option == "bridge_portprio":
-                if 'portprio' not in ifaces[currif]['bridge']:
-                    ifaces[currif]['bridge']['portprio'] = {}
-                ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
-        elif option.startswith('bond-'):
-            if 'bond' not in ifaces[currif]:
-                ifaces[currif]['bond'] = {}
-            bond_option = option.replace('bond-', '', 1)
-            ifaces[currif]['bond'][bond_option] = split[1]
-    for iface in ifaces.keys():
-        if 'auto' not in ifaces[iface]:
-            ifaces[iface]['auto'] = False
-
-
-def _parse_deb_config(path):
-    """Parses a debian network configuration file."""
-    ifaces = {}
-    with open(path, "r") as fp:
-        contents = fp.read().strip()
-    abs_path = os.path.abspath(path)
-    _parse_deb_config_data(
-        ifaces, contents,
-        os.path.dirname(abs_path), abs_path)
-    return ifaces
-
-
-class Renderer(object):
-    """Renders network information in a debian format."""
-
-    def render_persistent_net(self, network_state):
-        ''' Given state, emit udev rules to map
-            mac to ifname
-        '''
-        content = ""
-        interfaces = network_state.get('interfaces')
-        for iface in interfaces.values():
-            # for physical interfaces write out a persist net udev rule
-            if iface['type'] == 'physical' and \
-               'name' in iface and iface.get('mac_address'):
-                content += generate_udev_rule(iface['name'],
-                                              iface['mac_address'])
-
-        return content
-
-    def render_route(self, route, indent=""):
-        """ When rendering routes for an iface, in some cases applying a route
-        may result in the route command returning non-zero which produces
-        some confusing output for users manually using ifup/ifdown[1].  To
-        that end, we will optionally include an '|| true' postfix to each
-        route line allowing users to work with ifup/ifdown without using
-        --force option.
-
-        We may at somepoint not want to emit this additional postfix, and
-        add a 'strict' flag to this function.  When called with strict=True,
-        then we will not append the postfix.
-
-        1. http://askubuntu.com/questions/168033/
-                 how-to-set-static-routes-in-ubuntu-server
-        """
-        content = ""
-        up = indent + "post-up route add"
-        down = indent + "pre-down route del"
-        eol = " || true\n"
-        mapping = {
-            'network': '-net',
-            'netmask': 'netmask',
-            'gateway': 'gw',
-            'metric': 'metric',
-        }
-        if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
-            default_gw = " default gw %s" % route['gateway']
-            content += up + default_gw + eol
-            content += down + default_gw + eol
-        elif route['network'] == '::' and route['netmask'] == 0:
-            # ipv6!
-            default_gw = " -A inet6 default gw %s" % route['gateway']
-            content += up + default_gw + eol
-            content += down + default_gw + eol
-        else:
-            route_line = ""
-            for k in ['network', 'netmask', 'gateway', 'metric']:
-                if k in route:
-                    route_line += " %s %s" % (mapping[k], route[k])
-            content += up + route_line + eol
-            content += down + route_line + eol
-
-        return content
-
-    def render_interfaces(self, network_state):
-        ''' Given state, emit etc/network/interfaces content '''
-
-        content = ""
-        interfaces = network_state.get('interfaces')
-        ''' Apply a sort order to ensure that we write out
-            the physical interfaces first; this is critical for
-            bonding
-        '''
-        order = {
-            'physical': 0,
-            'bond': 1,
-            'bridge': 2,
-            'vlan': 3,
-        }
-        content += "auto lo\niface lo inet loopback\n"
-        for dnskey, value in network_state.get('dns', {}).items():
-            if len(value):
-                content += "    dns-{} {}\n".format(dnskey, " ".join(value))
-
-        for iface in sorted(interfaces.values(),
-                            key=lambda k: (order[k['type']], k['name'])):
-
-            if content[-2:] != "\n\n":
-                content += "\n"
-            subnets = iface.get('subnets', {})
-            if subnets:
-                for index, subnet in zip(range(0, len(subnets)), subnets):
-                    if content[-2:] != "\n\n":
-                        content += "\n"
-                    iface['index'] = index
-                    iface['mode'] = subnet['type']
-                    iface['control'] = subnet.get('control', 'auto')
-                    if iface['mode'].endswith('6'):
-                        iface['inet'] += '6'
-                    elif iface['mode'] == 'static' \
-                         and ":" in subnet['address']:
-                        iface['inet'] += '6'
-                    if iface['mode'].startswith('dhcp'):
-                        iface['mode'] = 'dhcp'
-
-                    content += _iface_start_entry(iface, index)
-                    content += _iface_add_subnet(iface, subnet)
-                    content += _iface_add_attrs(iface)
-            else:
-                # ifenslave docs say to auto the slave devices
-                if 'bond-master' in iface:
-                    content += "auto {name}\n".format(**iface)
-                content += "iface {name} {inet} {mode}\n".format(**iface)
-                content += _iface_add_attrs(iface)
-
-        for route in network_state.get('routes'):
-            content += self.render_route(route)
-
-        # global replacements until v2 format
-        content = content.replace('mac_address', 'hwaddress')
-        return content
-
-    def render_network_state(self,
-        target, network_state, eni="etc/network/interfaces",
-        links_prefix=LINKS_FNAME_PREFIX,
-        netrules='etc/udev/rules.d/70-persistent-net.rules'):
-
-        fpeni = os.path.sep.join((target, eni,))
-        util.ensure_dir(os.path.dirname(fpeni))
-        with open(fpeni, 'w+') as f:
-            f.write(self.render_interfaces(network_state))
-
-        if netrules:
-            netrules = os.path.sep.join((target, netrules,))
-            util.ensure_dir(os.path.dirname(netrules))
-            with open(netrules, 'w+') as f:
-                f.write(self.render_persistent_net(network_state))
-
-        if links_prefix:
-            self.render_systemd_links(target, network_state, links_prefix)
-
-    def render_systemd_links(self, target, network_state,
-                             links_prefix=LINKS_FNAME_PREFIX):
-        fp_prefix = os.path.sep.join((target, links_prefix))
-        for f in glob.glob(fp_prefix + "*"):
-            os.unlink(f)
-
-        interfaces = network_state.get('interfaces')
-        for iface in interfaces.values():
-            if (iface['type'] == 'physical' and 'name' in iface and
-                    iface.get('mac_address')):
-                fname = fp_prefix + iface['name'] + ".link"
-                with open(fname, "w") as fp:
-                    fp.write("\n".join([
-                        "[Match]",
-                        "MACAddress=" + iface['mac_address'],
-                        "",
-                        "[Link]",
-                        "Name=" + iface['name'],
-                        ""
-                    ]))
diff --git a/cloudinit/net/klibc.py b/cloudinit/net/klibc.py
deleted file mode 100644
index 958c264b..00000000
--- a/cloudinit/net/klibc.py
+++ /dev/null
@@ -1,191 +0,0 @@
-#   Copyright (C) 2013-2014 Canonical Ltd.
-#
-#   Author: Scott Moser <scott.moser@canonical.com>
-#   Author: Blake Rouse <blake.rouse@canonical.com>
-#
-#   Curtin is free software: you can redistribute it and/or modify it under
-#   the terms of the GNU Affero General Public License as published by the
-#   Free Software Foundation, either version 3 of the License, or (at your
-#   option) any later version.
-#
-#   Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
-#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-#   FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
-#   more details.
-#
-#   You should have received a copy of the GNU Affero General Public License
-#   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
-
-import base64
-import glob
-import gzip
-import io
-import shlex
-
-from cloudinit.net import get_devicelist
-from cloudinit.net import sys_netdev_info
-
-from cloudinit import util
-
-
-def _load_shell_content(content, add_empty=False, empty_val=None):
-    """Given shell like syntax (key=value\nkey2=value2\n) in content
-       return the data in dictionary form.  If 'add_empty' is True
-       then add entries in to the returned dictionary for 'VAR='
-       variables.  Set their value to empty_val."""
-    data = {}
-    for line in shlex.split(content):
-        key, value = line.split("=", 1)
-        if not value:
-            value = empty_val
-        if add_empty or value:
-            data[key] = value
-
-    return data
-
-
-def _klibc_to_config_entry(content, mac_addrs=None):
-    """Convert a klibc writtent shell content file to a 'config' entry
-    When ip= is seen on the kernel command line in debian initramfs
-    and networking is brought up, ipconfig will populate
-    /run/net-<name>.cfg.
-
-    The files are shell style syntax, and examples are in the tests
-    provided here.  There is no good documentation on this unfortunately.
-
-    DEVICE=<name> is expected/required and PROTO should indicate if
-    this is 'static' or 'dhcp'.
-    """
-
-    if mac_addrs is None:
-        mac_addrs = {}
-
-    data = _load_shell_content(content)
-    try:
-        name = data['DEVICE']
-    except KeyError:
-        raise ValueError("no 'DEVICE' entry in data")
-
-    # ipconfig on precise does not write PROTO
-    proto = data.get('PROTO')
-    if not proto:
-        if data.get('filename'):
-            proto = 'dhcp'
-        else:
-            proto = 'static'
-
-    if proto not in ('static', 'dhcp'):
-        raise ValueError("Unexpected value for PROTO: %s" % proto)
-
-    iface = {
-        'type': 'physical',
-        'name': name,
-        'subnets': [],
-    }
-
-    if name in mac_addrs:
-        iface['mac_address'] = mac_addrs[name]
-
-    # originally believed there might be IPV6* values
-    for v, pre in (('ipv4', 'IPV4'),):
-        # if no IPV4ADDR or IPV6ADDR, then go on.
-        if pre + "ADDR" not in data:
-            continue
-        subnet = {'type': proto, 'control': 'manual'}
-
-        # these fields go right on the subnet
-        for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
-            if pre + key in data:
-                subnet[key.lower()] = data[pre + key]
-
-        dns = []
-        # handle IPV4DNS0 or IPV6DNS0
-        for nskey in ('DNS0', 'DNS1'):
-            ns = data.get(pre + nskey)
-            # verify it has something other than 0.0.0.0 (or ipv6)
-            if ns and len(ns.strip(":.0")):
-                dns.append(data[pre + nskey])
-        if dns:
-            subnet['dns_nameservers'] = dns
-            # add search to both ipv4 and ipv6, as it has no namespace
-            search = data.get('DOMAINSEARCH')
-            if search:
-                if ',' in search:
-                    subnet['dns_search'] = search.split(",")
-                else:
-                    subnet['dns_search'] = search.split()
-
-        iface['subnets'].append(subnet)
-
-    return name, iface
-
-
-def config_from_klibc_net_cfg(files=None, mac_addrs=None):
-    if files is None:
-        files = glob.glob('/run/net*.conf')
-
-    entries = []
-    names = {}
-    for cfg_file in files:
-        name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
-                                             mac_addrs=mac_addrs)
-        if name in names:
-            raise ValueError(
-                "device '%s' defined multiple times: %s and %s" % (
-                    name, names[name], cfg_file))
-
-        names[name] = cfg_file
-        entries.append(entry)
-    return {'config': entries, 'version': 1}
-
-
-def _decomp_gzip(blob, strict=True):
-    # decompress blob. raise exception if not compressed unless strict=False.
-    with io.BytesIO(blob) as iobuf:
-        gzfp = None
-        try:
-            gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
-            return gzfp.read()
-        except IOError:
-            if strict:
-                raise
-            return blob
-        finally:
-            if gzfp:
-                gzfp.close()
-
-
-def _b64dgz(b64str, gzipped="try"):
-    # decode a base64 string.  If gzipped is true, transparently uncompresss
-    # if gzipped is 'try', then try gunzip, returning the original on fail.
-    try:
-        blob = base64.b64decode(b64str)
-    except TypeError:
-        raise ValueError("Invalid base64 text: %s" % b64str)
-
-    if not gzipped:
-        return blob
-
-    return _decomp_gzip(blob, strict=gzipped != "try")
-
-
-def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
-    if cmdline is None:
-        cmdline = util.get_cmdline()
-
-    if 'network-config=' in cmdline:
-        data64 = None
-        for tok in cmdline.split():
-            if tok.startswith("network-config="):
-                data64 = tok.split("=", 1)[1]
-        if data64:
-            return util.load_yaml(_b64dgz(data64))
-
-    if 'ip=' not in cmdline:
-        return None
-
-    if mac_addrs is None:
-        mac_addrs = {k: sys_netdev_info(k, 'address')
-                     for k in get_devicelist()}
-
-    return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
diff --git a/cloudinit/net/renderers/__init__.py b/cloudinit/net/renderers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cloudinit/net/renderers/eni.py b/cloudinit/net/renderers/eni.py
new file mode 100644
index 00000000..b427012e
--- /dev/null
+++ b/cloudinit/net/renderers/eni.py
@@ -0,0 +1,399 @@
+# vi: ts=4 expandtab
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import glob
+import os
+import re
+
+from cloudinit.net import LINKS_FNAME_PREFIX
+from cloudinit.net import ParserError
+from cloudinit.net.udev import generate_udev_rule
+from cloudinit import util
+
+
+NET_CONFIG_COMMANDS = [
+    "pre-up", "up", "post-up", "down", "pre-down", "post-down",
+]
+
+NET_CONFIG_BRIDGE_OPTIONS = [
+    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
+    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
+]
+
+NET_CONFIG_OPTIONS = [
+    "address", "netmask", "broadcast", "network", "metric", "gateway",
+    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
+    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
+    "netnum", "endpoint", "local", "ttl",
+]
+
+
+# TODO: switch valid_map based on mode inet/inet6
+def _iface_add_subnet(iface, subnet):
+    content = ""
+    valid_map = [
+        'address',
+        'netmask',
+        'broadcast',
+        'metric',
+        'gateway',
+        'pointopoint',
+        'mtu',
+        'scope',
+        'dns_search',
+        'dns_nameservers',
+    ]
+    for key, value in subnet.items():
+        if value and key in valid_map:
+            if type(value) == list:
+                value = " ".join(value)
+            if '_' in key:
+                key = key.replace('_', '-')
+            content += "    {} {}\n".format(key, value)
+
+    return content
+
+
+# TODO: switch to valid_map for attrs
+def _iface_add_attrs(iface):
+    content = ""
+    ignore_map = [
+        'control',
+        'index',
+        'inet',
+        'mode',
+        'name',
+        'subnets',
+        'type',
+    ]
+    if iface['type'] not in ['bond', 'bridge', 'vlan']:
+        ignore_map.append('mac_address')
+
+    for key, value in iface.items():
+        if value and key not in ignore_map:
+            if type(value) == list:
+                value = " ".join(value)
+            content += "    {} {}\n".format(key, value)
+
+    return content
+
+
+def _iface_start_entry(iface, index):
+    fullname = iface['name']
+    if index != 0:
+        fullname += ":%s" % index
+
+    control = iface['control']
+    if control == "auto":
+        cverb = "auto"
+    elif control in ("hotplug",):
+        cverb = "allow-" + control
+    else:
+        cverb = "# control-" + control
+
+    subst = iface.copy()
+    subst.update({'fullname': fullname, 'cverb': cverb})
+
+    return ("{cverb} {fullname}\n"
+            "iface {fullname} {inet} {mode}\n").format(**subst)
+
+
+def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
+    """Parses the file contents, placing result into ifaces.
+
+    '_source_path' is added to every dictionary entry to define which file
+    the configration information came from.
+
+    :param ifaces: interface dictionary
+    :param contents: contents of interfaces file
+    :param src_dir: directory interfaces file was located
+    :param src_path: file path the `contents` was read
+    """
+    currif = None
+    for line in contents.splitlines():
+        line = line.strip()
+        if line.startswith('#'):
+            continue
+        split = line.split(' ')
+        option = split[0]
+        if option == "source-directory":
+            parsed_src_dir = split[1]
+            if not parsed_src_dir.startswith("/"):
+                parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
+            for expanded_path in glob.glob(parsed_src_dir):
+                dir_contents = os.listdir(expanded_path)
+                dir_contents = [
+                    os.path.join(expanded_path, path)
+                    for path in dir_contents
+                    if (os.path.isfile(os.path.join(expanded_path, path)) and
+                        re.match("^[a-zA-Z0-9_-]+$", path) is not None)
+                ]
+                for entry in dir_contents:
+                    with open(entry, "r") as fp:
+                        src_data = fp.read().strip()
+                    abs_entry = os.path.abspath(entry)
+                    _parse_deb_config_data(
+                        ifaces, src_data,
+                        os.path.dirname(abs_entry), abs_entry)
+        elif option == "source":
+            new_src_path = split[1]
+            if not new_src_path.startswith("/"):
+                new_src_path = os.path.join(src_dir, new_src_path)
+            for expanded_path in glob.glob(new_src_path):
+                with open(expanded_path, "r") as fp:
+                    src_data = fp.read().strip()
+                abs_path = os.path.abspath(expanded_path)
+                _parse_deb_config_data(
+                    ifaces, src_data,
+                    os.path.dirname(abs_path), abs_path)
+        elif option == "auto":
+            for iface in split[1:]:
+                if iface not in ifaces:
+                    ifaces[iface] = {
+                        # Include the source path this interface was found in.
+                        "_source_path": src_path
+                    }
+                ifaces[iface]['auto'] = True
+        elif option == "iface":
+            iface, family, method = split[1:4]
+            if iface not in ifaces:
+                ifaces[iface] = {
+                    # Include the source path this interface was found in.
+                    "_source_path": src_path
+                }
+            elif 'family' in ifaces[iface]:
+                raise ParserError(
+                    "Interface %s can only be defined once. "
+                    "Re-defined in '%s'." % (iface, src_path))
+            ifaces[iface]['family'] = family
+            ifaces[iface]['method'] = method
+            currif = iface
+        elif option == "hwaddress":
+            ifaces[currif]['hwaddress'] = split[1]
+        elif option in NET_CONFIG_OPTIONS:
+            ifaces[currif][option] = split[1]
+        elif option in NET_CONFIG_COMMANDS:
+            if option not in ifaces[currif]:
+                ifaces[currif][option] = []
+            ifaces[currif][option].append(' '.join(split[1:]))
+        elif option.startswith('dns-'):
+            if 'dns' not in ifaces[currif]:
+                ifaces[currif]['dns'] = {}
+            if option == 'dns-search':
+                ifaces[currif]['dns']['search'] = []
+                for domain in split[1:]:
+                    ifaces[currif]['dns']['search'].append(domain)
+            elif option == 'dns-nameservers':
+                ifaces[currif]['dns']['nameservers'] = []
+                for server in split[1:]:
+                    ifaces[currif]['dns']['nameservers'].append(server)
+        elif option.startswith('bridge_'):
+            if 'bridge' not in ifaces[currif]:
+                ifaces[currif]['bridge'] = {}
+            if option in NET_CONFIG_BRIDGE_OPTIONS:
+                bridge_option = option.replace('bridge_', '', 1)
+                ifaces[currif]['bridge'][bridge_option] = split[1]
+            elif option == "bridge_ports":
+                ifaces[currif]['bridge']['ports'] = []
+                for iface in split[1:]:
+                    ifaces[currif]['bridge']['ports'].append(iface)
+            elif option == "bridge_hw" and split[1].lower() == "mac":
+                ifaces[currif]['bridge']['mac'] = split[2]
+            elif option == "bridge_pathcost":
+                if 'pathcost' not in ifaces[currif]['bridge']:
+                    ifaces[currif]['bridge']['pathcost'] = {}
+                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
+            elif option == "bridge_portprio":
+                if 'portprio' not in ifaces[currif]['bridge']:
+                    ifaces[currif]['bridge']['portprio'] = {}
+                ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
+        elif option.startswith('bond-'):
+            if 'bond' not in ifaces[currif]:
+                ifaces[currif]['bond'] = {}
+            bond_option = option.replace('bond-', '', 1)
+            ifaces[currif]['bond'][bond_option] = split[1]
+    for iface in ifaces.keys():
+        if 'auto' not in ifaces[iface]:
+            ifaces[iface]['auto'] = False
+
+
+def _parse_deb_config(path):
+    """Parses a debian network configuration file."""
+    ifaces = {}
+    with open(path, "r") as fp:
+        contents = fp.read().strip()
+    abs_path = os.path.abspath(path)
+    _parse_deb_config_data(
+        ifaces, contents,
+        os.path.dirname(abs_path), abs_path)
+    return ifaces
+
+
+class Renderer(object):
+    """Renders network information in a /etc/network/interfaces format."""
+
+    def render_persistent_net(self, network_state):
+        """Given state, emit udev rules to map mac to ifname."""
+        content = ""
+        interfaces = network_state.get('interfaces')
+        for iface in interfaces.values():
+            # for physical interfaces write out a persist net udev rule
+            if iface['type'] == 'physical' and \
+               'name' in iface and iface.get('mac_address'):
+                content += generate_udev_rule(iface['name'],
+                                              iface['mac_address'])
+
+        return content
+
+    def render_route(self, route, indent=""):
+        """ When rendering routes for an iface, in some cases applying a route
+        may result in the route command returning non-zero which produces
+        some confusing output for users manually using ifup/ifdown[1].  To
+        that end, we will optionally include an '|| true' postfix to each
+        route line allowing users to work with ifup/ifdown without using
+        --force option.
+
+        We may at somepoint not want to emit this additional postfix, and
+        add a 'strict' flag to this function.  When called with strict=True,
+        then we will not append the postfix.
+
+        1. http://askubuntu.com/questions/168033/
+                 how-to-set-static-routes-in-ubuntu-server
+        """
+        content = ""
+        up = indent + "post-up route add"
+        down = indent + "pre-down route del"
+        eol = " || true\n"
+        mapping = {
+            'network': '-net',
+            'netmask': 'netmask',
+            'gateway': 'gw',
+            'metric': 'metric',
+        }
+        if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
+            default_gw = " default gw %s" % route['gateway']
+            content += up + default_gw + eol
+            content += down + default_gw + eol
+        elif route['network'] == '::' and route['netmask'] == 0:
+            # ipv6!
+            default_gw = " -A inet6 default gw %s" % route['gateway']
+            content += up + default_gw + eol
+            content += down + default_gw + eol
+        else:
+            route_line = ""
+            for k in ['network', 'netmask', 'gateway', 'metric']:
+                if k in route:
+                    route_line += " %s %s" % (mapping[k], route[k])
+            content += up + route_line + eol
+            content += down + route_line + eol
+
+        return content
+
+    def render_interfaces(self, network_state):
+        ''' Given state, emit etc/network/interfaces content '''
+
+        content = ""
+        interfaces = network_state.get('interfaces')
+        ''' Apply a sort order to ensure that we write out
+            the physical interfaces first; this is critical for
+            bonding
+        '''
+        order = {
+            'physical': 0,
+            'bond': 1,
+            'bridge': 2,
+            'vlan': 3,
+        }
+        content += "auto lo\niface lo inet loopback\n"
+        for dnskey, value in network_state.get('dns', {}).items():
+            if len(value):
+                content += "    dns-{} {}\n".format(dnskey, " ".join(value))
+
+        for iface in sorted(interfaces.values(),
+                            key=lambda k: (order[k['type']], k['name'])):
+
+            if content[-2:] != "\n\n":
+                content += "\n"
+            subnets = iface.get('subnets', {})
+            if subnets:
+                for index, subnet in zip(range(0, len(subnets)), subnets):
+                    if content[-2:] != "\n\n":
+                        content += "\n"
+                    iface['index'] = index
+                    iface['mode'] = subnet['type']
+                    iface['control'] = subnet.get('control', 'auto')
+                    if iface['mode'].endswith('6'):
+                        iface['inet'] += '6'
+                    elif iface['mode'] == 'static' \
+                         and ":" in subnet['address']:
+                        iface['inet'] += '6'
+                    if iface['mode'].startswith('dhcp'):
+                        iface['mode'] = 'dhcp'
+
+                    content += _iface_start_entry(iface, index)
+                    content += _iface_add_subnet(iface, subnet)
+                    content += _iface_add_attrs(iface)
+            else:
+                # ifenslave docs say to auto the slave devices
+                if 'bond-master' in iface:
+                    content += "auto {name}\n".format(**iface)
+                content += "iface {name} {inet} {mode}\n".format(**iface)
+                content += _iface_add_attrs(iface)
+
+        for route in network_state.get('routes'):
+            content += self.render_route(route)
+
+        # global replacements until v2 format
+        content = content.replace('mac_address', 'hwaddress')
+        return content
+
+    def render_network_state(self,
+        target, network_state, eni="etc/network/interfaces",
+        links_prefix=LINKS_FNAME_PREFIX,
+        netrules='etc/udev/rules.d/70-persistent-net.rules'):
+
+        fpeni = os.path.sep.join((target, eni,))
+        util.ensure_dir(os.path.dirname(fpeni))
+        with open(fpeni, 'w+') as f:
+            f.write(self.render_interfaces(network_state))
+
+        if netrules:
+            netrules = os.path.sep.join((target, netrules,))
+            util.ensure_dir(os.path.dirname(netrules))
+            with open(netrules, 'w+') as f:
+                f.write(self.render_persistent_net(network_state))
+
+        if links_prefix:
+            self.render_systemd_links(target, network_state, links_prefix)
+
+    def render_systemd_links(self, target, network_state,
+                             links_prefix=LINKS_FNAME_PREFIX):
+        fp_prefix = os.path.sep.join((target, links_prefix))
+        for f in glob.glob(fp_prefix + "*"):
+            os.unlink(f)
+
+        interfaces = network_state.get('interfaces')
+        for iface in interfaces.values():
+            if (iface['type'] == 'physical' and 'name' in iface and
+                    iface.get('mac_address')):
+                fname = fp_prefix + iface['name'] + ".link"
+                with open(fname, "w") as fp:
+                    fp.write("\n".join([
+                        "[Match]",
+                        "MACAddress=" + iface['mac_address'],
+                        "",
+                        "[Link]",
+                        "Name=" + iface['name'],
+                        ""
+                    ]))
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index e6bd34fe..1112bf0d 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -43,7 +43,7 @@ from cloudinit import distros
 from cloudinit import helpers
 from cloudinit import importer
 from cloudinit import log as logging
-from cloudinit.net import klibc
+from cloudinit.net import cmdline
 from cloudinit import sources
 from cloudinit import type_utils
 from cloudinit import util
@@ -579,7 +579,7 @@ class Init(object):
         if os.path.exists(disable_file):
             return (None, disable_file)
 
-        cmdline_cfg = ('cmdline', klibc.read_kernel_cmdline_config())
+        cmdline_cfg = ('cmdline', cmdline.read_kernel_cmdline_config())
         dscfg = ('ds', None)
         if self.datasource and hasattr(self.datasource, 'network_config'):
             dscfg = ('ds', self.datasource.network_config)
-- 
cgit v1.2.3


From cc56ef479a4cfa4520dfcc7cc27c35bb6ac86bd2 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Tue, 10 May 2016 13:18:53 -0700
Subject: Fix up tests and debian distro

---
 cloudinit/distros/debian.py |  4 ++--
 tests/unittests/test_net.py | 23 ++++++++++++-----------
 2 files changed, 14 insertions(+), 13 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index 21a4f805..465413a8 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -27,7 +27,7 @@ from cloudinit import helpers
 from cloudinit import log as logging
 from cloudinit import util
 from cloudinit import net
-from cloudinit.net.distros import debian
+from cloudinit.net.renderers import eni
 
 from cloudinit.distros.parsers.hostname import HostnameConf
 
@@ -57,7 +57,7 @@ class Distro(distros.Distro):
         # should only happen say once per instance...)
         self._runner = helpers.Runners(paths)
         self.osfamily = 'debian'
-        self._net_renderer = debian.Renderer()
+        self._net_renderer = eni.Renderer()
 
     def apply_locale(self, locale, out_fn=None):
         if not out_fn:
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index a0cdc493..6daf9601 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -1,5 +1,6 @@
 from cloudinit import util
 from cloudinit import net
+from cloudinit.net import cmdline
 from .helpers import TestCase
 
 import base64
@@ -74,15 +75,15 @@ class TestNetConfigParsing(TestCase):
                     "mac_address": "c0:d6:9f:2c:e8:80",
                     "subnets": [{"type": "dhcp"}]}]}
 
-    def test_klibc_convert_dhcp(self):
-        found = net._klibc_to_config_entry(DHCP_CONTENT_1)
+    def test_cmdline_convert_dhcp(self):
+        found = cmdline._klibc_to_config_entry(DHCP_CONTENT_1)
         self.assertEqual(found, ('eth0', DHCP_EXPECTED_1))
 
-    def test_klibc_convert_static(self):
-        found = net._klibc_to_config_entry(STATIC_CONTENT_1)
+    def test_cmdline_convert_static(self):
+        found = cmdline._klibc_to_config_entry(STATIC_CONTENT_1)
         self.assertEqual(found, ('eth1', STATIC_EXPECTED_1))
 
-    def test_config_from_klibc_net_cfg(self):
+    def test_config_from_cmdline_net_cfg(self):
         files = []
         pairs = (('net-eth0.cfg', DHCP_CONTENT_1),
                  ('net-eth1.cfg', STATIC_CONTENT_1))
@@ -103,25 +104,25 @@ class TestNetConfigParsing(TestCase):
                 files.append(fp)
                 util.write_file(fp, content)
 
-            found = net.config_from_klibc_net_cfg(files=files, mac_addrs=macs)
+            found = cmdline.config_from_klibc_net_cfg(files=files,
+                                                      mac_addrs=macs)
             self.assertEqual(found, expected)
 
     def test_cmdline_with_b64(self):
         data = base64.b64encode(json.dumps(self.simple_cfg).encode())
         encoded_text = data.decode()
-        cmdline = 'ro network-config=' + encoded_text + ' root=foo'
-        found = net.read_kernel_cmdline_config(cmdline=cmdline)
+        raw_cmdline = 'ro network-config=' + encoded_text + ' root=foo'
+        found = cmdline.read_kernel_cmdline_config(cmdline=raw_cmdline)
         self.assertEqual(found, self.simple_cfg)
 
     def test_cmdline_with_b64_gz(self):
         data = _gzip_data(json.dumps(self.simple_cfg).encode())
         encoded_text = base64.b64encode(data).decode()
-        cmdline = 'ro network-config=' + encoded_text + ' root=foo'
-        found = net.read_kernel_cmdline_config(cmdline=cmdline)
+        raw_cmdline = 'ro network-config=' + encoded_text + ' root=foo'
+        found = cmdline.read_kernel_cmdline_config(cmdline=raw_cmdline)
         self.assertEqual(found, self.simple_cfg)
 
 
-
 def _gzip_data(data):
     with io.BytesIO() as iobuf:
         gzfp = gzip.GzipFile(mode="wb", fileobj=iobuf)
-- 
cgit v1.2.3


From 281551d4125b40836686793b6a0f8d2c34c3357f Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Tue, 10 May 2016 14:10:54 -0700
Subject: Move net/renderers -> net

---
 cloudinit/distros/debian.py         |   2 +-
 cloudinit/net/eni.py                | 399 ++++++++++++++++++++++++++++++++++++
 cloudinit/net/renderers/__init__.py |   0
 cloudinit/net/renderers/eni.py      | 399 ------------------------------------
 4 files changed, 400 insertions(+), 400 deletions(-)
 create mode 100644 cloudinit/net/eni.py
 delete mode 100644 cloudinit/net/renderers/__init__.py
 delete mode 100644 cloudinit/net/renderers/eni.py

(limited to 'cloudinit')

diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index 465413a8..ca069a60 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -27,7 +27,7 @@ from cloudinit import helpers
 from cloudinit import log as logging
 from cloudinit import util
 from cloudinit import net
-from cloudinit.net.renderers import eni
+from cloudinit.net import eni
 
 from cloudinit.distros.parsers.hostname import HostnameConf
 
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
new file mode 100644
index 00000000..b427012e
--- /dev/null
+++ b/cloudinit/net/eni.py
@@ -0,0 +1,399 @@
+# vi: ts=4 expandtab
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import glob
+import os
+import re
+
+from cloudinit.net import LINKS_FNAME_PREFIX
+from cloudinit.net import ParserError
+from cloudinit.net.udev import generate_udev_rule
+from cloudinit import util
+
+
+NET_CONFIG_COMMANDS = [
+    "pre-up", "up", "post-up", "down", "pre-down", "post-down",
+]
+
+NET_CONFIG_BRIDGE_OPTIONS = [
+    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
+    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
+]
+
+NET_CONFIG_OPTIONS = [
+    "address", "netmask", "broadcast", "network", "metric", "gateway",
+    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
+    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
+    "netnum", "endpoint", "local", "ttl",
+]
+
+
+# TODO: switch valid_map based on mode inet/inet6
+def _iface_add_subnet(iface, subnet):
+    content = ""
+    valid_map = [
+        'address',
+        'netmask',
+        'broadcast',
+        'metric',
+        'gateway',
+        'pointopoint',
+        'mtu',
+        'scope',
+        'dns_search',
+        'dns_nameservers',
+    ]
+    for key, value in subnet.items():
+        if value and key in valid_map:
+            if type(value) == list:
+                value = " ".join(value)
+            if '_' in key:
+                key = key.replace('_', '-')
+            content += "    {} {}\n".format(key, value)
+
+    return content
+
+
+# TODO: switch to valid_map for attrs
+def _iface_add_attrs(iface):
+    content = ""
+    ignore_map = [
+        'control',
+        'index',
+        'inet',
+        'mode',
+        'name',
+        'subnets',
+        'type',
+    ]
+    if iface['type'] not in ['bond', 'bridge', 'vlan']:
+        ignore_map.append('mac_address')
+
+    for key, value in iface.items():
+        if value and key not in ignore_map:
+            if type(value) == list:
+                value = " ".join(value)
+            content += "    {} {}\n".format(key, value)
+
+    return content
+
+
+def _iface_start_entry(iface, index):
+    fullname = iface['name']
+    if index != 0:
+        fullname += ":%s" % index
+
+    control = iface['control']
+    if control == "auto":
+        cverb = "auto"
+    elif control in ("hotplug",):
+        cverb = "allow-" + control
+    else:
+        cverb = "# control-" + control
+
+    subst = iface.copy()
+    subst.update({'fullname': fullname, 'cverb': cverb})
+
+    return ("{cverb} {fullname}\n"
+            "iface {fullname} {inet} {mode}\n").format(**subst)
+
+
+def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
+    """Parses the file contents, placing result into ifaces.
+
+    '_source_path' is added to every dictionary entry to define which file
+    the configration information came from.
+
+    :param ifaces: interface dictionary
+    :param contents: contents of interfaces file
+    :param src_dir: directory interfaces file was located
+    :param src_path: file path the `contents` was read
+    """
+    currif = None
+    for line in contents.splitlines():
+        line = line.strip()
+        if line.startswith('#'):
+            continue
+        split = line.split(' ')
+        option = split[0]
+        if option == "source-directory":
+            parsed_src_dir = split[1]
+            if not parsed_src_dir.startswith("/"):
+                parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
+            for expanded_path in glob.glob(parsed_src_dir):
+                dir_contents = os.listdir(expanded_path)
+                dir_contents = [
+                    os.path.join(expanded_path, path)
+                    for path in dir_contents
+                    if (os.path.isfile(os.path.join(expanded_path, path)) and
+                        re.match("^[a-zA-Z0-9_-]+$", path) is not None)
+                ]
+                for entry in dir_contents:
+                    with open(entry, "r") as fp:
+                        src_data = fp.read().strip()
+                    abs_entry = os.path.abspath(entry)
+                    _parse_deb_config_data(
+                        ifaces, src_data,
+                        os.path.dirname(abs_entry), abs_entry)
+        elif option == "source":
+            new_src_path = split[1]
+            if not new_src_path.startswith("/"):
+                new_src_path = os.path.join(src_dir, new_src_path)
+            for expanded_path in glob.glob(new_src_path):
+                with open(expanded_path, "r") as fp:
+                    src_data = fp.read().strip()
+                abs_path = os.path.abspath(expanded_path)
+                _parse_deb_config_data(
+                    ifaces, src_data,
+                    os.path.dirname(abs_path), abs_path)
+        elif option == "auto":
+            for iface in split[1:]:
+                if iface not in ifaces:
+                    ifaces[iface] = {
+                        # Include the source path this interface was found in.
+                        "_source_path": src_path
+                    }
+                ifaces[iface]['auto'] = True
+        elif option == "iface":
+            iface, family, method = split[1:4]
+            if iface not in ifaces:
+                ifaces[iface] = {
+                    # Include the source path this interface was found in.
+                    "_source_path": src_path
+                }
+            elif 'family' in ifaces[iface]:
+                raise ParserError(
+                    "Interface %s can only be defined once. "
+                    "Re-defined in '%s'." % (iface, src_path))
+            ifaces[iface]['family'] = family
+            ifaces[iface]['method'] = method
+            currif = iface
+        elif option == "hwaddress":
+            ifaces[currif]['hwaddress'] = split[1]
+        elif option in NET_CONFIG_OPTIONS:
+            ifaces[currif][option] = split[1]
+        elif option in NET_CONFIG_COMMANDS:
+            if option not in ifaces[currif]:
+                ifaces[currif][option] = []
+            ifaces[currif][option].append(' '.join(split[1:]))
+        elif option.startswith('dns-'):
+            if 'dns' not in ifaces[currif]:
+                ifaces[currif]['dns'] = {}
+            if option == 'dns-search':
+                ifaces[currif]['dns']['search'] = []
+                for domain in split[1:]:
+                    ifaces[currif]['dns']['search'].append(domain)
+            elif option == 'dns-nameservers':
+                ifaces[currif]['dns']['nameservers'] = []
+                for server in split[1:]:
+                    ifaces[currif]['dns']['nameservers'].append(server)
+        elif option.startswith('bridge_'):
+            if 'bridge' not in ifaces[currif]:
+                ifaces[currif]['bridge'] = {}
+            if option in NET_CONFIG_BRIDGE_OPTIONS:
+                bridge_option = option.replace('bridge_', '', 1)
+                ifaces[currif]['bridge'][bridge_option] = split[1]
+            elif option == "bridge_ports":
+                ifaces[currif]['bridge']['ports'] = []
+                for iface in split[1:]:
+                    ifaces[currif]['bridge']['ports'].append(iface)
+            elif option == "bridge_hw" and split[1].lower() == "mac":
+                ifaces[currif]['bridge']['mac'] = split[2]
+            elif option == "bridge_pathcost":
+                if 'pathcost' not in ifaces[currif]['bridge']:
+                    ifaces[currif]['bridge']['pathcost'] = {}
+                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
+            elif option == "bridge_portprio":
+                if 'portprio' not in ifaces[currif]['bridge']:
+                    ifaces[currif]['bridge']['portprio'] = {}
+                ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
+        elif option.startswith('bond-'):
+            if 'bond' not in ifaces[currif]:
+                ifaces[currif]['bond'] = {}
+            bond_option = option.replace('bond-', '', 1)
+            ifaces[currif]['bond'][bond_option] = split[1]
+    for iface in ifaces.keys():
+        if 'auto' not in ifaces[iface]:
+            ifaces[iface]['auto'] = False
+
+
+def _parse_deb_config(path):
+    """Parses a debian network configuration file."""
+    ifaces = {}
+    with open(path, "r") as fp:
+        contents = fp.read().strip()
+    abs_path = os.path.abspath(path)
+    _parse_deb_config_data(
+        ifaces, contents,
+        os.path.dirname(abs_path), abs_path)
+    return ifaces
+
+
+class Renderer(object):
+    """Renders network information in a /etc/network/interfaces format."""
+
+    def render_persistent_net(self, network_state):
+        """Given state, emit udev rules to map mac to ifname."""
+        content = ""
+        interfaces = network_state.get('interfaces')
+        for iface in interfaces.values():
+            # for physical interfaces write out a persist net udev rule
+            if iface['type'] == 'physical' and \
+               'name' in iface and iface.get('mac_address'):
+                content += generate_udev_rule(iface['name'],
+                                              iface['mac_address'])
+
+        return content
+
+    def render_route(self, route, indent=""):
+        """ When rendering routes for an iface, in some cases applying a route
+        may result in the route command returning non-zero which produces
+        some confusing output for users manually using ifup/ifdown[1].  To
+        that end, we will optionally include an '|| true' postfix to each
+        route line allowing users to work with ifup/ifdown without using
+        --force option.
+
+        We may at somepoint not want to emit this additional postfix, and
+        add a 'strict' flag to this function.  When called with strict=True,
+        then we will not append the postfix.
+
+        1. http://askubuntu.com/questions/168033/
+                 how-to-set-static-routes-in-ubuntu-server
+        """
+        content = ""
+        up = indent + "post-up route add"
+        down = indent + "pre-down route del"
+        eol = " || true\n"
+        mapping = {
+            'network': '-net',
+            'netmask': 'netmask',
+            'gateway': 'gw',
+            'metric': 'metric',
+        }
+        if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
+            default_gw = " default gw %s" % route['gateway']
+            content += up + default_gw + eol
+            content += down + default_gw + eol
+        elif route['network'] == '::' and route['netmask'] == 0:
+            # ipv6!
+            default_gw = " -A inet6 default gw %s" % route['gateway']
+            content += up + default_gw + eol
+            content += down + default_gw + eol
+        else:
+            route_line = ""
+            for k in ['network', 'netmask', 'gateway', 'metric']:
+                if k in route:
+                    route_line += " %s %s" % (mapping[k], route[k])
+            content += up + route_line + eol
+            content += down + route_line + eol
+
+        return content
+
+    def render_interfaces(self, network_state):
+        ''' Given state, emit etc/network/interfaces content '''
+
+        content = ""
+        interfaces = network_state.get('interfaces')
+        ''' Apply a sort order to ensure that we write out
+            the physical interfaces first; this is critical for
+            bonding
+        '''
+        order = {
+            'physical': 0,
+            'bond': 1,
+            'bridge': 2,
+            'vlan': 3,
+        }
+        content += "auto lo\niface lo inet loopback\n"
+        for dnskey, value in network_state.get('dns', {}).items():
+            if len(value):
+                content += "    dns-{} {}\n".format(dnskey, " ".join(value))
+
+        for iface in sorted(interfaces.values(),
+                            key=lambda k: (order[k['type']], k['name'])):
+
+            if content[-2:] != "\n\n":
+                content += "\n"
+            subnets = iface.get('subnets', {})
+            if subnets:
+                for index, subnet in zip(range(0, len(subnets)), subnets):
+                    if content[-2:] != "\n\n":
+                        content += "\n"
+                    iface['index'] = index
+                    iface['mode'] = subnet['type']
+                    iface['control'] = subnet.get('control', 'auto')
+                    if iface['mode'].endswith('6'):
+                        iface['inet'] += '6'
+                    elif iface['mode'] == 'static' \
+                         and ":" in subnet['address']:
+                        iface['inet'] += '6'
+                    if iface['mode'].startswith('dhcp'):
+                        iface['mode'] = 'dhcp'
+
+                    content += _iface_start_entry(iface, index)
+                    content += _iface_add_subnet(iface, subnet)
+                    content += _iface_add_attrs(iface)
+            else:
+                # ifenslave docs say to auto the slave devices
+                if 'bond-master' in iface:
+                    content += "auto {name}\n".format(**iface)
+                content += "iface {name} {inet} {mode}\n".format(**iface)
+                content += _iface_add_attrs(iface)
+
+        for route in network_state.get('routes'):
+            content += self.render_route(route)
+
+        # global replacements until v2 format
+        content = content.replace('mac_address', 'hwaddress')
+        return content
+
+    def render_network_state(self,
+        target, network_state, eni="etc/network/interfaces",
+        links_prefix=LINKS_FNAME_PREFIX,
+        netrules='etc/udev/rules.d/70-persistent-net.rules'):
+
+        fpeni = os.path.sep.join((target, eni,))
+        util.ensure_dir(os.path.dirname(fpeni))
+        with open(fpeni, 'w+') as f:
+            f.write(self.render_interfaces(network_state))
+
+        if netrules:
+            netrules = os.path.sep.join((target, netrules,))
+            util.ensure_dir(os.path.dirname(netrules))
+            with open(netrules, 'w+') as f:
+                f.write(self.render_persistent_net(network_state))
+
+        if links_prefix:
+            self.render_systemd_links(target, network_state, links_prefix)
+
+    def render_systemd_links(self, target, network_state,
+                             links_prefix=LINKS_FNAME_PREFIX):
+        fp_prefix = os.path.sep.join((target, links_prefix))
+        for f in glob.glob(fp_prefix + "*"):
+            os.unlink(f)
+
+        interfaces = network_state.get('interfaces')
+        for iface in interfaces.values():
+            if (iface['type'] == 'physical' and 'name' in iface and
+                    iface.get('mac_address')):
+                fname = fp_prefix + iface['name'] + ".link"
+                with open(fname, "w") as fp:
+                    fp.write("\n".join([
+                        "[Match]",
+                        "MACAddress=" + iface['mac_address'],
+                        "",
+                        "[Link]",
+                        "Name=" + iface['name'],
+                        ""
+                    ]))
diff --git a/cloudinit/net/renderers/__init__.py b/cloudinit/net/renderers/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/cloudinit/net/renderers/eni.py b/cloudinit/net/renderers/eni.py
deleted file mode 100644
index b427012e..00000000
--- a/cloudinit/net/renderers/eni.py
+++ /dev/null
@@ -1,399 +0,0 @@
-# vi: ts=4 expandtab
-#
-#    This program is free software: you can redistribute it and/or modify
-#    it under the terms of the GNU General Public License version 3, as
-#    published by the Free Software Foundation.
-#
-#    This program is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU General Public License for more details.
-#
-#    You should have received a copy of the GNU General Public License
-#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import glob
-import os
-import re
-
-from cloudinit.net import LINKS_FNAME_PREFIX
-from cloudinit.net import ParserError
-from cloudinit.net.udev import generate_udev_rule
-from cloudinit import util
-
-
-NET_CONFIG_COMMANDS = [
-    "pre-up", "up", "post-up", "down", "pre-down", "post-down",
-]
-
-NET_CONFIG_BRIDGE_OPTIONS = [
-    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
-    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
-]
-
-NET_CONFIG_OPTIONS = [
-    "address", "netmask", "broadcast", "network", "metric", "gateway",
-    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
-    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
-    "netnum", "endpoint", "local", "ttl",
-]
-
-
-# TODO: switch valid_map based on mode inet/inet6
-def _iface_add_subnet(iface, subnet):
-    content = ""
-    valid_map = [
-        'address',
-        'netmask',
-        'broadcast',
-        'metric',
-        'gateway',
-        'pointopoint',
-        'mtu',
-        'scope',
-        'dns_search',
-        'dns_nameservers',
-    ]
-    for key, value in subnet.items():
-        if value and key in valid_map:
-            if type(value) == list:
-                value = " ".join(value)
-            if '_' in key:
-                key = key.replace('_', '-')
-            content += "    {} {}\n".format(key, value)
-
-    return content
-
-
-# TODO: switch to valid_map for attrs
-def _iface_add_attrs(iface):
-    content = ""
-    ignore_map = [
-        'control',
-        'index',
-        'inet',
-        'mode',
-        'name',
-        'subnets',
-        'type',
-    ]
-    if iface['type'] not in ['bond', 'bridge', 'vlan']:
-        ignore_map.append('mac_address')
-
-    for key, value in iface.items():
-        if value and key not in ignore_map:
-            if type(value) == list:
-                value = " ".join(value)
-            content += "    {} {}\n".format(key, value)
-
-    return content
-
-
-def _iface_start_entry(iface, index):
-    fullname = iface['name']
-    if index != 0:
-        fullname += ":%s" % index
-
-    control = iface['control']
-    if control == "auto":
-        cverb = "auto"
-    elif control in ("hotplug",):
-        cverb = "allow-" + control
-    else:
-        cverb = "# control-" + control
-
-    subst = iface.copy()
-    subst.update({'fullname': fullname, 'cverb': cverb})
-
-    return ("{cverb} {fullname}\n"
-            "iface {fullname} {inet} {mode}\n").format(**subst)
-
-
-def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
-    """Parses the file contents, placing result into ifaces.
-
-    '_source_path' is added to every dictionary entry to define which file
-    the configration information came from.
-
-    :param ifaces: interface dictionary
-    :param contents: contents of interfaces file
-    :param src_dir: directory interfaces file was located
-    :param src_path: file path the `contents` was read
-    """
-    currif = None
-    for line in contents.splitlines():
-        line = line.strip()
-        if line.startswith('#'):
-            continue
-        split = line.split(' ')
-        option = split[0]
-        if option == "source-directory":
-            parsed_src_dir = split[1]
-            if not parsed_src_dir.startswith("/"):
-                parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
-            for expanded_path in glob.glob(parsed_src_dir):
-                dir_contents = os.listdir(expanded_path)
-                dir_contents = [
-                    os.path.join(expanded_path, path)
-                    for path in dir_contents
-                    if (os.path.isfile(os.path.join(expanded_path, path)) and
-                        re.match("^[a-zA-Z0-9_-]+$", path) is not None)
-                ]
-                for entry in dir_contents:
-                    with open(entry, "r") as fp:
-                        src_data = fp.read().strip()
-                    abs_entry = os.path.abspath(entry)
-                    _parse_deb_config_data(
-                        ifaces, src_data,
-                        os.path.dirname(abs_entry), abs_entry)
-        elif option == "source":
-            new_src_path = split[1]
-            if not new_src_path.startswith("/"):
-                new_src_path = os.path.join(src_dir, new_src_path)
-            for expanded_path in glob.glob(new_src_path):
-                with open(expanded_path, "r") as fp:
-                    src_data = fp.read().strip()
-                abs_path = os.path.abspath(expanded_path)
-                _parse_deb_config_data(
-                    ifaces, src_data,
-                    os.path.dirname(abs_path), abs_path)
-        elif option == "auto":
-            for iface in split[1:]:
-                if iface not in ifaces:
-                    ifaces[iface] = {
-                        # Include the source path this interface was found in.
-                        "_source_path": src_path
-                    }
-                ifaces[iface]['auto'] = True
-        elif option == "iface":
-            iface, family, method = split[1:4]
-            if iface not in ifaces:
-                ifaces[iface] = {
-                    # Include the source path this interface was found in.
-                    "_source_path": src_path
-                }
-            elif 'family' in ifaces[iface]:
-                raise ParserError(
-                    "Interface %s can only be defined once. "
-                    "Re-defined in '%s'." % (iface, src_path))
-            ifaces[iface]['family'] = family
-            ifaces[iface]['method'] = method
-            currif = iface
-        elif option == "hwaddress":
-            ifaces[currif]['hwaddress'] = split[1]
-        elif option in NET_CONFIG_OPTIONS:
-            ifaces[currif][option] = split[1]
-        elif option in NET_CONFIG_COMMANDS:
-            if option not in ifaces[currif]:
-                ifaces[currif][option] = []
-            ifaces[currif][option].append(' '.join(split[1:]))
-        elif option.startswith('dns-'):
-            if 'dns' not in ifaces[currif]:
-                ifaces[currif]['dns'] = {}
-            if option == 'dns-search':
-                ifaces[currif]['dns']['search'] = []
-                for domain in split[1:]:
-                    ifaces[currif]['dns']['search'].append(domain)
-            elif option == 'dns-nameservers':
-                ifaces[currif]['dns']['nameservers'] = []
-                for server in split[1:]:
-                    ifaces[currif]['dns']['nameservers'].append(server)
-        elif option.startswith('bridge_'):
-            if 'bridge' not in ifaces[currif]:
-                ifaces[currif]['bridge'] = {}
-            if option in NET_CONFIG_BRIDGE_OPTIONS:
-                bridge_option = option.replace('bridge_', '', 1)
-                ifaces[currif]['bridge'][bridge_option] = split[1]
-            elif option == "bridge_ports":
-                ifaces[currif]['bridge']['ports'] = []
-                for iface in split[1:]:
-                    ifaces[currif]['bridge']['ports'].append(iface)
-            elif option == "bridge_hw" and split[1].lower() == "mac":
-                ifaces[currif]['bridge']['mac'] = split[2]
-            elif option == "bridge_pathcost":
-                if 'pathcost' not in ifaces[currif]['bridge']:
-                    ifaces[currif]['bridge']['pathcost'] = {}
-                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
-            elif option == "bridge_portprio":
-                if 'portprio' not in ifaces[currif]['bridge']:
-                    ifaces[currif]['bridge']['portprio'] = {}
-                ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
-        elif option.startswith('bond-'):
-            if 'bond' not in ifaces[currif]:
-                ifaces[currif]['bond'] = {}
-            bond_option = option.replace('bond-', '', 1)
-            ifaces[currif]['bond'][bond_option] = split[1]
-    for iface in ifaces.keys():
-        if 'auto' not in ifaces[iface]:
-            ifaces[iface]['auto'] = False
-
-
-def _parse_deb_config(path):
-    """Parses a debian network configuration file."""
-    ifaces = {}
-    with open(path, "r") as fp:
-        contents = fp.read().strip()
-    abs_path = os.path.abspath(path)
-    _parse_deb_config_data(
-        ifaces, contents,
-        os.path.dirname(abs_path), abs_path)
-    return ifaces
-
-
-class Renderer(object):
-    """Renders network information in a /etc/network/interfaces format."""
-
-    def render_persistent_net(self, network_state):
-        """Given state, emit udev rules to map mac to ifname."""
-        content = ""
-        interfaces = network_state.get('interfaces')
-        for iface in interfaces.values():
-            # for physical interfaces write out a persist net udev rule
-            if iface['type'] == 'physical' and \
-               'name' in iface and iface.get('mac_address'):
-                content += generate_udev_rule(iface['name'],
-                                              iface['mac_address'])
-
-        return content
-
-    def render_route(self, route, indent=""):
-        """ When rendering routes for an iface, in some cases applying a route
-        may result in the route command returning non-zero which produces
-        some confusing output for users manually using ifup/ifdown[1].  To
-        that end, we will optionally include an '|| true' postfix to each
-        route line allowing users to work with ifup/ifdown without using
-        --force option.
-
-        We may at somepoint not want to emit this additional postfix, and
-        add a 'strict' flag to this function.  When called with strict=True,
-        then we will not append the postfix.
-
-        1. http://askubuntu.com/questions/168033/
-                 how-to-set-static-routes-in-ubuntu-server
-        """
-        content = ""
-        up = indent + "post-up route add"
-        down = indent + "pre-down route del"
-        eol = " || true\n"
-        mapping = {
-            'network': '-net',
-            'netmask': 'netmask',
-            'gateway': 'gw',
-            'metric': 'metric',
-        }
-        if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
-            default_gw = " default gw %s" % route['gateway']
-            content += up + default_gw + eol
-            content += down + default_gw + eol
-        elif route['network'] == '::' and route['netmask'] == 0:
-            # ipv6!
-            default_gw = " -A inet6 default gw %s" % route['gateway']
-            content += up + default_gw + eol
-            content += down + default_gw + eol
-        else:
-            route_line = ""
-            for k in ['network', 'netmask', 'gateway', 'metric']:
-                if k in route:
-                    route_line += " %s %s" % (mapping[k], route[k])
-            content += up + route_line + eol
-            content += down + route_line + eol
-
-        return content
-
-    def render_interfaces(self, network_state):
-        ''' Given state, emit etc/network/interfaces content '''
-
-        content = ""
-        interfaces = network_state.get('interfaces')
-        ''' Apply a sort order to ensure that we write out
-            the physical interfaces first; this is critical for
-            bonding
-        '''
-        order = {
-            'physical': 0,
-            'bond': 1,
-            'bridge': 2,
-            'vlan': 3,
-        }
-        content += "auto lo\niface lo inet loopback\n"
-        for dnskey, value in network_state.get('dns', {}).items():
-            if len(value):
-                content += "    dns-{} {}\n".format(dnskey, " ".join(value))
-
-        for iface in sorted(interfaces.values(),
-                            key=lambda k: (order[k['type']], k['name'])):
-
-            if content[-2:] != "\n\n":
-                content += "\n"
-            subnets = iface.get('subnets', {})
-            if subnets:
-                for index, subnet in zip(range(0, len(subnets)), subnets):
-                    if content[-2:] != "\n\n":
-                        content += "\n"
-                    iface['index'] = index
-                    iface['mode'] = subnet['type']
-                    iface['control'] = subnet.get('control', 'auto')
-                    if iface['mode'].endswith('6'):
-                        iface['inet'] += '6'
-                    elif iface['mode'] == 'static' \
-                         and ":" in subnet['address']:
-                        iface['inet'] += '6'
-                    if iface['mode'].startswith('dhcp'):
-                        iface['mode'] = 'dhcp'
-
-                    content += _iface_start_entry(iface, index)
-                    content += _iface_add_subnet(iface, subnet)
-                    content += _iface_add_attrs(iface)
-            else:
-                # ifenslave docs say to auto the slave devices
-                if 'bond-master' in iface:
-                    content += "auto {name}\n".format(**iface)
-                content += "iface {name} {inet} {mode}\n".format(**iface)
-                content += _iface_add_attrs(iface)
-
-        for route in network_state.get('routes'):
-            content += self.render_route(route)
-
-        # global replacements until v2 format
-        content = content.replace('mac_address', 'hwaddress')
-        return content
-
-    def render_network_state(self,
-        target, network_state, eni="etc/network/interfaces",
-        links_prefix=LINKS_FNAME_PREFIX,
-        netrules='etc/udev/rules.d/70-persistent-net.rules'):
-
-        fpeni = os.path.sep.join((target, eni,))
-        util.ensure_dir(os.path.dirname(fpeni))
-        with open(fpeni, 'w+') as f:
-            f.write(self.render_interfaces(network_state))
-
-        if netrules:
-            netrules = os.path.sep.join((target, netrules,))
-            util.ensure_dir(os.path.dirname(netrules))
-            with open(netrules, 'w+') as f:
-                f.write(self.render_persistent_net(network_state))
-
-        if links_prefix:
-            self.render_systemd_links(target, network_state, links_prefix)
-
-    def render_systemd_links(self, target, network_state,
-                             links_prefix=LINKS_FNAME_PREFIX):
-        fp_prefix = os.path.sep.join((target, links_prefix))
-        for f in glob.glob(fp_prefix + "*"):
-            os.unlink(f)
-
-        interfaces = network_state.get('interfaces')
-        for iface in interfaces.values():
-            if (iface['type'] == 'physical' and 'name' in iface and
-                    iface.get('mac_address')):
-                fname = fp_prefix + iface['name'] + ".link"
-                with open(fname, "w") as fp:
-                    fp.write("\n".join([
-                        "[Match]",
-                        "MACAddress=" + iface['mac_address'],
-                        "",
-                        "[Link]",
-                        "Name=" + iface['name'],
-                        ""
-                    ]))
-- 
cgit v1.2.3


From 4b0d2430e7674d5abb8fb27ac9ddb129d2bc0715 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Tue, 10 May 2016 14:16:07 -0700
Subject: Fix removal of validate_command

---
 cloudinit/net/network_state.py | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 73be84e1..2530a601 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -39,26 +39,25 @@ def from_state_file(state_file):
     return network_state
 
 
+def diff_keys(expected, actual):
+    missing = set(expected)
+    for key in actual:
+        missing.discard(key)
+    return missing
+
+
 class InvalidCommand(Exception):
     pass
 
 
 def ensure_command_keys(required_keys):
-    required_keys = frozenset(required_keys)
-
-    def extract_missing(command):
-        missing_keys = set()
-        for key in required_keys:
-            if key not in command:
-                missing_keys.add(key)
-        return missing_keys
 
     def wrapper(func):
 
         @six.wraps(func)
         def decorator(self, command, *args, **kwargs):
             if required_keys:
-                missing_keys = extract_missing(command)
+                missing_keys = diff_keys(required_keys, command)
                 if missing_keys:
                     raise InvalidCommand("Command missing %s of required"
                                          " keys %s" % (missing_keys,
@@ -120,10 +119,11 @@ class NetworkState(object):
             raise Exception('Invalid state, missing version field')
 
         required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]
-        if not self.valid_command(state, required_keys):
-            msg = 'Invalid state, missing keys: {}'.format(required_keys)
+        missing_keys = diff_keys(required_keys, state)
+        if missing_keys:
+            msg = 'Invalid state, missing keys: %s'.format(missing_keys)
             LOG.error(msg)
-            raise Exception(msg)
+            raise ValueError(msg)
 
         # v1 - direct attr mapping, except version
         for key in [k for k in required_keys if k not in ['version']]:
-- 
cgit v1.2.3


From c5eb65ed705475640fce1025c74a54052c6e9731 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Tue, 10 May 2016 15:12:44 -0700
Subject: Add some basic eni rendering tests

---
 cloudinit/net/__init__.py      | 11 ++++---
 cloudinit/net/network_state.py |  2 +-
 tests/unittests/test_net.py    | 68 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 74 insertions(+), 7 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index e911ed0c..0202cbd8 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -107,7 +107,7 @@ class ParserError(Exception):
     """Raised when parser has issue parsing the interfaces file."""
 
 
-def parse_net_config_data(net_config):
+def parse_net_config_data(net_config, skip_broken=True):
     """Parses the config, returns NetworkState object
 
     :param net_config: curtin network config dict
@@ -116,20 +116,19 @@ def parse_net_config_data(net_config):
     if 'version' in net_config and 'config' in net_config:
         ns = network_state.NetworkState(version=net_config.get('version'),
                                         config=net_config.get('config'))
-        ns.parse_config()
+        ns.parse_config(skip_broken=skip_broken)
         state = ns.network_state
-
     return state
 
 
-def parse_net_config(path):
+def parse_net_config(path, skip_broken=True):
     """Parses a curtin network configuration file and
        return network state"""
     ns = None
     net_config = util.read_conf(path)
     if 'network' in net_config:
-        ns = parse_net_config_data(net_config.get('network'))
-
+        ns = parse_net_config_data(net_config.get('network'),
+                                   skip_broken=skip_broken)
     return ns
 
 
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 2530a601..2feffa71 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -142,7 +142,7 @@ class NetworkState(object):
                 raise RuntimeError("No handler found for"
                                    " command '%s'" % command_type)
             try:
-                handler(command)
+                handler(self, command)
             except InvalidCommand:
                 if not skip_broken:
                     raise
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 6daf9601..37b48efb 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -1,7 +1,9 @@
 from cloudinit import util
 from cloudinit import net
 from cloudinit.net import cmdline
+from cloudinit.net import eni
 from .helpers import TestCase
+from .helpers import mock
 
 import base64
 import copy
@@ -9,6 +11,8 @@ import io
 import gzip
 import json
 import os
+import shutil
+import tempfile
 
 DHCP_CONTENT_1 = """
 DEVICE='eth0'
@@ -69,6 +73,70 @@ STATIC_EXPECTED_1 = {
 }
 
 
+class TestEniNetRendering(TestCase):
+
+    @mock.patch("cloudinit.net.sys_dev_path")
+    @mock.patch("cloudinit.net.sys_netdev_info")
+    @mock.patch("cloudinit.net.get_devicelist")
+    def test_generation(self, mock_get_devicelist, mock_sys_netdev_info,
+                        mock_sys_dev_path):
+        mock_get_devicelist.return_value = ['eth1000', 'lo']
+
+        dev_characteristics = {
+            'eth1000': {
+                "bridge": False,
+                "carrier": False,
+                "dormant": False,
+                "operstate": "down",
+                "address": "07-1C-C6-75-A4-BE",
+            }
+        }
+
+        def netdev_info(name, field):
+            return dev_characteristics[name][field]
+
+        mock_sys_netdev_info.side_effect = netdev_info
+
+        tmp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+
+        def sys_dev_path(devname, path=""):
+            return tmp_dir + devname + "/" + path
+
+        for dev in dev_characteristics:
+            os.makedirs(os.path.join(tmp_dir, dev))
+            with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
+                fh.write("down")
+
+        mock_sys_dev_path.side_effect = sys_dev_path
+
+        network_cfg = net.generate_fallback_config()
+        network_state = net.parse_net_config_data(network_cfg,
+                                                  skip_broken=False)
+
+        render_dir = os.path.join(tmp_dir, "render")
+        os.makedirs(render_dir)
+
+        renderer = eni.Renderer()
+        renderer.render_network_state(render_dir, network_state,
+                                      eni="interfaces",
+                                      links_prefix=None,
+                                      netrules=None)
+
+        self.assertTrue(os.path.exists(os.path.join(render_dir,
+                                                    'interfaces')))
+        with open(os.path.join(render_dir, 'interfaces')) as fh:
+            contents = fh.read()
+
+        expected = """auto lo
+iface lo inet loopback
+
+auto eth1000
+iface eth1000 inet dhcp
+"""
+        self.assertEqual(expected, contents)
+
+
 class TestNetConfigParsing(TestCase):
     simple_cfg = {
         'config': [{"type": "physical", "name": "eth0",
-- 
cgit v1.2.3


From 26ea813d293467921ab6b1e32abd2ab8fcefa3bd Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Wed, 11 May 2016 14:18:02 -0700
Subject: Fix py26 for rhel (and older versions of python)

---
 cloudinit/net/cmdline.py                           | 26 ++++---
 cloudinit/sources/DataSourceAzure.py               |  2 +-
 cloudinit/sources/helpers/openstack.py             |  8 +-
 cloudinit/util.py                                  | 25 ++++++-
 requirements.txt                                   |  8 +-
 setup.py                                           |  1 -
 test-requirements.txt                              |  1 +
 tests/unittests/helpers.py                         | 86 +++-------------------
 tests/unittests/test__init__.py                    | 18 ++---
 tests/unittests/test_cli.py                        |  7 +-
 tests/unittests/test_cs_util.py                    | 42 ++++-------
 tests/unittests/test_datasource/test_azure.py      | 12 +--
 .../unittests/test_datasource/test_azure_helper.py | 12 +--
 tests/unittests/test_datasource/test_cloudsigma.py | 26 +++++--
 tests/unittests/test_datasource/test_cloudstack.py | 11 +--
 .../unittests/test_datasource/test_configdrive.py  | 11 +--
 tests/unittests/test_datasource/test_nocloud.py    | 14 +---
 tests/unittests/test_datasource/test_smartos.py    | 21 ++++--
 tests/unittests/test_net.py                        |  7 +-
 tests/unittests/test_reporting.py                  |  4 +-
 tests/unittests/test_rh_subscription.py            | 23 ++++--
 tox.ini                                            | 36 ++++-----
 22 files changed, 163 insertions(+), 238 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
index 958c264b..21bc35d9 100644
--- a/cloudinit/net/cmdline.py
+++ b/cloudinit/net/cmdline.py
@@ -20,7 +20,6 @@ import base64
 import glob
 import gzip
 import io
-import shlex
 
 from cloudinit.net import get_devicelist
 from cloudinit.net import sys_netdev_info
@@ -34,13 +33,17 @@ def _load_shell_content(content, add_empty=False, empty_val=None):
        then add entries in to the returned dictionary for 'VAR='
        variables.  Set their value to empty_val."""
     data = {}
-    for line in shlex.split(content):
-        key, value = line.split("=", 1)
-        if not value:
-            value = empty_val
-        if add_empty or value:
-            data[key] = value
-
+    for line in util.shlex_split(content):
+        try:
+            key, value = line.split("=", 1)
+        except ValueError:
+            # Unsplittable line, skip it...
+            pass
+        else:
+            if not value:
+                value = empty_val
+            if add_empty or value:
+                data[key] = value
     return data
 
 
@@ -60,6 +63,9 @@ def _klibc_to_config_entry(content, mac_addrs=None):
     if mac_addrs is None:
         mac_addrs = {}
 
+    print("Reading content")
+    print(content)
+
     data = _load_shell_content(content)
     try:
         name = data['DEVICE']
@@ -185,7 +191,7 @@ def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
         return None
 
     if mac_addrs is None:
-        mac_addrs = {k: sys_netdev_info(k, 'address')
-                     for k in get_devicelist()}
+        mac_addrs = dict((k, sys_netdev_info(k, 'address'))
+                         for k in get_devicelist())
 
     return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 698f4cac..66c8ced8 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -421,7 +421,7 @@ def write_files(datadir, files, dirmode=None):
                     elem.text = DEF_PASSWD_REDACTION
             return ET.tostring(root)
         except Exception:
-            LOG.critical("failed to redact userpassword in {}".format(fname))
+            LOG.critical("failed to redact userpassword in %s", fname)
             return cnt
 
     if not datadir:
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 845ea971..b2acc648 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -534,13 +534,13 @@ def convert_net_json(network_json):
     config = []
     for link in links:
         subnets = []
-        cfg = {k: v for k, v in link.items()
-               if k in valid_keys['physical']}
+        cfg = dict((k, v) for k, v in link.items()
+                   if k in valid_keys['physical'])
         cfg.update({'name': link['id']})
         for network in [net for net in networks
                         if net['link'] == link['id']]:
-            subnet = {k: v for k, v in network.items()
-                      if k in valid_keys['subnet']}
+            subnet = dict((k, v) for k, v in network.items()
+                          if k in valid_keys['subnet'])
             if 'dhcp' in network['type']:
                 t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
                 subnet.update({
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 0d21e11b..7562b97a 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -37,6 +37,7 @@ import pwd
 import random
 import re
 import shutil
+import shlex
 import socket
 import stat
 import string
@@ -81,6 +82,7 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
                    ['lxc-is-container'])
 
 PROC_CMDLINE = None
+PY26 = sys.version_info[0:2] == (2, 6)
 
 
 def decode_binary(blob, encoding='utf-8'):
@@ -171,7 +173,8 @@ class ProcessExecutionError(IOError):
 
     def __init__(self, stdout=None, stderr=None,
                  exit_code=None, cmd=None,
-                 description=None, reason=None):
+                 description=None, reason=None,
+                 errno=None):
         if not cmd:
             self.cmd = '-'
         else:
@@ -202,6 +205,7 @@ class ProcessExecutionError(IOError):
         else:
             self.reason = '-'
 
+        self.errno = errno
         message = self.MESSAGE_TMPL % {
             'description': self.description,
             'cmd': self.cmd,
@@ -1147,7 +1151,14 @@ def find_devs_with(criteria=None, oformat='device',
         options.append(path)
     cmd = blk_id_cmd + options
     # See man blkid for why 2 is added
-    (out, _err) = subp(cmd, rcs=[0, 2])
+    try:
+        (out, _err) = subp(cmd, rcs=[0, 2])
+    except ProcessExecutionError as e:
+        if e.errno == errno.ENOENT:
+            # blkid not found...
+            out = ""
+        else:
+            raise
     entries = []
     for line in out.splitlines():
         line = line.strip()
@@ -1191,6 +1202,13 @@ def load_file(fname, read_cb=None, quiet=False, decode=True):
         return contents
 
 
+def shlex_split(blob):
+    if PY26 and isinstance(blob, six.text_type):
+        # Older versions don't support unicode input
+        blob = blob.encode("utf8")
+    return shlex.split(blob)
+
+
 def get_cmdline():
     if 'DEBUG_PROC_CMDLINE' in os.environ:
         return os.environ["DEBUG_PROC_CMDLINE"]
@@ -1696,7 +1714,8 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
         sp = subprocess.Popen(args, **kws)
         (out, err) = sp.communicate(data)
     except OSError as e:
-        raise ProcessExecutionError(cmd=args, reason=e)
+        raise ProcessExecutionError(cmd=args, reason=e,
+                                    errno=e.errno)
     rc = sp.returncode
     if rc not in rcs:
         raise ProcessExecutionError(stdout=out, stderr=err,
diff --git a/requirements.txt b/requirements.txt
index 19c88857..cc1dc05f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,8 +11,12 @@ PrettyTable
 oauthlib
 
 # This one is currently used only by the CloudSigma and SmartOS datasources.
-# If these datasources are removed, this is no longer needed
-pyserial
+# If these datasources are removed, this is no longer needed.
+#
+# This will not work in py2.6 so it is only optionally installed on
+# python 2.7 and later.
+#
+# pyserial
 
 # This is only needed for places where we need to support configs in a manner
 # that the built-in config parser is not sufficent (ie
diff --git a/setup.py b/setup.py
index f86727b2..6b4fc031 100755
--- a/setup.py
+++ b/setup.py
@@ -197,7 +197,6 @@ requirements = read_requires()
 if sys.version_info < (3,):
     requirements.append('cheetah')
 
-
 setuptools.setup(
     name='cloud-init',
     version=get_version(),
diff --git a/test-requirements.txt b/test-requirements.txt
index 9b3d07c5..d9c757e6 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,3 +5,4 @@ pep8==1.5.7
 pyflakes
 contextlib2
 setuptools
+unittest2
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index fb9c83a7..33f89254 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -7,12 +7,10 @@ import shutil
 import tempfile
 import unittest
 
+import mock
 import six
+import unittest2
 
-try:
-    from unittest import mock
-except ImportError:
-    import mock
 try:
     from contextlib import ExitStack
 except ImportError:
@@ -21,6 +19,9 @@ except ImportError:
 from cloudinit import helpers as ch
 from cloudinit import util
 
+# Used for skipping tests
+SkipTest = unittest2.SkipTest
+
 # Used for detecting different python versions
 PY2 = False
 PY26 = False
@@ -44,79 +45,6 @@ else:
         if _PY_MINOR == 4 and _PY_MICRO < 3:
             FIX_HTTPRETTY = True
 
-if PY26:
-    # For now add these on, taken from python 2.7 + slightly adjusted.  Drop
-    # all this once Python 2.6 is dropped as a minimum requirement.
-    class TestCase(unittest.TestCase):
-        def setUp(self):
-            super(TestCase, self).setUp()
-            self.__all_cleanups = ExitStack()
-
-        def tearDown(self):
-            self.__all_cleanups.close()
-            unittest.TestCase.tearDown(self)
-
-        def addCleanup(self, function, *args, **kws):
-            self.__all_cleanups.callback(function, *args, **kws)
-
-        def assertIs(self, expr1, expr2, msg=None):
-            if expr1 is not expr2:
-                standardMsg = '%r is not %r' % (expr1, expr2)
-                self.fail(self._formatMessage(msg, standardMsg))
-
-        def assertIn(self, member, container, msg=None):
-            if member not in container:
-                standardMsg = '%r not found in %r' % (member, container)
-                self.fail(self._formatMessage(msg, standardMsg))
-
-        def assertNotIn(self, member, container, msg=None):
-            if member in container:
-                standardMsg = '%r unexpectedly found in %r'
-                standardMsg = standardMsg % (member, container)
-                self.fail(self._formatMessage(msg, standardMsg))
-
-        def assertIsNone(self, value, msg=None):
-            if value is not None:
-                standardMsg = '%r is not None'
-                standardMsg = standardMsg % (value)
-                self.fail(self._formatMessage(msg, standardMsg))
-
-        def assertIsInstance(self, obj, cls, msg=None):
-            """Same as self.assertTrue(isinstance(obj, cls)), with a nicer
-            default message."""
-            if not isinstance(obj, cls):
-                standardMsg = '%s is not an instance of %r' % (repr(obj), cls)
-                self.fail(self._formatMessage(msg, standardMsg))
-
-        def assertDictContainsSubset(self, expected, actual, msg=None):
-            missing = []
-            mismatched = []
-            for k, v in expected.items():
-                if k not in actual:
-                    missing.append(k)
-                elif actual[k] != v:
-                    mismatched.append('%r, expected: %r, actual: %r'
-                                      % (k, v, actual[k]))
-
-            if len(missing) == 0 and len(mismatched) == 0:
-                return
-
-            standardMsg = ''
-            if missing:
-                standardMsg = 'Missing: %r' % ','.join(m for m in missing)
-            if mismatched:
-                if standardMsg:
-                    standardMsg += '; '
-                standardMsg += 'Mismatched values: %s' % ','.join(mismatched)
-
-            self.fail(self._formatMessage(msg, standardMsg))
-
-
-else:
-    class TestCase(unittest.TestCase):
-        pass
-
-
 # Makes the old path start
 # with new base instead of whatever
 # it previously had
@@ -151,6 +79,10 @@ def retarget_many_wrapper(new_base, am, old_func):
     return wrapper
 
 
+class TestCase(unittest2.TestCase):
+    pass
+
+
 class ResourceUsingTestCase(TestCase):
     def setUp(self):
         super(ResourceUsingTestCase, self).setUp()
diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py
index 153f1658..a9b35afe 100644
--- a/tests/unittests/test__init__.py
+++ b/tests/unittests/test__init__.py
@@ -1,16 +1,7 @@
 import os
 import shutil
 import tempfile
-import unittest
-
-try:
-    from unittest import mock
-except ImportError:
-    import mock
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
+import unittest2
 
 from cloudinit import handlers
 from cloudinit import helpers
@@ -18,7 +9,7 @@ from cloudinit import settings
 from cloudinit import url_helper
 from cloudinit import util
 
-from .helpers import TestCase
+from .helpers import TestCase, ExitStack, mock
 
 
 class FakeModule(handlers.Handler):
@@ -99,9 +90,10 @@ class TestWalkerHandleHandler(TestCase):
         self.assertEqual(self.data['handlercount'], 0)
 
 
-class TestHandlerHandlePart(unittest.TestCase):
+class TestHandlerHandlePart(TestCase):
 
     def setUp(self):
+        super(TestHandlerHandlePart, self).setUp()
         self.data = "fake data"
         self.ctype = "fake ctype"
         self.filename = "fake filename"
@@ -177,7 +169,7 @@ class TestHandlerHandlePart(unittest.TestCase):
             self.data, self.ctype, self.filename, self.payload)
 
 
-class TestCmdlineUrl(unittest.TestCase):
+class TestCmdlineUrl(TestCase):
     def test_invalid_content(self):
         url = "http://example.com/foo"
         key = "mykey"
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index ed863399..f8fe7c9b 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -4,12 +4,7 @@ import sys
 import six
 
 from . import helpers as test_helpers
-
-try:
-    from unittest import mock
-except ImportError:
-    import mock
-
+mock = test_helpers.mock
 
 BIN_CLOUDINIT = "bin/cloud-init"
 
diff --git a/tests/unittests/test_cs_util.py b/tests/unittests/test_cs_util.py
index d7273035..8c9ac0cd 100644
--- a/tests/unittests/test_cs_util.py
+++ b/tests/unittests/test_cs_util.py
@@ -1,20 +1,14 @@
 from __future__ import print_function
 
-import sys
-import unittest
+from . import helpers as test_helpers
 
-from cloudinit.cs_utils import Cepko
+import unittest2
 
 try:
-    skip = unittest.skip
-except AttributeError:
-    # Python 2.6.  Doesn't have to be high fidelity.
-    def skip(reason):
-        def decorator(func):
-            def wrapper(*args, **kws):
-                print(reason, file=sys.stderr)
-            return wrapper
-        return decorator
+    from cloudinit.cs_utils import Cepko
+    WILL_WORK = True
+except ImportError:
+    WILL_WORK = False
 
 
 SERVER_CONTEXT = {
@@ -32,29 +26,21 @@ SERVER_CONTEXT = {
 }
 
 
-class CepkoMock(Cepko):
-    def all(self):
-        return SERVER_CONTEXT
+if WILL_WORK:
+    class CepkoMock(Cepko):
+        def all(self):
+            return SERVER_CONTEXT
 
-    def get(self, key="", request_pattern=None):
-        return SERVER_CONTEXT['tags']
+        def get(self, key="", request_pattern=None):
+            return SERVER_CONTEXT['tags']
 
 
 # 2015-01-22 BAW: This test is completely useless because it only ever tests
 # the CepkoMock object.  Even in its original form, I don't think it ever
 # touched the underlying Cepko class methods.
-@skip('This test is completely useless')
-class CepkoResultTests(unittest.TestCase):
+class CepkoResultTests(test_helpers.TestCase):
     def setUp(self):
-        pass
-        # self.mocked = self.mocker.replace("cloudinit.cs_utils.Cepko",
-        #                     spec=CepkoMock,
-        #                     count=False,
-        #                     passthrough=False)
-        # self.mocked()
-        # self.mocker.result(CepkoMock())
-        # self.mocker.replay()
-        # self.c = Cepko()
+        raise unittest2.SkipTest('This test is completely useless')
 
     def test_getitem(self):
         result = self.c.all()
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 444e2799..aafe1bc2 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -1,16 +1,8 @@
 from cloudinit import helpers
 from cloudinit.util import b64e, decode_binary, load_file
 from cloudinit.sources import DataSourceAzure
-from ..helpers import TestCase, populate_dir
 
-try:
-    from unittest import mock
-except ImportError:
-    import mock
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
+from ..helpers import TestCase, populate_dir, mock, ExitStack, PY26, SkipTest
 
 import crypt
 import os
@@ -83,6 +75,8 @@ class TestAzureDataSource(TestCase):
 
     def setUp(self):
         super(TestAzureDataSource, self).setUp()
+        if PY26:
+            raise SkipTest("Does not work on python 2.6")
         self.tmp = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, self.tmp)
 
diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
index 1134199b..736f4463 100644
--- a/tests/unittests/test_datasource/test_azure_helper.py
+++ b/tests/unittests/test_datasource/test_azure_helper.py
@@ -1,17 +1,11 @@
 import os
 
 from cloudinit.sources.helpers import azure as azure_helper
-from ..helpers import TestCase
 
-try:
-    from unittest import mock
-except ImportError:
-    import mock
+from ..helpers import ExitStack
+from ..helpers import TestCase
 
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
+from ..helpers import mock
 
 
 GOAL_STATE_TEMPLATE = """\
diff --git a/tests/unittests/test_datasource/test_cloudsigma.py b/tests/unittests/test_datasource/test_cloudsigma.py
index 772d189a..11968796 100644
--- a/tests/unittests/test_datasource/test_cloudsigma.py
+++ b/tests/unittests/test_datasource/test_cloudsigma.py
@@ -1,11 +1,18 @@
 # coding: utf-8
+
 import copy
 
-from cloudinit.cs_utils import Cepko
-from cloudinit.sources import DataSourceCloudSigma
+try:
+    # Serial does not work on py2.6 (anymore)
+    import pyserial
+    from cloudinit.cs_utils import Cepko
+    from cloudinit.sources import DataSourceCloudSigma
+    WILL_WORK = True
+except ImportError:
+    WILL_WORK = False
 
 from .. import helpers as test_helpers
-
+from ..helpers import SkipTest
 
 SERVER_CONTEXT = {
     "cpu": 1000,
@@ -29,17 +36,20 @@ SERVER_CONTEXT = {
 }
 
 
-class CepkoMock(Cepko):
-    def __init__(self, mocked_context):
-        self.result = mocked_context
+if WILL_WORK:
+    class CepkoMock(Cepko):
+        def __init__(self, mocked_context):
+            self.result = mocked_context
 
-    def all(self):
-        return self
+        def all(self):
+            return self
 
 
 class DataSourceCloudSigmaTest(test_helpers.TestCase):
     def setUp(self):
         super(DataSourceCloudSigmaTest, self).setUp()
+        if not WILL_WORK:
+            raise SkipTest("Datasource testing not supported")
         self.datasource = DataSourceCloudSigma.DataSourceCloudSigma("", "", "")
         self.datasource.is_running_in_cloudsigma = lambda: True
         self.datasource.cepko = CepkoMock(SERVER_CONTEXT)
diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
index 656d80d1..4d6b47b4 100644
--- a/tests/unittests/test_datasource/test_cloudstack.py
+++ b/tests/unittests/test_datasource/test_cloudstack.py
@@ -1,15 +1,6 @@
 from cloudinit import helpers
 from cloudinit.sources.DataSourceCloudStack import DataSourceCloudStack
-from ..helpers import TestCase
-
-try:
-    from unittest import mock
-except ImportError:
-    import mock
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
+from ..helpers import TestCase, mock, ExitStack
 
 
 class TestCloudStackPasswordFetching(TestCase):
diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
index 8beaf95e..14cc8266 100644
--- a/tests/unittests/test_datasource/test_configdrive.py
+++ b/tests/unittests/test_datasource/test_configdrive.py
@@ -5,22 +5,13 @@ import shutil
 import six
 import tempfile
 
-try:
-    from unittest import mock
-except ImportError:
-    import mock
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
-
 from cloudinit import helpers
 from cloudinit import settings
 from cloudinit.sources import DataSourceConfigDrive as ds
 from cloudinit.sources.helpers import openstack
 from cloudinit import util
 
-from ..helpers import TestCase
+from ..helpers import TestCase, ExitStack, mock
 
 
 PUBKEY = u'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n'
diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py
index 2d5fc37c..a92dd3b3 100644
--- a/tests/unittests/test_datasource/test_nocloud.py
+++ b/tests/unittests/test_datasource/test_nocloud.py
@@ -1,22 +1,12 @@
 from cloudinit import helpers
 from cloudinit.sources import DataSourceNoCloud
 from cloudinit import util
-from ..helpers import TestCase, populate_dir
+from ..helpers import TestCase, populate_dir, mock, ExitStack
 
 import os
 import yaml
 import shutil
 import tempfile
-import unittest
-
-try:
-    from unittest import mock
-except ImportError:
-    import mock
-try:
-    from contextlib import ExitStack
-except ImportError:
-    from contextlib2 import ExitStack
 
 
 class TestNoCloudDataSource(TestCase):
@@ -139,7 +129,7 @@ class TestNoCloudDataSource(TestCase):
         self.assertTrue(ret)
 
 
-class TestParseCommandLineData(unittest.TestCase):
+class TestParseCommandLineData(TestCase):
 
     def test_parse_cmdline_data_valid(self):
         ds_id = "ds=nocloud"
diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py
index 5c49966a..6b628276 100644
--- a/tests/unittests/test_datasource/test_smartos.py
+++ b/tests/unittests/test_datasource/test_smartos.py
@@ -33,19 +33,21 @@ import tempfile
 import uuid
 from binascii import crc32
 
-import serial
+try:
+    # Serial does not work on py2.6 (anymore)
+    import serial
+    from cloudinit.sources import DataSourceSmartOS
+    WILL_WORK = True
+except ImportError:
+    WILL_WORK = False
+
 import six
 
 from cloudinit import helpers as c_helpers
-from cloudinit.sources import DataSourceSmartOS
 from cloudinit.util import b64e
 
 from .. import helpers
-
-try:
-    from unittest import mock
-except ImportError:
-    import mock
+from ..helpers import mock, SkipTest
 
 MOCK_RETURNS = {
     'hostname': 'test-host',
@@ -79,7 +81,8 @@ def get_mock_client(mockdata):
 class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):
     def setUp(self):
         super(TestSmartOSDataSource, self).setUp()
-
+        if not WILL_WORK:
+            raise SkipTest("This test will not work")
         self.tmp = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, self.tmp)
         self.legacy_user_d = tempfile.mkdtemp()
@@ -445,6 +448,8 @@ class TestJoyentMetadataClient(helpers.FilesystemMockingTestCase):
 
     def setUp(self):
         super(TestJoyentMetadataClient, self).setUp()
+        if not WILL_WORK:
+            raise SkipTest("This test will not work")
         self.serial = mock.MagicMock(spec=serial.Serial)
         self.request_id = 0xabcdef12
         self.metadata_value = 'value'
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 005957de..ed2c6d0f 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -78,8 +78,9 @@ class TestEniNetRendering(TestCase):
     @mock.patch("cloudinit.net.sys_dev_path")
     @mock.patch("cloudinit.net.sys_netdev_info")
     @mock.patch("cloudinit.net.get_devicelist")
-    def test_generation(self, mock_get_devicelist, mock_sys_netdev_info,
-                        mock_sys_dev_path):
+    def test_default_generation(self, mock_get_devicelist,
+                                mock_sys_netdev_info,
+                                mock_sys_dev_path):
         mock_get_devicelist.return_value = ['eth1000', 'lo']
 
         dev_characteristics = {
@@ -138,7 +139,7 @@ iface eth1000 inet dhcp
         self.assertEqual(expected.lstrip(), contents.lstrip())
 
 
-class TestNetConfigParsing(TestCase):
+class TestCmdlineConfigParsing(TestCase):
     simple_cfg = {
         'config': [{"type": "physical", "name": "eth0",
                     "mac_address": "c0:d6:9f:2c:e8:80",
diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py
index 32356ef9..493bb261 100644
--- a/tests/unittests/test_reporting.py
+++ b/tests/unittests/test_reporting.py
@@ -7,7 +7,9 @@ from cloudinit import reporting
 from cloudinit.reporting import handlers
 from cloudinit.reporting import events
 
-from .helpers import (mock, TestCase)
+import mock
+
+from .helpers import TestCase
 
 
 def _fake_registry():
diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py
index 8c586ad7..13045aaf 100644
--- a/tests/unittests/test_rh_subscription.py
+++ b/tests/unittests/test_rh_subscription.py
@@ -1,11 +1,24 @@
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+
 from cloudinit import util
 from cloudinit.config import cc_rh_subscription
-import logging
-import mock
-import unittest
+
+from .helpers import TestCase, mock
 
 
-class GoodTests(unittest.TestCase):
+class GoodTests(TestCase):
     def setUp(self):
         super(GoodTests, self).setUp()
         self.name = "cc_rh_subscription"
@@ -92,7 +105,7 @@ class GoodTests(unittest.TestCase):
         self.assertEqual(self.SM._sub_man_cli.call_count, 9)
 
 
-class TestBadInput(unittest.TestCase):
+class TestBadInput(TestCase):
     name = "cc_rh_subscription"
     cloud_init = None
     log = logging.getLogger("bad_tests")
diff --git a/tox.ini b/tox.ini
index bd7c27dd..3210b0ee 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,32 +1,32 @@
 [tox]
-envlist = py27,py3,pyflakes
+envlist = py27,py26,py3,pyflakes
 recreate = True
+usedevelop = True
 
 [testenv]
 commands = python -m nose {posargs:tests}
 deps = -r{toxinidir}/test-requirements.txt
-    -r{toxinidir}/requirements.txt
-
-[testenv:py3]
-basepython = python3
+       -r{toxinidir}/requirements.txt
+setenv =
+    LC_ALL = en_US.utf-8
 
 [testenv:pyflakes]
 basepython = python3
 commands = {envpython} -m pyflakes {posargs:cloudinit/ tests/ tools/}
-   {envpython} -m pep8 {posargs:cloudinit/ tests/ tools/}
+           {envpython} -m pep8 {posargs:cloudinit/ tests/ tools/}
 
-# https://github.com/gabrielfalcao/HTTPretty/issues/223
-setenv =
-    LC_ALL = en_US.utf-8
+[testenv:py3]
+basepython = python3
+deps = -r{toxinidir}/test-requirements.txt
+       -r{toxinidir}/requirements.txt
+       pyserial
+
+[testenv:py27]
+deps = -r{toxinidir}/test-requirements.txt
+       -r{toxinidir}/requirements.txt
+       pyserial
 
 [testenv:py26]
 commands = nosetests {posargs:tests}
-deps =
-     contextlib2
-     httpretty>=0.7.1
-     mock
-     nose
-     pep8==1.5.7
-     pyflakes
-setenv =
-    LC_ALL = C
+deps = -r{toxinidir}/test-requirements.txt
+       -r{toxinidir}/requirements.txt
-- 
cgit v1.2.3


From e885f694c9951101b57ee182bebc000e398da563 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Wed, 11 May 2016 14:19:33 -0700
Subject: Remove stray prints leftover

---
 cloudinit/net/cmdline.py | 3 ---
 1 file changed, 3 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
index 21bc35d9..f5712533 100644
--- a/cloudinit/net/cmdline.py
+++ b/cloudinit/net/cmdline.py
@@ -63,9 +63,6 @@ def _klibc_to_config_entry(content, mac_addrs=None):
     if mac_addrs is None:
         mac_addrs = {}
 
-    print("Reading content")
-    print(content)
-
     data = _load_shell_content(content)
     try:
         name = data['DEVICE']
-- 
cgit v1.2.3


From 12d7ee2cb6589b866ab26b508b15c65326481d6c Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Wed, 11 May 2016 16:47:50 -0700
Subject: Use a fake serial module that will allow tests to contine

Instead of aborting all serial using tests instead just
create a serial module in cloudinit that will create a fake
and broken serial class when pyserial is not actually installed.

This allows for using the datasource and tests that exist in
a more functional and tested manner (even when pyserial is not
found).
---
 cloudinit/cs_utils.py                              |  3 +-
 cloudinit/serial.py                                | 50 ++++++++++++++++++++++
 cloudinit/sources/DataSourceSmartOS.py             |  4 +-
 tests/unittests/test_cs_util.py                    | 21 +++------
 tests/unittests/test_datasource/test_cloudsigma.py | 23 +++-------
 tests/unittests/test_datasource/test_smartos.py    | 15 ++-----
 6 files changed, 71 insertions(+), 45 deletions(-)
 create mode 100644 cloudinit/serial.py

(limited to 'cloudinit')

diff --git a/cloudinit/cs_utils.py b/cloudinit/cs_utils.py
index 83ac1a0e..412431f2 100644
--- a/cloudinit/cs_utils.py
+++ b/cloudinit/cs_utils.py
@@ -33,7 +33,8 @@ API Docs: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html
 import json
 import platform
 
-import serial
+from cloudinit import serial
+
 
 # these high timeouts are necessary as read may read a lot of data.
 READ_TIMEOUT = 60
diff --git a/cloudinit/serial.py b/cloudinit/serial.py
new file mode 100644
index 00000000..af45c13e
--- /dev/null
+++ b/cloudinit/serial.py
@@ -0,0 +1,50 @@
+# vi: ts=4 expandtab
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from __future__ import absolute_import
+
+try:
+    from serial import Serial
+except ImportError:
+    # For older versions of python (ie 2.6) pyserial may not exist and/or
+    # work and/or be installed, so make a dummy/fake serial that blows up
+    # when used...
+    class Serial(object):
+        def __init__(self, *args, **kwargs):
+            pass
+
+        @staticmethod
+        def isOpen():
+            return False
+
+        @staticmethod
+        def write(data):
+            raise IOError("Unable to perform serial `write` operation,"
+                          " pyserial not installed.")
+
+        @staticmethod
+        def readline():
+            raise IOError("Unable to perform serial `readline` operation,"
+                          " pyserial not installed.")
+
+        @staticmethod
+        def flush():
+            raise IOError("Unable to perform serial `flush` operation,"
+                          " pyserial not installed.")
+
+        @staticmethod
+        def read(size=1):
+            raise IOError("Unable to perform serial `read` operation,"
+                          " pyserial not installed.")
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index 6cbd8dfa..c7641eb3 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -40,13 +40,11 @@ import re
 import socket
 import stat
 
-import serial
-
 from cloudinit import log as logging
+from cloudinit import serial
 from cloudinit import sources
 from cloudinit import util
 
-
 LOG = logging.getLogger(__name__)
 
 SMARTOS_ATTRIB_MAP = {
diff --git a/tests/unittests/test_cs_util.py b/tests/unittests/test_cs_util.py
index 8c9ac0cd..56c9ce9e 100644
--- a/tests/unittests/test_cs_util.py
+++ b/tests/unittests/test_cs_util.py
@@ -2,13 +2,7 @@ from __future__ import print_function
 
 from . import helpers as test_helpers
 
-import unittest2
-
-try:
-    from cloudinit.cs_utils import Cepko
-    WILL_WORK = True
-except ImportError:
-    WILL_WORK = False
+from cloudinit.cs_utils import Cepko
 
 
 SERVER_CONTEXT = {
@@ -26,13 +20,12 @@ SERVER_CONTEXT = {
 }
 
 
-if WILL_WORK:
-    class CepkoMock(Cepko):
-        def all(self):
-            return SERVER_CONTEXT
+class CepkoMock(Cepko):
+    def all(self):
+        return SERVER_CONTEXT
 
-        def get(self, key="", request_pattern=None):
-            return SERVER_CONTEXT['tags']
+    def get(self, key="", request_pattern=None):
+        return SERVER_CONTEXT['tags']
 
 
 # 2015-01-22 BAW: This test is completely useless because it only ever tests
@@ -40,7 +33,7 @@ if WILL_WORK:
 # touched the underlying Cepko class methods.
 class CepkoResultTests(test_helpers.TestCase):
     def setUp(self):
-        raise unittest2.SkipTest('This test is completely useless')
+        raise test_helpers.SkipTest('This test is completely useless')
 
     def test_getitem(self):
         result = self.c.all()
diff --git a/tests/unittests/test_datasource/test_cloudsigma.py b/tests/unittests/test_datasource/test_cloudsigma.py
index 11968796..7950fc52 100644
--- a/tests/unittests/test_datasource/test_cloudsigma.py
+++ b/tests/unittests/test_datasource/test_cloudsigma.py
@@ -2,14 +2,8 @@
 
 import copy
 
-try:
-    # Serial does not work on py2.6 (anymore)
-    import pyserial
-    from cloudinit.cs_utils import Cepko
-    from cloudinit.sources import DataSourceCloudSigma
-    WILL_WORK = True
-except ImportError:
-    WILL_WORK = False
+from cloudinit.cs_utils import Cepko
+from cloudinit.sources import DataSourceCloudSigma
 
 from .. import helpers as test_helpers
 from ..helpers import SkipTest
@@ -36,20 +30,17 @@ SERVER_CONTEXT = {
 }
 
 
-if WILL_WORK:
-    class CepkoMock(Cepko):
-        def __init__(self, mocked_context):
-            self.result = mocked_context
+class CepkoMock(Cepko):
+    def __init__(self, mocked_context):
+        self.result = mocked_context
 
-        def all(self):
-            return self
+    def all(self):
+        return self
 
 
 class DataSourceCloudSigmaTest(test_helpers.TestCase):
     def setUp(self):
         super(DataSourceCloudSigmaTest, self).setUp()
-        if not WILL_WORK:
-            raise SkipTest("Datasource testing not supported")
         self.datasource = DataSourceCloudSigma.DataSourceCloudSigma("", "", "")
         self.datasource.is_running_in_cloudsigma = lambda: True
         self.datasource.cepko = CepkoMock(SERVER_CONTEXT)
diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py
index 6b628276..f536ef4f 100644
--- a/tests/unittests/test_datasource/test_smartos.py
+++ b/tests/unittests/test_datasource/test_smartos.py
@@ -33,13 +33,8 @@ import tempfile
 import uuid
 from binascii import crc32
 
-try:
-    # Serial does not work on py2.6 (anymore)
-    import serial
-    from cloudinit.sources import DataSourceSmartOS
-    WILL_WORK = True
-except ImportError:
-    WILL_WORK = False
+from cloudinit import serial
+from cloudinit.sources import DataSourceSmartOS
 
 import six
 
@@ -81,8 +76,7 @@ def get_mock_client(mockdata):
 class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):
     def setUp(self):
         super(TestSmartOSDataSource, self).setUp()
-        if not WILL_WORK:
-            raise SkipTest("This test will not work")
+
         self.tmp = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, self.tmp)
         self.legacy_user_d = tempfile.mkdtemp()
@@ -448,8 +442,7 @@ class TestJoyentMetadataClient(helpers.FilesystemMockingTestCase):
 
     def setUp(self):
         super(TestJoyentMetadataClient, self).setUp()
-        if not WILL_WORK:
-            raise SkipTest("This test will not work")
+
         self.serial = mock.MagicMock(spec=serial.Serial)
         self.request_id = 0xabcdef12
         self.metadata_value = 'value'
-- 
cgit v1.2.3


From abb3c00fadefea8056c300faf141260e124a5064 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Tue, 17 May 2016 17:29:59 -0700
Subject: Don't expose anything but 'render_network_state'

This should be the visible api of a network renderer
as anything more granular varies between the different
render types and will not apply to those renderers.
---
 cloudinit/net/eni.py | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index b427012e..b1bdac24 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -243,7 +243,7 @@ def _parse_deb_config(path):
 class Renderer(object):
     """Renders network information in a /etc/network/interfaces format."""
 
-    def render_persistent_net(self, network_state):
+    def _render_persistent_net(self, network_state):
         """Given state, emit udev rules to map mac to ifname."""
         content = ""
         interfaces = network_state.get('interfaces')
@@ -256,7 +256,7 @@ class Renderer(object):
 
         return content
 
-    def render_route(self, route, indent=""):
+    def _render_route(self, route, indent=""):
         """ When rendering routes for an iface, in some cases applying a route
         may result in the route command returning non-zero which produces
         some confusing output for users manually using ifup/ifdown[1].  To
@@ -300,7 +300,7 @@ class Renderer(object):
 
         return content
 
-    def render_interfaces(self, network_state):
+    def _render_interfaces(self, network_state):
         ''' Given state, emit etc/network/interfaces content '''
 
         content = ""
@@ -352,7 +352,7 @@ class Renderer(object):
                 content += _iface_add_attrs(iface)
 
         for route in network_state.get('routes'):
-            content += self.render_route(route)
+            content += self._render_route(route)
 
         # global replacements until v2 format
         content = content.replace('mac_address', 'hwaddress')
@@ -366,19 +366,19 @@ class Renderer(object):
         fpeni = os.path.sep.join((target, eni,))
         util.ensure_dir(os.path.dirname(fpeni))
         with open(fpeni, 'w+') as f:
-            f.write(self.render_interfaces(network_state))
+            f.write(self._render_interfaces(network_state))
 
         if netrules:
             netrules = os.path.sep.join((target, netrules,))
             util.ensure_dir(os.path.dirname(netrules))
             with open(netrules, 'w+') as f:
-                f.write(self.render_persistent_net(network_state))
+                f.write(self._render_persistent_net(network_state))
 
         if links_prefix:
-            self.render_systemd_links(target, network_state, links_prefix)
+            self._render_systemd_links(target, network_state, links_prefix)
 
-    def render_systemd_links(self, target, network_state,
-                             links_prefix=LINKS_FNAME_PREFIX):
+    def _render_systemd_links(self, target, network_state,
+                              links_prefix=LINKS_FNAME_PREFIX):
         fp_prefix = os.path.sep.join((target, links_prefix))
         for f in glob.glob(fp_prefix + "*"):
             os.unlink(f)
-- 
cgit v1.2.3


From 880d9fc2f9c62abf19b1506595aa81e5417dea45 Mon Sep 17 00:00:00 2001
From: Joshua Harlow <harlowja@gmail.com>
Date: Thu, 19 May 2016 14:13:07 -0700
Subject: Adjust net module to be more isolated

This allows it to be used outside of cloudinit
more easily in the future.
---
 cloudinit/net/__init__.py              | 108 ++++++++++++++++++---------------
 cloudinit/net/cmdline.py               |  15 ++++-
 cloudinit/net/eni.py                   |  35 +++++------
 cloudinit/net/network_state.py         |  38 +++++++++---
 cloudinit/sources/helpers/openstack.py |   4 ++
 cloudinit/util.py                      |   9 ---
 tests/unittests/test_net.py            |   7 ++-
 7 files changed, 129 insertions(+), 87 deletions(-)

(limited to 'cloudinit')

diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 0202cbd8..07e7307e 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -17,44 +17,81 @@
 #   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
 
 import errno
+import logging
 import os
 
-
-from cloudinit import log as logging
-from cloudinit.net import network_state
-from cloudinit import util
-
+import six
+import yaml
 
 LOG = logging.getLogger(__name__)
 SYS_CLASS_NET = "/sys/class/net/"
 LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-"
 DEFAULT_PRIMARY_INTERFACE = 'eth0'
 
+# NOTE(harlowja): some of these are similar to what is in cloudinit main
+# source or utils tree/module but the reason that is done is so that this
+# whole module can be easily extracted and placed into other
+# code-bases (curtin for example).
 
-def sys_dev_path(devname, path=""):
-    return SYS_CLASS_NET + devname + "/" + path
+def write_file(path, content):
+    """Simple writing a file helper."""
+    base_path = os.path.dirname(path)
+    if not os.path.isdir(base_path):
+        os.makedirs(base_path)
+    with open(path, "wb+") as fh:
+        if isinstance(content, six.text_type):
+            content = content.encode("utf8")
+        fh.write(content)
 
 
-def read_sys_net(devname, path, translate=None, enoent=None, keyerror=None):
+def read_file(path, decode='utf8', enoent=None):
     try:
-        contents = ""
-        with open(sys_dev_path(devname, path), "r") as fp:
-            contents = fp.read().strip()
-        if translate is None:
-            return contents
-
-        try:
-            return translate.get(contents)
-        except KeyError:
-            LOG.debug("found unexpected value '%s' in '%s/%s'", contents,
-                      devname, path)
-            if keyerror is not None:
-                return keyerror
-            raise
+        with open(path, "rb") as fh:
+            contents = fh.load()
     except OSError as e:
         if e.errno == errno.ENOENT and enoent is not None:
             return enoent
         raise
+    if decode:
+        return contents.decode(decode)
+    return contents
+
+
+def dump_yaml(obj):
+    return yaml.safe_dump(obj,
+                          line_break="\n",
+                          indent=4,
+                          explicit_start=True,
+                          explicit_end=True,
+                          default_flow_style=False)
+
+
+def read_yaml_file(path):
+    val = yaml.safe_load(read_file(path))
+    if not isinstance(val, dict):
+        gotten_type_name = type(val).__name__
+        raise TypeError("Expected dict to be loaded from %s, got"
+                        " '%s' instead" % (path, gotten_type_name))
+    return val
+
+
+def sys_dev_path(devname, path=""):
+    return SYS_CLASS_NET + devname + "/" + path
+
+
+def read_sys_net(devname, path, translate=None, enoent=None, keyerror=None):
+    contents = read_file(sys_dev_path(devname, path), enoent=enoent)
+    contents = contents.strip()
+    if translate is None:
+        return contents
+    try:
+        return translate.get(contents)
+    except KeyError:
+        LOG.debug("found unexpected value '%s' in '%s/%s'", contents,
+                  devname, path)
+        if keyerror is not None:
+            return keyerror
+        raise
 
 
 def is_up(devname):
@@ -107,31 +144,6 @@ class ParserError(Exception):
     """Raised when parser has issue parsing the interfaces file."""
 
 
-def parse_net_config_data(net_config, skip_broken=True):
-    """Parses the config, returns NetworkState object
-
-    :param net_config: curtin network config dict
-    """
-    state = None
-    if 'version' in net_config and 'config' in net_config:
-        ns = network_state.NetworkState(version=net_config.get('version'),
-                                        config=net_config.get('config'))
-        ns.parse_config(skip_broken=skip_broken)
-        state = ns.network_state
-    return state
-
-
-def parse_net_config(path, skip_broken=True):
-    """Parses a curtin network configuration file and
-       return network state"""
-    ns = None
-    net_config = util.read_conf(path)
-    if 'network' in net_config:
-        ns = parse_net_config_data(net_config.get('network'),
-                                   skip_broken=skip_broken)
-    return ns
-
-
 def is_disabled_cfg(cfg):
     if not cfg or not isinstance(cfg, dict):
         return False
@@ -146,7 +158,7 @@ def sys_netdev_info(name, field):
     fname = os.path.join(SYS_CLASS_NET, name, field)
     if not os.path.exists(fname):
         raise OSError("%s: could not find sysfs entry: %s" % (name, fname))
-    data = util.load_file(fname)
+    data = read_file(fname)
     if data[-1] == '\n':
         data = data[:-1]
     return data
diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
index f5712533..b85d4b0a 100644
--- a/cloudinit/net/cmdline.py
+++ b/cloudinit/net/cmdline.py
@@ -20,12 +20,25 @@ import base64
 import glob
 import gzip
 import io
+import shlex
+import sys
+
+import six
 
 from cloudinit.net import get_devicelist
 from cloudinit.net import sys_netdev_info
 
 from cloudinit import util
 
+PY26 = sys.version_info[0:2] == (2, 6)
+
+
+def _shlex_split(blob):
+    if PY26 and isinstance(blob, six.text_type):
+        # Older versions don't support unicode input
+        blob = blob.encode("utf8")
+    return shlex.split(blob)
+
 
 def _load_shell_content(content, add_empty=False, empty_val=None):
     """Given shell like syntax (key=value\nkey2=value2\n) in content
@@ -33,7 +46,7 @@ def _load_shell_content(content, add_empty=False, empty_val=None):
        then add entries in to the returned dictionary for 'VAR='
        variables.  Set their value to empty_val."""
     data = {}
-    for line in util.shlex_split(content):
+    for line in _shlex_split(content):
         try:
             key, value = line.split("=", 1)
         except ValueError:
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index b1bdac24..adb31c22 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -16,10 +16,11 @@ import glob
 import os
 import re
 
+from cloudinit import net
+
 from cloudinit.net import LINKS_FNAME_PREFIX
 from cloudinit.net import ParserError
 from cloudinit.net.udev import generate_udev_rule
-from cloudinit import util
 
 
 NET_CONFIG_COMMANDS = [
@@ -363,16 +364,13 @@ class Renderer(object):
         links_prefix=LINKS_FNAME_PREFIX,
         netrules='etc/udev/rules.d/70-persistent-net.rules'):
 
-        fpeni = os.path.sep.join((target, eni,))
-        util.ensure_dir(os.path.dirname(fpeni))
-        with open(fpeni, 'w+') as f:
-            f.write(self._render_interfaces(network_state))
+        fpeni = os.path.join(target, eni)
+        net.write_file(fpeni, self._render_interfaces(network_state))
 
         if netrules:
-            netrules = os.path.sep.join((target, netrules,))
-            util.ensure_dir(os.path.dirname(netrules))
-            with open(netrules, 'w+') as f:
-                f.write(self._render_persistent_net(network_state))
+            netrules = os.path.join(target, netrules)
+            net.write_file(netrules,
+                           self._render_persistent_net(network_state))
 
         if links_prefix:
             self._render_systemd_links(target, network_state, links_prefix)
@@ -382,18 +380,17 @@ class Renderer(object):
         fp_prefix = os.path.sep.join((target, links_prefix))
         for f in glob.glob(fp_prefix + "*"):
             os.unlink(f)
-
         interfaces = network_state.get('interfaces')
         for iface in interfaces.values():
             if (iface['type'] == 'physical' and 'name' in iface and
                     iface.get('mac_address')):
                 fname = fp_prefix + iface['name'] + ".link"
-                with open(fname, "w") as fp:
-                    fp.write("\n".join([
-                        "[Match]",
-                        "MACAddress=" + iface['mac_address'],
-                        "",
-                        "[Link]",
-                        "Name=" + iface['name'],
-                        ""
-                    ]))
+                content = "\n".join([
+                    "[Match]",
+                    "MACAddress=" + iface['mac_address'],
+                    "",
+                    "[Link]",
+                    "Name=" + iface['name'],
+                    ""
+                ])
+                net.write_file(fname, content)
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 2feffa71..c5aeadb5 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -16,12 +16,11 @@
 #   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
 
 import copy
+import logging
 
 import six
 
-from cloudinit import log as logging
-from cloudinit import util
-from cloudinit.util import yaml_dumps as dump_config
+from cloudinit import net
 
 LOG = logging.getLogger(__name__)
 
@@ -31,9 +30,34 @@ NETWORK_STATE_REQUIRED_KEYS = {
 }
 
 
+def parse_net_config_data(net_config, skip_broken=True):
+    """Parses the config, returns NetworkState object
+
+    :param net_config: curtin network config dict
+    """
+    state = None
+    if 'version' in net_config and 'config' in net_config:
+        ns = NetworkState(version=net_config.get('version'),
+                          config=net_config.get('config'))
+        ns.parse_config(skip_broken=skip_broken)
+        state = ns.network_state
+    return state
+
+
+def parse_net_config(path, skip_broken=True):
+    """Parses a curtin network configuration file and
+       return network state"""
+    ns = None
+    net_config = net.read_yaml_file(path)
+    if 'network' in net_config:
+        ns = parse_net_config_data(net_config.get('network'),
+                                   skip_broken=skip_broken)
+    return ns
+
+
 def from_state_file(state_file):
     network_state = None
-    state = util.read_conf(state_file)
+    state = net.read_yaml_file(state_file)
     network_state = NetworkState()
     network_state.load(state)
     return network_state
@@ -111,7 +135,7 @@ class NetworkState(object):
             'config': self.config,
             'network_state': self.network_state,
         }
-        return dump_config(state)
+        return net.dump_yaml(state)
 
     def load(self, state):
         if 'version' not in state:
@@ -121,7 +145,7 @@ class NetworkState(object):
         required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]
         missing_keys = diff_keys(required_keys, state)
         if missing_keys:
-            msg = 'Invalid state, missing keys: %s'.format(missing_keys)
+            msg = 'Invalid state, missing keys: %s' % (missing_keys)
             LOG.error(msg)
             raise ValueError(msg)
 
@@ -130,7 +154,7 @@ class NetworkState(object):
             setattr(self, key, state[key])
 
     def dump_network_state(self):
-        return dump_config(self.network_state)
+        return net.dump_yaml(self.network_state)
 
     def parse_config(self, skip_broken=True):
         # rebuild network state
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index b2acc648..f85fd864 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -551,6 +551,10 @@ def convert_net_json(network_json):
                     'type': 'static',
                     'address': network.get('ip_address'),
                 })
+                if network['type'] == 'ipv6':
+                    subnet['ipv6'] = True
+                else:
+                    subnet['ipv4'] = True
             subnets.append(subnet)
         cfg.update({'subnets': subnets})
         if link['type'] in ['ethernet', 'vif', 'ovs', 'phy']:
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 7562b97a..2bec476e 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -37,7 +37,6 @@ import pwd
 import random
 import re
 import shutil
-import shlex
 import socket
 import stat
 import string
@@ -82,7 +81,6 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
                    ['lxc-is-container'])
 
 PROC_CMDLINE = None
-PY26 = sys.version_info[0:2] == (2, 6)
 
 
 def decode_binary(blob, encoding='utf-8'):
@@ -1202,13 +1200,6 @@ def load_file(fname, read_cb=None, quiet=False, decode=True):
         return contents
 
 
-def shlex_split(blob):
-    if PY26 and isinstance(blob, six.text_type):
-        # Older versions don't support unicode input
-        blob = blob.encode("utf8")
-    return shlex.split(blob)
-
-
 def get_cmdline():
     if 'DEBUG_PROC_CMDLINE' in os.environ:
         return os.environ["DEBUG_PROC_CMDLINE"]
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index ed2c6d0f..75c433f6 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -2,6 +2,7 @@ from cloudinit import util
 from cloudinit import net
 from cloudinit.net import cmdline
 from cloudinit.net import eni
+from cloudinit.net import network_state
 from .helpers import TestCase
 from .helpers import mock
 
@@ -112,14 +113,14 @@ class TestEniNetRendering(TestCase):
         mock_sys_dev_path.side_effect = sys_dev_path
 
         network_cfg = net.generate_fallback_config()
-        network_state = net.parse_net_config_data(network_cfg,
-                                                  skip_broken=False)
+        ns = network_state.parse_net_config_data(network_cfg,
+                                                 skip_broken=False)
 
         render_dir = os.path.join(tmp_dir, "render")
         os.makedirs(render_dir)
 
         renderer = eni.Renderer()
-        renderer.render_network_state(render_dir, network_state,
+        renderer.render_network_state(render_dir, ns,
                                       eni="interfaces",
                                       links_prefix=None,
                                       netrules=None)
-- 
cgit v1.2.3