--- /dev/null
+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]
--- /dev/null
+#!/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))
--- /dev/null
+#!/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))
--- /dev/null
+#!/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))
--- /dev/null
+#!/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))
--- /dev/null
+#!/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))
--- /dev/null
+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.
--- /dev/null
+includes: ['layer:basic']
--- /dev/null
+
+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))
--- /dev/null
+name: vpe-router
+maintainers:
+ - Marco Ceppi <marco.ceppi@canonical.com>
+ - Adam Israel <adam.israel@canonical.com>
+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
--- /dev/null
+
+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!')
--- /dev/null
+paramiko>=1.16.0,<1.17
+++ /dev/null
-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]
+++ /dev/null
-#!/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))
+++ /dev/null
-#!/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))
+++ /dev/null
-#!/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))
+++ /dev/null
-#!/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))
+++ /dev/null
-#!/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))
+++ /dev/null
-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.
+++ /dev/null
-includes: ['layer:basic']
+++ /dev/null
-
-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))
+++ /dev/null
-name: vpe-router
-maintainers:
- - Marco Ceppi <marco.ceppi@canonical.com>
- - Adam Israel <adam.israel@canonical.com>
-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
+++ /dev/null
-
-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!')
+++ /dev/null
-paramiko>=1.16.0,<1.17