From: Adam Israel Date: Tue, 27 Sep 2016 20:15:59 +0000 (-0400) Subject: Migrate vpe-router layer to new directory structure X-Git-Tag: v1.0.0~5 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=88f87dfdc882afc13e0395deb6626e007b67b9fe;p=osm%2Fjuju-charms.git Migrate vpe-router layer to new directory structure Change-Id: I107d48c2977531155072cd3f57bc76f732bb2c50 Signed-off-by: Adam Israel --- diff --git a/layers/vpe-router/README.md b/layers/vpe-router/README.md new file mode 100644 index 0000000..e69de29 diff --git a/layers/vpe-router/actions.yaml b/layers/vpe-router/actions.yaml new file mode 100644 index 0000000..913cc64 --- /dev/null +++ b/layers/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/layers/vpe-router/actions/add-corporation b/layers/vpe-router/actions/add-corporation new file mode 100755 index 0000000..c8ab2f8 --- /dev/null +++ b/layers/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/layers/vpe-router/actions/configure-interface b/layers/vpe-router/actions/configure-interface new file mode 100755 index 0000000..db9a099 --- /dev/null +++ b/layers/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/layers/vpe-router/actions/connect-domains b/layers/vpe-router/actions/connect-domains new file mode 100755 index 0000000..48adfc7 --- /dev/null +++ b/layers/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/layers/vpe-router/actions/delete-corporation b/layers/vpe-router/actions/delete-corporation new file mode 100755 index 0000000..0576c08 --- /dev/null +++ b/layers/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/layers/vpe-router/actions/delete-domain-connection b/layers/vpe-router/actions/delete-domain-connection new file mode 100755 index 0000000..5ba05f6 --- /dev/null +++ b/layers/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/layers/vpe-router/config.yaml b/layers/vpe-router/config.yaml new file mode 100644 index 0000000..562515f --- /dev/null +++ b/layers/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/layers/vpe-router/layer.yaml b/layers/vpe-router/layer.yaml new file mode 100644 index 0000000..524a4f4 --- /dev/null +++ b/layers/vpe-router/layer.yaml @@ -0,0 +1 @@ +includes: ['layer:basic'] diff --git a/layers/vpe-router/lib/charms/router.py b/layers/vpe-router/lib/charms/router.py new file mode 100644 index 0000000..54ff7fb --- /dev/null +++ b/layers/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/layers/vpe-router/metadata.yaml b/layers/vpe-router/metadata.yaml new file mode 100644 index 0000000..65ae2da --- /dev/null +++ b/layers/vpe-router/metadata.yaml @@ -0,0 +1,14 @@ +name: vpe-router +maintainers: + - Marco Ceppi + - Adam Israel +summary: setup a virtualized PE Router with GRE tunnels +series: + - trusty + - xenial +description: | + this charm, when deployed and configured, will provide a secure virtualized + provider edge router. +peers: + loadbalance: + interface: vpe-router diff --git a/layers/vpe-router/reactive/vpe_router.py b/layers/vpe-router/reactive/vpe_router.py new file mode 100644 index 0000000..e2be327 --- /dev/null +++ b/layers/vpe-router/reactive/vpe_router.py @@ -0,0 +1,644 @@ + +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: + remove_state('vpe.configured') + status_set('blocked', 'vpe is not configured') + 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) + set_state('vpe.configured') + status_set('active', 'ready!') + else: + remove_state('vpe.configured') + status_set('blocked', 'vpe is not configured') + except subprocess.CalledProcessError as e: + remove_state('vpe.configured') + status_set('blocked', 'validation failed: %s' % e) + log('Command failed: %s (%s)' % + (' '.join(e.cmd), str(e.output))) + raise + + 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/layers/vpe-router/wheelhouse.txt b/layers/vpe-router/wheelhouse.txt new file mode 100644 index 0000000..df2de69 --- /dev/null +++ b/layers/vpe-router/wheelhouse.txt @@ -0,0 +1 @@ +paramiko>=1.16.0,<1.17 diff --git a/vpe-router/actions.yaml b/vpe-router/actions.yaml deleted file mode 100644 index 913cc64..0000000 --- a/vpe-router/actions.yaml +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100755 index c8ab2f8..0000000 --- a/vpe-router/actions/add-corporation +++ /dev/null @@ -1,18 +0,0 @@ -#!/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 deleted file mode 100755 index db9a099..0000000 --- a/vpe-router/actions/configure-interface +++ /dev/null @@ -1,18 +0,0 @@ -#!/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 deleted file mode 100755 index 48adfc7..0000000 --- a/vpe-router/actions/connect-domains +++ /dev/null @@ -1,20 +0,0 @@ -#!/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 deleted file mode 100755 index 0576c08..0000000 --- a/vpe-router/actions/delete-corporation +++ /dev/null @@ -1,19 +0,0 @@ -#!/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 deleted file mode 100755 index 5ba05f6..0000000 --- a/vpe-router/actions/delete-domain-connection +++ /dev/null @@ -1,19 +0,0 @@ -#!/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 deleted file mode 100644 index 562515f..0000000 --- a/vpe-router/config.yaml +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 524a4f4..0000000 --- a/vpe-router/layer.yaml +++ /dev/null @@ -1 +0,0 @@ -includes: ['layer:basic'] diff --git a/vpe-router/lib/charms/router.py b/vpe-router/lib/charms/router.py deleted file mode 100644 index 54ff7fb..0000000 --- a/vpe-router/lib/charms/router.py +++ /dev/null @@ -1,80 +0,0 @@ - -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 deleted file mode 100644 index 65ae2da..0000000 --- a/vpe-router/metadata.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: vpe-router -maintainers: - - Marco Ceppi - - Adam Israel -summary: setup a virtualized PE Router with GRE tunnels -series: - - trusty - - xenial -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 deleted file mode 100644 index e2be327..0000000 --- a/vpe-router/reactive/vpe_router.py +++ /dev/null @@ -1,644 +0,0 @@ - -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: - remove_state('vpe.configured') - status_set('blocked', 'vpe is not configured') - 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) - set_state('vpe.configured') - status_set('active', 'ready!') - else: - remove_state('vpe.configured') - status_set('blocked', 'vpe is not configured') - except subprocess.CalledProcessError as e: - remove_state('vpe.configured') - status_set('blocked', 'validation failed: %s' % e) - log('Command failed: %s (%s)' % - (' '.join(e.cmd), str(e.output))) - raise - - 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 deleted file mode 100644 index df2de69..0000000 --- a/vpe-router/wheelhouse.txt +++ /dev/null @@ -1 +0,0 @@ -paramiko>=1.16.0,<1.17