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!')