summaryrefslogtreecommitdiff
path: root/cloudinit/net/network_state.py
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/net/network_state.py')
-rw-r--r--cloudinit/net/network_state.py403
1 files changed, 403 insertions, 0 deletions
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
new file mode 100644
index 00000000..df04c526
--- /dev/null
+++ b/cloudinit/net/network_state.py
@@ -0,0 +1,403 @@
+# Copyright (C) 2013-2014 Canonical Ltd.
+#
+# Author: Ryan Harper <ryan.harper@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/>.
+
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.util import yaml_dumps as dump_config
+
+LOG = logging.getLogger(__name__)
+
+NETWORK_STATE_VERSION = 1
+NETWORK_STATE_REQUIRED_KEYS = {
+ 1: ['version', 'config', 'network_state'],
+}
+
+
+def from_state_file(state_file):
+ network_state = None
+ state = util.read_conf(state_file)
+ network_state = NetworkState()
+ network_state.load(state)
+
+ return network_state
+
+
+class NetworkState:
+ def __init__(self, version=NETWORK_STATE_VERSION, config=None):
+ self.version = version
+ self.config = config
+ self.network_state = {
+ 'interfaces': {},
+ 'routes': [],
+ 'dns': {
+ 'nameservers': [],
+ '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 = {
+ 'version': self.version,
+ 'config': self.config,
+ 'network_state': self.network_state,
+ }
+ return dump_config(state)
+
+ def load(self, state):
+ if 'version' not in state:
+ LOG.error('Invalid state, missing version field')
+ 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)
+ LOG.error(msg)
+ raise Exception(msg)
+
+ # 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)
+
+ def parse_config(self):
+ # 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)
+
+ def handle_physical(self, command):
+ '''
+ command = {
+ 'type': 'physical',
+ 'mac_address': 'c0:d6:9f:2c:e8:80',
+ 'name': 'eth0',
+ 'subnets': [
+ {'type': 'dhcp4'}
+ ]
+ }
+ '''
+ 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'], {})
+ for param, val in command.get('params', {}).items():
+ iface.update({param: val})
+ iface.update({
+ 'name': command.get('name'),
+ 'type': command.get('type'),
+ 'mac_address': command.get('mac_address'),
+ 'inet': 'inet',
+ 'mode': 'manual',
+ 'mtu': command.get('mtu'),
+ 'address': None,
+ 'gateway': None,
+ 'subnets': command.get('subnets'),
+ })
+ self.network_state['interfaces'].update({command.get('name'): iface})
+ self.dump_network_state()
+
+ def handle_vlan(self, command):
+ '''
+ auto eth0.222
+ iface eth0.222 inet static
+ address 10.10.10.1
+ netmask 255.255.255.0
+ 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'), {})
+ iface['vlan-raw-device'] = command.get('vlan_link')
+ iface['vlan_id'] = command.get('vlan_id')
+ interfaces.update({iface['name']: iface})
+
+ def handle_bond(self, command):
+ '''
+ #/etc/network/interfaces
+ auto eth0
+ iface eth0 inet manual
+ bond-master bond0
+ bond-mode 802.3ad
+
+ auto eth1
+ iface eth1 inet manual
+ bond-master bond0
+ bond-mode 802.3ad
+
+ auto bond0
+ iface bond0 inet static
+ address 192.168.0.10
+ gateway 192.168.0.1
+ netmask 255.255.255.0
+ bond-slaves none
+ bond-mode 802.3ad
+ bond-miimon 100
+ bond-downdelay 200
+ 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')
+ iface = interfaces.get(command.get('name'), {})
+ for param, val in command.get('params').items():
+ iface.update({param: val})
+ iface.update({'bond-slaves': 'none'})
+ self.network_state['interfaces'].update({iface['name']: iface})
+
+ # handle bond slaves
+ for ifname in command.get('bond_interfaces'):
+ if ifname not in interfaces:
+ cmd = {
+ 'name': ifname,
+ 'type': 'bond',
+ }
+ # inject placeholder
+ self.handle_physical(cmd)
+
+ interfaces = self.network_state.get('interfaces')
+ bond_if = interfaces.get(ifname)
+ bond_if['bond-master'] = command.get('name')
+ # copy in bond config into slave
+ for param, val in command.get('params').items():
+ bond_if.update({param: val})
+ self.network_state['interfaces'].update({ifname: bond_if})
+
+ def handle_bridge(self, command):
+ '''
+ auto br0
+ iface br0 inet static
+ address 10.10.10.1
+ netmask 255.255.255.0
+ bridge_ports eth0 eth1
+ bridge_stp off
+ bridge_fd 0
+ bridge_maxwait 0
+
+ bridge_params = [
+ "bridge_ports",
+ "bridge_ageing",
+ "bridge_bridgeprio",
+ "bridge_fd",
+ "bridge_gcint",
+ "bridge_hello",
+ "bridge_hw",
+ "bridge_maxage",
+ "bridge_maxwait",
+ "bridge_pathcost",
+ "bridge_portprio",
+ "bridge_stp",
+ "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
+ interfaces = self.network_state.get('interfaces')
+ for ifname in command.get('bridge_interfaces'):
+ if ifname in interfaces:
+ continue
+
+ cmd = {
+ 'name': ifname,
+ }
+ # inject placeholder
+ self.handle_physical(cmd)
+
+ interfaces = self.network_state.get('interfaces')
+ self.handle_physical(command)
+ iface = interfaces.get(command.get('name'), {})
+ iface['bridge_ports'] = command['bridge_interfaces']
+ for param, val in command.get('params').items():
+ iface.update({param: val})
+
+ interfaces.update({iface['name']: iface})
+
+ 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']
+ if not type(addrs) == list:
+ addrs = [addrs]
+ for addr in addrs:
+ dns['nameservers'].append(addr)
+ if 'search' in command:
+ paths = command['search']
+ if not isinstance(paths, list):
+ paths = [paths]
+ for path in paths:
+ dns['search'].append(path)
+
+ 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))
+ route = {
+ 'network': network,
+ 'netmask': netmask,
+ 'gateway': command.get('gateway'),
+ 'metric': command.get('metric'),
+ }
+ routes.append(route)
+
+
+def cidr2mask(cidr):
+ mask = [0, 0, 0, 0]
+ for i in list(range(0, cidr)):
+ idx = int(i / 8)
+ mask[idx] = mask[idx] + (1 << (7 - i % 8))
+ return ".".join([str(x) for x in 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)