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