#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}

DOCUMENTATION = r'''
---
module: bigip_vlan
short_description: Manage VLANs on a BIG-IP system
description:
  - Manage VLANs on a BIG-IP system
version_added: "2.2"
options:
  description:
    description:
      - The description to give to the VLAN.
  tagged_interfaces:
    description:
      - Specifies a list of tagged interfaces and trunks that you want to
        configure for the VLAN. Use tagged interfaces or trunks when
        you want to assign a single interface or trunk to multiple VLANs.
    aliases:
      - tagged_interface
  untagged_interfaces:
    description:
      - Specifies a list of untagged interfaces and trunks that you want to
        configure for the VLAN.
    aliases:
      - untagged_interface
  name:
    description:
      - The VLAN to manage. If the special VLAN C(ALL) is specified with
        the C(state) value of C(absent) then all VLANs will be removed.
    required: True
  state:
    description:
      - The state of the VLAN on the system. When C(present), guarantees
        that the VLAN exists with the provided attributes. When C(absent),
        removes the VLAN from the system.
    default: present
    choices:
      - absent
      - present
  tag:
    description:
      - Tag number for the VLAN. The tag number can be any integer between 1
        and 4094. The system automatically assigns a tag number if you do not
        specify a value.
  mtu:
    description:
      - Specifies the maximum transmission unit (MTU) for traffic on this VLAN.
        When creating a new VLAN, if this parameter is not specified, the default
        value used will be C(1500).
      - This number must be between 576 to 9198.
    version_added: 2.5
  cmp_hash:
    description:
      - Specifies how the traffic on the VLAN will be disaggregated. The value
        selected determines the traffic disaggregation method. You can choose to
        disaggregate traffic based on C(source-address) (the source IP address),
        C(destination-address) (destination IP address), or C(default), which
        specifies that the default CMP hash uses L4 ports.
      - When creating a new VLAN, if this parameter is not specified, the default
        of C(default) is used.
    version_added: 2.5
  dag_tunnel:
    description:
      - Specifies how the disaggregator (DAG) distributes received tunnel-encapsulated
        packets to TMM instances. Select C(inner) to distribute packets based on information
        in inner headers. Select C(outer) to distribute packets based on information in
        outer headers without inspecting inner headers.
      - When creating a new VLAN, if this parameter is not specified, the default
        of C(outer) is used.
      - This parameter is not supported on Virtual Editions of BIG-IP.
    version_added: 2.5
  dag_round_robin:
    description:
      - Specifies whether some of the stateless traffic on the VLAN should be
        disaggregated in a round-robin order instead of using a static hash. The
        stateless traffic includes non-IP L2 traffic, ICMP, some UDP protocols,
        and so on.
      - When creating a new VLAN, if this parameter is not specified, the default
        of (no) is used.
    version_added: 2.5
    type: bool
  partition:
    description:
      - Device partition to manage resources on.
    default: Common
    version_added: 2.5
notes:
  - Requires BIG-IP versions >= 12.0.0
extends_documentation_fragment: f5
author:
  - Tim Rupp (@caphrim007)
  - Wojciech Wypior (@wojtek0806)
'''

EXAMPLES = r'''
- name: Create VLAN
  bigip_vlan:
      name: "net1"
      password: "secret"
      server: "lb.mydomain.com"
      user: "admin"
      validate_certs: "no"
  delegate_to: localhost

- name: Set VLAN tag
  bigip_vlan:
      name: "net1"
      password: "secret"
      server: "lb.mydomain.com"
      tag: "2345"
      user: "admin"
      validate_certs: "no"
  delegate_to: localhost

- name: Add VLAN 2345 as tagged to interface 1.1
  bigip_vlan:
      tagged_interface: 1.1
      name: "net1"
      password: "secret"
      server: "lb.mydomain.com"
      tag: "2345"
      user: "admin"
      validate_certs: "no"
  delegate_to: localhost

- name: Add VLAN 1234 as tagged to interfaces 1.1 and 1.2
  bigip_vlan:
      tagged_interfaces:
          - 1.1
          - 1.2
      name: "net1"
      password: "secret"
      server: "lb.mydomain.com"
      tag: "1234"
      user: "admin"
      validate_certs: "no"
  delegate_to: localhost
'''

