#!/usr/bin/python
#
# Copyright (c) 2015, Arista Networks, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#   Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
#
#   Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
#
#   Neither the name of Arista Networks nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
DOCUMENTATION = """
---
module: eos_portchannel
short_description: Manage Port-Channel interfaces in EOS
description:
  - The eos_portchannel module manages the interface configuration for
    logical Port-Channel interfaces on EOS nodes.
version_added: 1.0.0
category: Interfaces
author: Arista EOS+
requirements:
  - Arista EOS 4.13.7M or later with command API enabled
  - Python Client for eAPI 0.3.0 or later
notes:
  - All configuration is idempotent unless otherwise specified
  - Supports eos metaparameters for using the eAPI transport
  - Supports stateful resource configuration.
options:
  name:
    description:
      - The unique interface identifier name.  The interface name must use
        the full interface name (no abbreviated names).  For example,
        interfaces should be specified as Ethernet1 not Et1
    required: true
    default: null
    choices: []
    aliases: []
    version_added: 1.0.0
  enable:
    description:
      - Configures the administrative state for the interface.  Setting
        the value to true will adminstrative enable the interface and
        setting the value to false will administratively disable the
        interface.  The EOS default value for enable is true
    required: false
    default: true
    choices: ['True', 'False']
    aliases: []
    version_added: 1.0.0
  description:
    description:
      - Configures a one lne ASCII description for the interface.  The EOS
        default value for description is None
    required: false
    default: null
    choices: []
    aliases: []
    version_added: 1.0.0
  members:
    description:
      - Configures the set of physical Ethernet interfaces that are bundled
        together to create the logical Port-Channel interface.  Member
        interface names should be a comma separated list of physical Ethernet
        interface names to be included in the named interface.
    required: false
    default: null
    choices: []
    aliases: []
    version_added: 1.0.0
  minimum_links:
    description:
      - Conifugres the minimum links value which specifies the miniumum
        number of physical Ethernet interfaces that must be operationally
        up for the entire Port-Channel interface to be considered
        operationally up.  Valid values for minimum links are in the range
        of 0 to 16.  The EOS default value for min-links is 0
    required: false
    default: null
    choices: []
    aliases: []
    version_added: 1.0.0
  lacp_mode:
    description:
      - Configures the LACP mode configured on the named interface.  The
        LACP mode identifies the negotiation protocol used between peers.
    required: false
    default: null
    choices: ['active', 'passive', 'disabled']
    aliases: []
    version_added: 1.0.0
"""

EXAMPLES = """

- name: Ensure Port-Channel1 has members Ethernet1 and 2
  eos_portchannel: name=Port-Channel1 members=Ethernet1,Ethernet2

- name: Ensure Port-Channel10 uses lacp mode active
  eos_portchannel: name=Port-Channel10 members=Ethernet1,Ethernet3
                   lacp_mode=active
"""
#<<EOS_COMMON_MODULE_START>>

import syslog
import collections

from ansible.module_utils.basic import *

try:
    import pyeapi
    PYEAPI_AVAILABLE = True
except ImportError:
    PYEAPI_AVAILABLE = False

DEFAULT_SYSLOG_PRIORITY = syslog.LOG_NOTICE
DEFAULT_CONNECTION = 'localhost'
TRANSPORTS = ['socket', 'http', 'https', 'http_local']

