From: Marco Ceppi Date: Mon, 28 Mar 2016 19:21:34 +0000 (-0400) Subject: Inclusion of vpe-router layer from MWC demo X-Git-Tag: v2.0.2~7^2~24 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;ds=sidebyside;h=c5a5e53d6e7d31717208fe1f9f2254f9917ef8ea;hp=--cc;p=osm%2Fdevops.git Inclusion of vpe-router layer from MWC demo Signed-off-by: Marco Ceppi --- c5a5e53d6e7d31717208fe1f9f2254f9917ef8ea diff --git a/vpe-router/actions.yaml b/vpe-router/actions.yaml new file mode 100644 index 00000000..913cc64e --- /dev/null +++ b/vpe-router/actions.yaml @@ -0,0 +1,96 @@ +configure-interface: + description: Configure an ethernet interface. + params: + iface-name: + type: string + description: Device name, e.g. eth1 + cidr: + type: string + description: Network range to assign to the interface + required: [iface-name] +add-corporation: + description: Add a new corporation to the router + params: + domain-name: + type: string + description: Name of the vlan corporation + iface-name: + type: string + description: Device name. eg eth1 + vlan-id: + type: integer + description: The name of the vlan? + cidr: + type: string + description: Network range to assign to the tagged vlan-id + area: + type: string + description: Link State Advertisements (LSA) type + subnet-cidr: + type: string + description: Network range + subnet-area: + type: string + description: Link State Advertisements (LSA) type + required: [domain-name, iface-name, vlan-id, cidr, area, subnet-cidr, subnet-area] +delete-corporation: + description: Remove the corporation from the router completely + params: + domain-name: + type: string + description: The domain of the corporation to remove + cidr: + type: string + description: Network range to assign to the tagged vlan-id + area: + type: string + description: Link State Advertisements (LSA) type + subnet-cidr: + type: string + description: Network range + subnet-area: + type: string + description: Link State Advertisements (LSA) type + required: [domain-name, cidr, area, subnet-cidr, subnet-area] +connect-domains: + description: Connect the router to another router, where the same domain is present + params: + domain-name: + type: string + description: The domain of the coproration to connect + iface-name: + type: string + description: Device name. eg eth1 + tunnel-name: + type: string + description: Name of the tunnel ? + local-ip: + type: string + description: local ip ? + remote-ip: + type: string + description: remote ip ? + tunnel-key: + type: string + description: tunnel key? + internal-local-ip: + type: string + description: internal local ip? + internal-remote-ip: + type: string + description: internal remote ip? + tunnel-type: + type: string + default: gre + description: The type of tunnel to establish. + required: [domain-name, iface-name, tunnel-name, local-ip, remote-ip, tunnel-key, internal-local-ip, internal-remote-ip] +delete-domain-connection: + description: Remove the tunnel to another router where the domain is present. + params: + domain-name: + type: string + description: The domain of the corporation to unlink + tunnel-name: + type: string + description: The name of the tunnel to unlink that the domain-name is attached to + required: [domain-name, tunnel-name] diff --git a/vpe-router/actions/add-corporation b/vpe-router/actions/add-corporation new file mode 100755 index 00000000..c8ab2f80 --- /dev/null +++ b/vpe-router/actions/add-corporation @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import sys +sys.path.append('lib') + +from charms.reactive import main +from charms.reactive import set_state +from charmhelpers.core.hookenv import action_fail + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_state('vpe.add-corporation') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/vpe-router/actions/configure-interface b/vpe-router/actions/configure-interface new file mode 100755 index 00000000..db9a099c --- /dev/null +++ b/vpe-router/actions/configure-interface @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import sys +sys.path.append('lib') + +from charms.reactive import main +from charms.reactive import set_state +from charmhelpers.core.hookenv import action_fail + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_state('vpe.configure-interface') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/vpe-router/actions/connect-domains b/vpe-router/actions/connect-domains new file mode 100755 index 00000000..48adfc76 --- /dev/null +++ b/vpe-router/actions/connect-domains @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.reactive import main +from charms.reactive import set_state +from charmhelpers.core.hookenv import action_fail + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_state('vpe.connect-domains') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/vpe-router/actions/delete-corporation b/vpe-router/actions/delete-corporation new file mode 100755 index 00000000..0576c082 --- /dev/null +++ b/vpe-router/actions/delete-corporation @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import sys +sys.path.append('lib') + +from charms.reactive import main +from charms.reactive import set_state +from charmhelpers.core.hookenv import action_fail + + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_state('vpe.delete-corporation') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/vpe-router/actions/delete-domain-connection b/vpe-router/actions/delete-domain-connection new file mode 100755 index 00000000..5ba05f65 --- /dev/null +++ b/vpe-router/actions/delete-domain-connection @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import sys +sys.path.append('lib') + +from charms.reactive import main +from charms.reactive import set_state +from charmhelpers.core.hookenv import action_fail + + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_state('vpe.delete-domain-connection') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/vpe-router/config.yaml b/vpe-router/config.yaml new file mode 100644 index 00000000..562515fa --- /dev/null +++ b/vpe-router/config.yaml @@ -0,0 +1,17 @@ +options: + vpe-router: + default: + type: string + description: Hostname or IP of the vpe router to connect to + user: + type: string + default: root + description: Username for VPE Router + pass: + type: string + default: + description: Password for VPE Router + hostname: + type: string + default: + description: The hostname to set the vpe router to. diff --git a/vpe-router/layer.yaml b/vpe-router/layer.yaml new file mode 100644 index 00000000..524a4f45 --- /dev/null +++ b/vpe-router/layer.yaml @@ -0,0 +1 @@ +includes: ['layer:basic'] diff --git a/vpe-router/lib/charms/router.py b/vpe-router/lib/charms/router.py new file mode 100644 index 00000000..54ff7fb6 --- /dev/null +++ b/vpe-router/lib/charms/router.py @@ -0,0 +1,80 @@ + +import paramiko +import subprocess + +from charmhelpers.core.hookenv import config + + +class NetNS(object): + def __init__(self, name): + pass + + @classmethod + def create(cls, name): + # @TODO: Need to check if namespace exists already + try: + ip('netns', 'add', name) + except Exception as e: + raise Exception('could not create net namespace: %s' % e) + + return cls(name) + + def up(self, iface, cidr): + self.do('ip', 'link', 'set', 'dev', iface, 'up') + self.do('ip', 'address', 'add', cidr, 'dev', iface) + + def add_iface(self, iface): + ip('link', 'set', 'dev', iface, 'netns', self.name) + + def do(self, *cmd): + ip(*['netns', 'exec', self.name] + cmd) + + +def ip(*args): + return _run(['ip'] + list(args)) + + +def _run(cmd, env=None): + if isinstance(cmd, str): + cmd = cmd.split() if ' ' in cmd else [cmd] + + cfg = config() + if all(k in cfg for k in ['pass', 'vpe-router', 'user']): + router = cfg['vpe-router'] + user = cfg['user'] + passwd = cfg['pass'] + + if router and user and passwd: + return ssh(cmd, router, user, passwd) + + p = subprocess.Popen(cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + retcode = p.poll() + if retcode > 0: + raise subprocess.CalledProcessError(returncode=retcode, + cmd=cmd, + output=stderr.decode("utf-8").strip()) + return (''.join(stdout), ''.join(stderr)) + + +def ssh(cmd, host, user, password=None): + ''' Suddenly this project needs to SSH to something. So we replicate what + _run was doing with subprocess using the Paramiko library. This is + temporary until this charm /is/ the VPE Router ''' + + cmds = ' '.join(cmd) + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(host, port=22, username=user, password=password) + + stdin, stdout, stderr = client.exec_command(cmds) + retcode = stdout.channel.recv_exit_status() + client.close() # @TODO re-use connections + if retcode > 0: + output = stderr.read().strip() + raise subprocess.CalledProcessError(returncode=retcode, cmd=cmd, + output=output) + return (''.join(stdout), ''.join(stderr)) diff --git a/vpe-router/metadata.yaml b/vpe-router/metadata.yaml new file mode 100644 index 00000000..ccc841fa --- /dev/null +++ b/vpe-router/metadata.yaml @@ -0,0 +1,11 @@ +name: vpe-router +maintainers: + - Marco Ceppi + - Adam Israel +summary: setup a virtualized PE Router with GRE tunnels +description: | + this charm, when deployed and configured, will provide a secure virtualized + provider edge router. +peers: + loadbalance: + interface: vpe-router diff --git a/vpe-router/reactive/vpe_router.py b/vpe-router/reactive/vpe_router.py new file mode 100644 index 00000000..c62983d8 --- /dev/null +++ b/vpe-router/reactive/vpe_router.py @@ -0,0 +1,639 @@ + +from charmhelpers.core.hookenv import ( + config, + status_set, + action_get, + action_fail, + log, +) + +from charms.reactive import ( + hook, + when, + when_not, + helpers, + set_state, + remove_state, +) + +from charms import router +import subprocess + +cfg = config() + + +@hook('config-changed') +def validate_config(): + try: + """ + If the ssh credentials are available, we'll act as a proxy charm. + Otherwise, we execute against the unit we're deployed on to. + """ + if all(k in cfg for k in ['pass', 'vpe-router', 'user']): + routerip = cfg['vpe-router'] + user = cfg['user'] + passwd = cfg['pass'] + + if routerip and user and passwd: + # Assumption: this will be a root user + out, err = router.ssh(['whoami'], routerip, + user, passwd) + if out.strip() != user: + raise Exception('invalid credentials') + + # Set the router's hostname + try: + if user == 'root' and 'hostname' in cfg: + hostname = cfg['hostname'] + out, err = router.ssh(['hostname', hostname], + routerip, + user, passwd) + out, err = router.ssh(['sed', + '-i', + '"s/hostname.*$/hostname %s/"' + % hostname, + '/usr/admin/global/hostname.sh' + ], + routerip, + user, passwd) + + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + raise + + set_state('vpe.configured') + status_set('active', 'ready!') + + except Exception as e: + log(repr(e)) + remove_state('vpe.configured') + status_set('blocked', 'validation failed: %s' % e) + + +@when_not('vpe.configured') +def not_ready_add(): + actions = [ + 'vpe.add-corporation', + 'vpe.connect-domains', + 'vpe.delete-domain-connections', + 'vpe.remove-corporation', + 'vpe.configure-interface', + 'vpe.configure-ospf', + ] + + if helpers.any_states(*actions): + action_fail('VPE is not configured') + + status_set('blocked', 'vpe is not configured') + + +def start_ospfd(): + # We may want to make this configurable via config setting + ospfd = '/usr/local/bin/ospfd' + + try: + (stdout, stderr) = router._run(['touch', + '/usr/admin/global/ospfd.conf']) + (stdout, stderr) = router._run([ospfd, '-d', '-f', + '/usr/admin/global/ospfd.conf']) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + + +def configure_ospf(domain, cidr, area, subnet_cidr, subnet_area, enable=True): + """Configure the OSPF service""" + + # Check to see if the OSPF daemon is running, and start it if not + try: + (stdout, stderr) = router._run(['pgrep', 'ospfd']) + except subprocess.CalledProcessError as e: + # If pgrep fails, the process wasn't found. + start_ospfd() + log('Command failed (ospfd not running): %s (%s)' % + (' '.join(e.cmd), str(e.output))) + + upordown = '' + if not enable: + upordown = 'no' + try: + vrfctl = '/usr/local/bin/vrfctl' + vtysh = '/usr/local/bin/vtysh' + + (stdout, stderr) = router._run([vrfctl, 'list']) + + domain_id = 0 + for line in stdout.split('\n'): + if domain in line: + domain_id = int(line[3:5]) + + if domain_id > 0: + router._run([vtysh, + '-c', + '"configure terminal"', + '-c', + '"router ospf %d vr %d"' % (domain_id, domain_id), + '-c', + '"%s network %s area %s"' % (upordown, cidr, area), + '-c', + '"%s network %s area %s"' % (upordown, + subnet_cidr, + subnet_area), + ]) + + else: + log("Invalid domain id") + except subprocess.CalledProcessError as e: + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + finally: + remove_state('vpe.configure-interface') + status_set('active', 'ready!') + + +@when('vpe.configured') +@when('vpe.configure-interface') +def configure_interface(): + """ + Configure an ethernet interface + """ + iface_name = action_get('iface-name') + cidr = action_get('cidr') + + # cidr is optional + if cidr: + try: + # Add may fail, but change seems to add or update + router.ip('address', 'change', cidr, 'dev', iface_name) + except subprocess.CalledProcessError as e: + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + return + finally: + remove_state('vpe.configure-interface') + status_set('active', 'ready!') + + try: + router.ip('link', 'set', 'dev', iface_name, 'up') + except subprocess.CalledProcessError as e: + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + finally: + remove_state('vpe.configure-interface') + status_set('active', 'ready!') + + +@when('vpe.configured') +@when('vpe.add-corporation') +def add_corporation(): + ''' + Create and Activate the network corporation + ''' + domain_name = action_get('domain-name') + iface_name = action_get('iface-name') + # HACK: python's list, used deeper, throws an exception on ints in a tuple + vlan_id = str(action_get('vlan-id')) + cidr = action_get('cidr') + area = action_get('area') + subnet_cidr = action_get('subnet-cidr') + subnet_area = action_get('subnet-area') + + iface_vlanid = '%s.%s' % (iface_name, vlan_id) + + status_set('maintenance', 'adding corporation {}'.format(domain_name)) + + """ + Attempt to run all commands to add the network corporation. If any step + fails, abort and call `delete_corporation()` to undo. + """ + try: + """ + $ ip link add link eth3 name eth3.103 type vlan id 103 + """ + router.ip('link', + 'add', + 'link', + iface_name, + 'name', + iface_vlanid, + 'type', + 'vlan', + 'id', + vlan_id) + + """ + $ ip netns add domain + """ + router.ip('netns', + 'add', + domain_name) + + """ + $ ip link set dev eth3.103 netns corpB + """ + router.ip('link', + 'set', + 'dev', + iface_vlanid, + 'netns', + domain_name) + + """ + $ ifconfig eth3 up + """ + router._run(['ifconfig', iface_name, 'up']) + + """ + $ ip netns exec corpB ip link set dev eth3.103 up + """ + router.ip('netns', + 'exec', + domain_name, + 'ip', + 'link', + 'set', + 'dev', + iface_vlanid, + 'up') + + """ + $ ip netns exec corpB ip address add 10.0.1.1/24 dev eth3.103 + """ + mask = cidr.split("/")[1] + ip = '%s/%s' % (area, mask) + router.ip('netns', + 'exec', + domain_name, + 'ip', + 'address', + 'add', + ip, + 'dev', + iface_vlanid) + + configure_ospf(domain_name, cidr, area, subnet_cidr, subnet_area, True) + + except subprocess.CalledProcessError as e: + delete_corporation() + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + finally: + remove_state('vpe.add-corporation') + status_set('active', 'ready!') + + +@when('vpe.configured') +@when('vpe.delete-corporation') +def delete_corporation(): + + domain_name = action_get('domain-name') + cidr = action_get('cidr') + area = action_get('area') + subnet_cidr = action_get('subnet-cidr') + subnet_area = action_get('subnet-area') + + status_set('maintenance', 'deleting corporation {}'.format(domain_name)) + + try: + """ + Remove all tunnels defined for this domain + + $ ip netns exec domain_name ip tun show + | grep gre + | grep -v "remote any" + | cut -d":" -f1 + """ + p = router.ip( + 'netns', + 'exec', + domain_name, + 'ip', + 'tun', + 'show', + '|', + 'grep', + 'gre', + '|', + 'grep', + '-v', + '"remote any"', + '|', + 'cut -d":" -f1' + ) + + # `p` should be a tuple of (stdout, stderr) + tunnels = p[0].split('\n') + + for tunnel in tunnels: + try: + """ + $ ip netns exec domain_name ip link set $tunnel_name down + """ + router.ip( + 'netns', + 'exec', + domain_name, + 'ip', + 'link', + 'set', + tunnel, + 'down' + ) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + pass + + try: + """ + $ ip netns exec domain_name ip tunnel del $tunnel_name + """ + router.ip( + 'netns', + 'exec', + domain_name, + 'ip', + 'tunnel', + 'del', + tunnel + ) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + pass + + """ + Remove all interfaces associated to the domain + + $ ip netns exec domain_name ifconfig | grep mtu | cut -d":" -f1 + """ + p = router.ip( + 'netns', + 'exec', + domain_name, + 'ifconfig', + '|', + 'grep mtu', + '|', + 'cut -d":" -f1' + ) + + ifaces = p[0].split('\n') + for iface in ifaces: + + try: + """ + $ ip netns exec domain_name ip link set $iface down + """ + router.ip( + 'netns', + 'exec', + domain_name, + 'ip', + 'link', + 'set', + iface, + 'down' + ) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + + try: + """ + $ ifconfig eth3 down + """ + router._run(['ifconfig', iface, 'down']) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + pass + + try: + """ + $ ip link del dev $iface + """ + router.ip( + 'link', + 'del', + 'dev', + iface + ) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + pass + + try: + """ + Remove the domain + + $ ip netns del domain_name + """ + router.ip( + 'netns', + 'del', + domain_name + ) + except subprocess.CalledProcessError as e: + log('Command failed: %s (%s)' % (' '.join(e.cmd), str(e.output))) + pass + + try: + configure_ospf(domain_name, + cidr, + area, + subnet_cidr, + subnet_area, + False) + except subprocess.CalledProcessError as e: + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + + except: + # Do nothing + log('delete-corporation failed.') + pass + + finally: + remove_state('vpe.delete-corporation') + status_set('active', 'ready!') + + +@when('vpe.configured') +@when('vpe.connect-domains') +def connect_domains(): + + params = [ + 'domain-name', + 'iface-name', + 'tunnel-name', + 'local-ip', + 'remote-ip', + 'tunnel-key', + 'internal-local-ip', + 'internal-remote-ip', + 'tunnel-type', + ] + + config = {} + for p in params: + config[p] = action_get(p) + + status_set('maintenance', 'connecting domains') + + try: + """ + $ ip tunnel add tunnel_name mode gre local local_ip remote remote_ip + dev iface_name key tunnel_key csum + """ + router.ip( + 'tunnel', + 'add', + config['tunnel-name'], + 'mode', + config['tunnel-type'], + 'local', + config['local-ip'], + 'remote', + config['remote-ip'], + 'dev', + config['iface-name'], + 'key', + config['tunnel-key'], + 'csum' + ) + + except subprocess.CalledProcessError as e: + log('Command failed (retrying with ip tunnel change): %s (%s)' % + (' '.join(e.cmd), str(e.output))) + try: + """ + If the tunnel already exists (like gre0) and can't be deleted, + modify it instead of trying to add it. + """ + router.ip( + 'tunnel', + 'change', + config['tunnel-name'], + 'mode', + config['tunnel-type'], + 'local', + config['local-ip'], + 'remote', + config['remote-ip'], + 'dev', + config['iface-name'], + 'key', + config['tunnel-key'], + 'csum' + ) + except subprocess.CalledProcessError as e: + delete_domain_connection() + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + finally: + remove_state('vpe.connect-domains') + status_set('active', 'ready!') + + try: + """ + $ ip link set dev tunnel_name netns domain_name + """ + router.ip( + 'link', + 'set', + 'dev', + config['tunnel-name'], + 'netns', + config['domain-name'] + ) + + """ + $ ip netns exec domain_name ip link set dev tunnel_name up + """ + router.ip( + 'netns', + 'exec', + config['domain-name'], + 'ip', + 'link', + 'set', + 'dev', + config['tunnel-name'], + 'up' + ) + + """ + $ ip netns exec domain_name ip address add internal_local_ip peer + internal_remote_ip dev tunnel_name + """ + router.ip( + 'netns', + 'exec', + config['domain-name'], + 'ip', + 'address', + 'add', + config['internal-local-ip'], + 'peer', + config['internal-remote-ip'], + 'dev', + config['tunnel-name'] + ) + except subprocess.CalledProcessError as e: + delete_domain_connection() + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + finally: + remove_state('vpe.connect-domains') + status_set('active', 'ready!') + + +@when('vpe.configured') +@when('vpe.delete-domain-connection') +def delete_domain_connection(): + ''' Remove the tunnel to another router where the domain is present ''' + domain = action_get('domain-name') + tunnel_name = action_get('tunnel-name') + + status_set('maintenance', 'deleting domain connection: {}'.format(domain)) + + try: + + try: + """ + $ ip netns exec domain_name ip link set tunnel_name down + """ + router.ip('netns', + 'exec', + domain, + 'ip', + 'link', + 'set', + tunnel_name, + 'down') + except subprocess.CalledProcessError as e: + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + + try: + """ + $ ip netns exec domain_name ip tunnel del tunnel_name + """ + router.ip('netns', + 'exec', + domain, + 'ip', + 'tunnel', + 'del', + tunnel_name) + except subprocess.CalledProcessError as e: + action_fail('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + except: + pass + finally: + remove_state('vpe.delete-domain-connection') + status_set('active', 'ready!') diff --git a/vpe-router/wheelhouse.txt b/vpe-router/wheelhouse.txt new file mode 100644 index 00000000..df2de692 --- /dev/null +++ b/vpe-router/wheelhouse.txt @@ -0,0 +1 @@ +paramiko>=1.16.0,<1.17