RETURN = r'''
description:
  description: The description set on the VLAN.
  returned: changed
  type: string
  sample: foo VLAN
interfaces:
  description: Interfaces that the VLAN is assigned to.
  returned: changed
  type: list
  sample: ['1.1','1.2']
partition:
  description: The partition that the VLAN was created on.
  returned: changed
  type: string
  sample: Common
tag:
  description: The ID of the VLAN.
  returned: changed
  type: int
  sample: 2345
cmp_hash:
  description: New traffic disaggregation method.
  returned: changed
  type: string
  sample: source-address
dag_tunnel:
  description: The new DAG tunnel setting.
  returned: changed
  type: string
  sample: outer
'''

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback

try:
    # Sideband repository used for dev
    from library.module_utils.network.f5.bigip import HAS_F5SDK
    from library.module_utils.network.f5.bigip import F5Client
    from library.module_utils.network.f5.common import F5ModuleError
    from library.module_utils.network.f5.common import AnsibleF5Parameters
    from library.module_utils.network.f5.common import cleanup_tokens
    from library.module_utils.network.f5.common import fqdn_name
    from library.module_utils.network.f5.common import f5_argument_spec
    try:
        from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
    except ImportError:
        HAS_F5SDK = False
    HAS_DEVEL_IMPORTS = True
except ImportError:
    # Upstream Ansible
    from ansible.module_utils.network.f5.bigip import HAS_F5SDK
    from ansible.module_utils.network.f5.bigip import F5Client
    from ansible.module_utils.network.f5.common import F5ModuleError
    from ansible.module_utils.network.f5.common import AnsibleF5Parameters
    from ansible.module_utils.network.f5.common import cleanup_tokens
    from ansible.module_utils.network.f5.common import fqdn_name
    from ansible.module_utils.network.f5.common import f5_argument_spec
    try:
        from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
    except ImportError:
        HAS_F5SDK = False


class Parameters(AnsibleF5Parameters):
    api_map = {
        'cmpHash': 'cmp_hash',
        'dagTunnel': 'dag_tunnel',
        'dagRoundRobin': 'dag_round_robin'
    }

    updatables = [
        'tagged_interfaces', 'untagged_interfaces', 'tag',
        'description', 'mtu', 'cmp_hash', 'dag_tunnel',
        'dag_round_robin'
    ]

    returnables = [
        'description', 'partition', 'tag', 'interfaces',
        'tagged_interfaces', 'untagged_interfaces', 'mtu',
        'cmp_hash', 'dag_tunnel', 'dag_round_robin'
    ]

    api_attributes = [
        'description', 'interfaces', 'tag', 'mtu', 'cmpHash',
        'dagTunnel', 'dagRoundRobin'
    ]

    def to_return(self):
        result = {}
        for returnable in self.returnables:
            result[returnable] = getattr(self, returnable)
        result = self._filter_params(result)
        return result


class ApiParameters(Parameters):
    @property
    def tagged_interfaces(self):
        if self._values['interfaces'] is None:
            return None
        result = [str(x.name) for x in self._values['interfaces'] if x.tagged is True]
        result = sorted(result)
        return result

    @property
    def untagged_interfaces(self):
        if self._values['interfaces'] is None:
            return None
        result = [str(x.name) for x in self._values['interfaces'] if x.untagged is True]
        result = sorted(result)
        return result


class ModuleParameters(Parameters):
    @property
    def untagged_interfaces(self):
        if self._values['untagged_interfaces'] is None:
            return None
        if self._values['untagged_interfaces'] is None:
            return None
        if len(self._values['untagged_interfaces']) == 1 and self._values['untagged_interfaces'][0] == '':
            return ''
        result = sorted([str(x) for x in self._values['untagged_interfaces']])
        return result

    @property
    def tagged_interfaces(self):
        if self._values['tagged_interfaces'] is None:
            return None
        if self._values['tagged_interfaces'] is None:
            return None
        if len(self._values['tagged_interfaces']) == 1 and self._values['tagged_interfaces'][0] == '':
            return ''
        result = sorted([str(x) for x in self._values['tagged_interfaces']])
        return result

    @property
    def mtu(self):
        if self._values['mtu'] is None:
            return None
        if int(self._values['mtu']) < 576 or int(self._values['mtu']) > 9198:
            raise F5ModuleError(
                "The mtu value must be between 576 - 9198"
            )
        return int(self._values['mtu'])

    @property
    def cmp_hash(self):
        if self._values['cmp_hash'] is None:
            return None
        if self._values['cmp_hash'] in ['source-address', 'src', 'src-ip', 'source']:
            return 'src-ip'
        if self._values['cmp_hash'] in ['destination-address', 'dest', 'dst-ip', 'destination', 'dst']:
            return 'dst-ip'
        else:
            return 'default'

    @property
    def dag_round_robin(self):
        if self._values['dag_round_robin'] is None:
            return None
        if self._values['dag_round_robin'] is True:
            return 'enabled'
        else:
            return 'disabled'