class EosAnsibleModule(AnsibleModule):

    meta_args = {
        'config': dict(),
        'username': dict(),
        'password': dict(),
        'host': dict(),
        'connection': dict(default=DEFAULT_CONNECTION),
        'transport': dict(choices=TRANSPORTS),
        'port': dict(),
        'debug': dict(type='bool', default='false'),
        'logging': dict(type='bool', default='true')
    }

    stateful_args = {
        'state': dict(default='present', choices=['present', 'absent']),
    }

    def __init__(self, stateful=True, *args, **kwargs):

        kwargs['argument_spec'].update(self.meta_args)

        self._stateful = stateful
        if stateful:
            kwargs['argument_spec'].update(self.stateful_args)

        super(EosAnsibleModule, self).__init__(*args, **kwargs)

        self.result = dict(changed=False, changes=dict())

        self._debug = kwargs.get('debug') or self.boolean(self.params['debug'])
        self._logging = kwargs.get('logging') or self.params['logging']

        self.log('DEBUG flag is %s' % self._debug)

        self.debug('pyeapi_version', self.check_pyeapi())
        self.debug('stateful', self._stateful)
        self.debug('params', self.params)

        self._attributes = self.map_argument_spec()
        self.validate()

        self._node = self.connect()
        self._instance = None

        self.desired_state = self.params['state'] if self._stateful else None
        self.exit_after_flush = kwargs.get('exit_after_flush')

    @property
    def instance(self):
        if self._instance:
            return self._instance

        func = self.func('instance')
        if not func:
            self.fail('Module does not support "instance"')

        try:
            self._instance = func(self)
        except Exception as exc:
            self.fail('instance[error]: %s' % exc.message)

        self.log("called instance: %s" % self._instance)
        return self._instance

    @property
    def attributes(self):
        return self._attributes

    @property
    def node(self):
        if self._node:
            return self._node
        self._node = self.connect()
        return self._node

    def check_pyeapi(self):
        if not PYEAPI_AVAILABLE:
            self.fail('Unable to import pyeapi, is it installed?')
        return pyeapi.__version__

    def map_argument_spec(self):
        """map_argument_spec maps only the module argument spec to attrs

        This method will map the argumentspec minus the meta_args to attrs
        and return the attrs.  This returns a dict object that includes only
        the original argspec plus the stateful_args (if self._stateful=True)

        Returns:
            dict: Returns a dict object that includes the original
                argument_spec plus stateful_args with values minus meta_args

        """
        keys = set(self.params).difference(self.meta_args)
        attrs = dict()
        attrs = dict([(k, self.params[k]) for k in self.params if k in keys])
        if 'CHECKMODE' in attrs:
            del attrs['CHECKMODE']
        return attrs

    def validate(self):
        for key, value in self.attributes.iteritems():
            func = self.func('validate_%s' % key)
            if func:
                self.attributes[key] = func(value)

    def create(self):
        if not self.check_mode:
            func = self.func('create')
            if not func:
                self.fail('Module must define "create" function')
            return self.invoke(func, self)

    def remove(self):
        if not self.check_mode:
            func = self.func('remove')
            if not func:
                self.fail('Module most define "remove" function')
            return self.invoke(func, self)

    def flush(self, exit_after_flush=False):
        self.exit_after_flush = exit_after_flush

        if self.desired_state == 'present' or not self._stateful:
            if self.instance.get('state') == 'absent':
                changed = self.create()
                self.result['changed'] = changed or True
                self.refresh()

            changeset = self.attributes.viewitems() - self.instance.viewitems()

            if self._debug:
                self.debug('desired_state', self.attributes)
                self.debug('current_state', self.instance)

            changes = self.update(changeset)
            if changes:
                self.result['changes'] = changes
                self.result['changed'] = True

            self._attributes.update(changes)

            flush = self.func('flush')
            if flush:
                self.invoke(flush, self)

        elif self.desired_state == 'absent' and self._stateful:
            if self.instance.get('state') == 'present':
                changed = self.remove()
                self.result['changed'] = changed or True

        elif self._stateful:
            if self.desired_state != self.instance.get('state'):
                changed = self.invoke(self.instance.get('state'))
                self.result['changed'] = changed or True

        self.refresh()
        self.result['instance'] = self.instance

        if self.exit_after_flush:
            self.exit()

    def update(self, changeset):
        changes = dict()
        for key, value in changeset:
            if value is not None:
                changes[key] = value
                func = self.func('set_%s' % key)
                if func and not self.check_mode:
                    try:
                        self.invoke(func, self)
                    except Exception as exc:
                        self.fail(exc.message)
        return changes

    def connect(self):
        if self.params['config']:
            pyeapi.load_config(self.params['config'])

        config = dict()

        if self.params['connection']:
            config = pyeapi.config_for(self.params['connection'])
            if not config:
                msg = 'Connection name "%s" not found' % self.params['connection']
                self.fail(msg)

        if self.params['username']:
            config['username'] = self.params['username']

        if self.params['password']:
            config['password'] = self.params['password']

        if self.params['transport']:
            config['transport'] = self.params['transport']

        if self.params['port']:
            config['port'] = self.params['port']

        if self.params['host']:
            config['host'] = self.params['host']

        if 'transport' not in config:
            self.fail('Connection must define a transport')

        connection = pyeapi.client.make_connection(**config)
        node = pyeapi.client.Node(connection, **config)

        try:
            resp = node.enable('show version')
            self.debug('eos_version', resp[0]['result']['version'])
            self.debug('eos_model', resp[0]['result']['modelName'])
        except (pyeapi.eapilib.ConnectionError, pyeapi.eapilib.CommandError):
            self.fail('unable to connect to %s' % node)
        else:
            self.log('Connected to node %s' % node)
            self.debug('node', str(node))

        return node

    def config(self, commands):
        self.result['changed'] = True
        if not self.check_mode:
            self.node.config(commands)

    def api(self, module):
        return self.node.api(module)

    def func(self, name):
        return globals().get(name)

    def invoke(self, func, *args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as exc:
            self.fail(exc.message)

    def invoke_function(self, name, *args, **kwargs):
        func = self.func(name)
        if func:
            return self.invoke(func, *args, **kwargs)

    def fail(self, msg):
        self.invoke_function('on_fail', self)
        self.log('ERROR: %s' % msg, syslog.LOG_ERR)
        self.fail_json(msg=msg)

    def exit(self):
        self.invoke_function('on_exit', self)
        self.log('Module completed successfully')
        self.exit_json(**self.result)

    def refresh(self):
        self._instance = None

    def debug(self, key, value):
        if self._debug:
            if 'debug' not in self.result:
                self.result['debug'] = dict()
            self.result['debug'][key] = value

    def log(self, message, priority=None):
        if self._logging:
            syslog.openlog('ansible-eos')
            priority = priority or DEFAULT_SYSLOG_PRIORITY
            syslog.syslog(priority, str(message))

    @classmethod
    def add_state(cls, name):
        cls.stateful_args['state']['choices'].append(name)

#<<EOS_COMMON_MODULE_END>>

def instance(module):
    """ Returns  the interface properties for the specified name
    """
    name = module.attributes['name']
    result = module.node.api('interfaces').get(name)
    _instance = dict(name=name, state='absent')
    if result:
        _instance['state'] = 'present'
        _instance.update(result)
        desc = '' if not result['description'] else result['description']
        _instance['description'] = desc
        _instance['enable'] = not result['shutdown']
        _instance['members'] = ','.join(result['members'])
        lacp_mode = result['lacp_mode']
        _instance['lacp_mode'] = 'disabled' if lacp_mode == 'on' else lacp_mode
    return _instance

def create(module):
    """Creates a new instance of interface on the node
    """
    name = module.attributes['name']
    module.log('Invoked create for eos_portchannel[%s]' % name)
    module.node.api('interfaces').create(name)

def remove(module):
    """Creates a new instance of interface on the node
    """
    name = module.attributes['name']
    module.log('Invoked remove for eos_portchannel[%s]' % name)
    module.node.api('interfaces').delete(name)

def set_description(module):
    """ Configures the description attribute for the interface
    """
    value = module.attributes['description']
    name = module.attributes['name']
    value = None if value == '' else value
    module.log('Invoked set_description for eos_portchannel[%s] '
               'with value %s' % (name, value))
    module.node.api('interfaces').set_description(name, value)

def set_enable(module):
    """ Configures the enable attribute for the interface
    """
    value = not module.attributes['enable']
    name = module.attributes['name']
    module.log('Invoked set_enable for eos_portchannel[%s] '
               'with value %s' % (name, value))
    module.node.api('interfaces').set_shutdown(name, value)

def set_members(module):
    """ Configures the members attribute for the interface
    """
    value = module.attributes['members'].split(',')
    mode = module.attributes['lacp_mode']
    name = module.attributes['name']
    module.log('Invoked set_members for eos_portchannel[%s] '
               'with value %s, mode %s' % (name, value, mode))
    module.node.api('interfaces').set_members(name, value, mode=mode)

def set_minimum_links(module):
    """ Configures the minimum links attribute for the interface
    """
    value = module.attributes['minimum_links']
    name = module.attributes['name']
    module.log('Invoked set_minimum_links for eos_portchannel[%s] '
               'with value %s' % (name, value))
    module.node.api('interfaces').set_minimum_links(name, value)

def set_lacp_mode(module):
    """ Configures the lacp mode attribute for the interface
    """
    value = module.attributes['lacp_mode']
    value = 'on' if value == 'disabled' else value
    name = module.attributes['name']
    module.log('Invoked set_lacp_mode for eos_portchannel[%s] '
               'with value %s' % (name, value))
    module.node.api('interfaces').set_lacp_mode(name, value)

def main():
    """ The main module routine called when the module is run by Ansible
    """

    argument_spec = dict(
        name=dict(required=True),
        enable=dict(type='bool', default=True),
        description=dict(),
        members=dict(),
        minimum_links=dict(type='int'),
        lacp_mode=dict(choices=['active', 'passive', 'disabled'])
    )

    module = EosAnsibleModule(argument_spec=argument_spec,
                              supports_check_mode=True)

    module.flush(True)


main()