class Changes(Parameters):
    def to_return(self):
        result = {}
        try:
            for returnable in self.returnables:
                result[returnable] = getattr(self, returnable)
            result = self._filter_params(result)
        except Exception:
            pass
        return result


class UsableChanges(Changes):
    pass


class ReportableChanges(Changes):
    @property
    def tagged_interfaces(self):
        if self._values['interfaces'] is None:
            return None
        result = [str(x['name']) for x in self._values['interfaces'] if 'tagged' in x and x['tagged'] is True]
        result = sorted(result)
        return result

    @property
    def untagged_interfaces(self):
        if self._values['interfaces'] is None:
            return None
        result = [str(x['name']) for x in self._values['interfaces'] if 'untagged' in x and x['untagged'] is True]
        result = sorted(result)
        return result


class Difference(object):
    def __init__(self, want, have=None):
        self.want = want
        self.have = have

    def compare(self, param):
        try:
            result = getattr(self, param)
            return result
        except AttributeError:
            return self.__default(param)

    def __default(self, param):
        attr1 = getattr(self.want, param)
        try:
            attr2 = getattr(self.have, param)
            if attr1 != attr2:
                return attr1
        except AttributeError:
            return attr1

    @property
    def untagged_interfaces(self):
        result = []
        if self.want.untagged_interfaces is None:
            return None
        elif self.want.untagged_interfaces == '' and self.have.untagged_interfaces is None:
            return None
        elif self.want.untagged_interfaces == '' and len(self.have.untagged_interfaces) > 0:
            pass
        elif not self.have.untagged_interfaces:
            result = dict(
                interfaces=[dict(name=x, untagged=True) for x in self.want.untagged_interfaces]
            )
        elif set(self.want.untagged_interfaces) != set(self.have.untagged_interfaces):
            result = dict(
                interfaces=[dict(name=x, untagged=True) for x in self.want.untagged_interfaces]
            )
        else:
            return None
        return result

    @property
    def tagged_interfaces(self):
        result = []
        if self.want.tagged_interfaces is None:
            return None
        elif self.want.tagged_interfaces == '' and self.have.tagged_interfaces is None:
            return None
        elif self.want.tagged_interfaces == '' and len(self.have.tagged_interfaces) > 0:
            pass
        elif not self.have.tagged_interfaces:
            result = dict(
                interfaces=[dict(name=x, tagged=True) for x in self.want.tagged_interfaces]
            )
        elif set(self.want.tagged_interfaces) != set(self.have.tagged_interfaces):
            result = dict(
                interfaces=[dict(name=x, tagged=True) for x in self.want.tagged_interfaces]
            )
        else:
            return None
        return result


class ModuleManager(object):
    def __init__(self, *args, **kwargs):
        self.module = kwargs.get('module', None)
        self.client = kwargs.get('client', None)
        self.want = ModuleParameters(params=self.module.params)
        self.have = ApiParameters()
        self.changes = UsableChanges()

    def _update_changed_options(self):
        diff = Difference(self.want, self.have)
        updatables = Parameters.updatables
        changed = dict()
        for k in updatables:
            change = diff.compare(k)
            if change is None:
                continue
            else:
                if isinstance(change, dict):
                    changed.update(change)
                else:
                    changed[k] = change
        if changed:
            self.changes = UsableChanges(params=changed)
            return True
        return False

    def exec_module(self):
        changed = False
        result = dict()
        state = self.want.state

        try:
            if state == "present":
                changed = self.present()
            elif state == "absent":
                changed = self.absent()
        except iControlUnexpectedHTTPError as e:
            raise F5ModuleError(str(e))

        reportable = ReportableChanges(params=self.changes.to_return())
        changes = reportable.to_return()
        result.update(**changes)
        result.update(dict(changed=changed))
        self._announce_deprecations(result)
        return result

    def _announce_deprecations(self, result):
        warnings = result.pop('__warnings', [])
        for warning in warnings:
            self.module.deprecate(
                msg=warning['msg'],
                version=warning['version']
            )

    def present(self):
        if self.exists():
            return self.update()
        else:
            return self.create()

    def absent(self):
        if self.exists():
            return self.remove()
        return False

    def should_update(self):
        result = self._update_changed_options()
        if result:
            return True
        return False

    def update(self):
        self.have = self.read_current_from_device()
        if not self.should_update():
            return False
        if self.module.check_mode:
            return True
        self.update_on_device()
        return True

    def remove(self):
        if self.module.check_mode:
            return True
        self.remove_from_device()
        if self.exists():
            raise F5ModuleError("Failed to delete the VLAN")
        return True

    def create(self):
        self.have = ApiParameters()
        if self.want.mtu is None:
            self.want.update({'mtu': 1500})
        self._update_changed_options()
        if self.module.check_mode:
            return True
        self.create_on_device()
        return True

    def create_on_device(self):
        params = self.changes.api_params()
        self.client.api.tm.net.vlans.vlan.create(
            name=self.want.name,
            partition=self.want.partition,
            **params
        )

    def update_on_device(self):
        params = self.changes.api_params()
        resource = self.client.api.tm.net.vlans.vlan.load(
            name=self.want.name,
            partition=self.want.partition
        )
        resource.modify(**params)

    def exists(self):
        return self.client.api.tm.net.vlans.vlan.exists(
            name=self.want.name,
            partition=self.want.partition
        )

    def remove_from_device(self):
        resource = self.client.api.tm.net.vlans.vlan.load(
            name=self.want.name,
            partition=self.want.partition
        )
        if resource:
            resource.delete()

    def read_current_from_device(self):
        resource = self.client.api.tm.net.vlans.vlan.load(
            name=self.want.name, partition=self.want.partition
        )
        interfaces = resource.interfaces_s.get_collection()
        result = resource.attrs
        result['interfaces'] = interfaces
        return ApiParameters(params=result)


class ArgumentSpec(object):
    def __init__(self):
        self.supports_check_mode = True
        argument_spec = dict(
            name=dict(
                required=True,
            ),
            tagged_interfaces=dict(
                type='list',
                aliases=['tagged_interface']
            ),
            untagged_interfaces=dict(
                type='list',
                aliases=['untagged_interface']
            ),
            description=dict(),
            tag=dict(
                type='int'
            ),
            mtu=dict(type='int'),
            cmp_hash=dict(
                choices=[
                    'default',
                    'destination-address', 'dest', 'dst-ip', 'destination', 'dst',
                    'source-address', 'src', 'src-ip', 'source'
                ]
            ),
            dag_tunnel=dict(
                choices=['inner', 'outer']
            ),
            dag_round_robin=dict(type='bool'),
            state=dict(
                default='present',
                choices=['present', 'absent']
            ),
            partition=dict(
                default='Common',
                fallback=(env_fallback, ['F5_PARTITION'])
            )
        )
        self.argument_spec = {}
        self.argument_spec.update(f5_argument_spec)
        self.argument_spec.update(argument_spec)
        self.mutually_exclusive = [
            ['tagged_interfaces', 'untagged_interfaces']
        ]


def main():
    spec = ArgumentSpec()

    module = AnsibleModule(
        argument_spec=spec.argument_spec,
        supports_check_mode=spec.supports_check_mode,
        mutually_exclusive=spec.mutually_exclusive
    )
    if not HAS_F5SDK:
        module.fail_json(msg="The python f5-sdk module is required")

    try:
        client = F5Client(**module.params)
        mm = ModuleManager(module=module, client=client)
        results = mm.exec_module()
        cleanup_tokens(client)
        module.exit_json(**results)
    except F5ModuleError as e:
        cleanup_tokens(client)
        module.fail_json(msg=str(e))


if __name__ == '__main__':
    main()
