#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = '''
---
module: port
short_description: Add/Update/Delete ports from an OpenStack cloud.
author: OpenStack Ansible SIG
description:
   - Add, Update or Remove ports from an OpenStack cloud.
options:
    allowed_address_pairs:
        description:
          - "Allowed address pairs list. Allowed address pairs are supported
            with dictionary structure.
            e.g.  allowed_address_pairs:
                    - ip_address: 10.1.0.12
                      mac_address: ab:cd:ef:12:34:56
                    - ip_address: ..."
          - The port will change during update if not all suboptions are
            specified, e.g. when ip_address is given but mac_address is not.
        type: list
        elements: dict
        suboptions:
            ip_address:
                description: The IP address.
                type: str
            mac_address:
                description: The MAC address.
                type: str
    binding_profile:
        description:
          - Binding profile dict that the port should be created with.
        type: dict
    binding_vnic_type:
        description:
          - The type of the port that should be created
        choices: [normal,
                  direct,
                  direct-physical,
                  macvtap,
                  baremetal,
                  virtio-forwarder]
        type: str
        aliases: ['vnic_type']
    description:
        description:
          - Description of the port.
        type: str
    device_id:
        description:
          - Device ID of device using this port.
        type: str
    device_owner:
        description:
           - The ID of the entity that uses this port.
        type: str
    dns_domain:
        description:
          - The dns domain of the port ( only with dns-integration enabled )
        type: str
    dns_name:
        description:
          - The dns name of the port ( only with dns-integration enabled )
        type: str
    extra_dhcp_opts:
        description:
          - "Extra dhcp options to be assigned to this port. Extra options are
            supported with dictionary structure. Note that options cannot be
            removed only updated.
            e.g.  extra_dhcp_opts:
                    - ip_version: 4
                      opt_name: bootfile-name
                      opt_value: pxelinux.0
                    - opt_name: ..."
          - The port will change during update if not all suboptions are
            specified, e.g. when opt_name is given but ip_version is not.
        type: list
        elements: dict
        suboptions:
            ip_version:
                description: The IP version this DHCP option is for.
                type: int
                required: true
            opt_name:
                description: The name of the DHCP option to set.
                type: str
                required: true
            opt_value:
                description: The value of the DHCP option to set.
                type: str
                required: true
    fixed_ips:
        description:
          - Desired IP and/or subnet for this port.  Subnet is referenced by
            subnet_id and IP is referenced by ip_address.
          - The port will change during update if not all suboptions are
            specified, e.g. when ip_address is given but subnet_id is not.
        type: list
        elements: dict
        suboptions:
            ip_address:
                description: The fixed IP address to attempt to allocate.
                required: true
                type: str
            subnet_id:
                description: The subnet to attach the IP address to.
                type: str
    is_admin_state_up:
        description:
          - Sets admin state.
        type: bool
        aliases: ['admin_state_up']
    mac_address:
        description:
          - MAC address of this port.
        type: str
    name:
        description:
          - Name that has to be given to the port.
          - This port attribute cannot be updated.
        type: str
        required: true
    network:
        description:
          - ID or name of the network this port belongs to.
          - Required when creating a new port.
          - Must be a name when creating a port.
          - This port attribute cannot be updated.
        type: str
    no_security_groups:
        description:
          - Do not associate a security group with this port.
          - "Deprecated. Use I(security_groups): C([]) instead
            of I(no_security_groups): C(true)."
        type: bool
        default: 'false'
    is_port_security_enabled:
        description:
          - Whether to enable or disable the port security on the network.
        type: bool
        aliases: ['port_security_enabled']
    security_groups:
        description:
          - Security group(s) ID(s) or name(s) associated with the port.
        type: list
        elements: str
    state:
        description:
          - Should the resource be present or absent.
        choices: [present, absent]
        default: present
        type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''

EXAMPLES = '''
# Create a port
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    network: foo

# Create a port with a static IP
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    network: foo
    fixed_ips:
      - ip_address: 10.1.0.21

# Create a port with No security groups
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    network: foo
    no_security_groups: True

# Update the existing 'port1' port with multiple security groups (version 1)
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    security_groups: 1496e8c7-4918-482a-9172-f4f00fc4a3a5,057d4bdf-6d4d-472...

# Update the existing 'port1' port with multiple security groups (version 2)
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    security_groups:
      - 1496e8c7-4918-482a-9172-f4f00fc4a3a5
      - 057d4bdf-6d4d-472...

# Create port of type 'direct'
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    network: foo
    binding_vnic_type: direct

# Create a port with binding profile
- openstack.cloud.port:
    state: present
    auth:
      auth_url: https://identity.example.com
      username: admin
      password: admin
      project_name: admin
    name: port1
    network: foo
    binding_profile:
      pci_slot: "0000:03:11.1"
      physical_network: "provider"
'''

RETURN = '''
port:
    description: Dictionary describing the port.
    type: dict
    returned: On success when I(state) is C(present).
    contains:
        allowed_address_pairs:
            description: Allowed address pairs.
            returned: success
            type: list
            sample: []
        binding_host_id:
            description: |
                The ID of the host where the port is allocated. In some cases,
                different implementations can run on different hosts.
            returned: success
            type: str
            sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759"
        binding_profile:
            description: |
                A dictionary the enables the application running on the
                specified host to pass and receive vif port-specific
                information to the plug-in.
            returned: success
            type: dict
            sample: {}
        binding_vif_details:
            description: |
                A dictionary that enables the application to pass
                information about functions that the Networking API provides.
            returned: success
            type: dict
        binding_vif_type:
            description: The VIF type for the port.
            returned: success
            type: dict
        binding_vnic_type:
            description: |
                The virtual network interface card (vNIC) type that is
                bound to the neutron port.
            returned: success
            type: str
            sample: "normal"
        created_at:
            description: Timestamp when the port was created.
            returned: success
            type: str
            sample: "2022-02-03T13:28:25Z"
        data_plane_status:
            description: Status of the underlying data plane of a port.
            returned: success
            type: str
        description:
            description: The port description.
            returned: success
            type: str
        device_id:
            description: Device ID of this port.
            returned: success
            type: str
            sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759"
        device_owner:
            description: Device owner of this port, e.g. C(network:dhcp).
            returned: success
            type: str
            sample: "network:router_interface"
        device_profile:
            description: |
                Device profile of this port, refers to Cyborg device-profiles:
                https://docs.openstack.org/api-ref/accelerator/v2/index.html#
                device-profiles.
            returned: success
            type: str
        dns_assignment:
            description: DNS assignment for the port.
            returned: success
            type: list
        dns_domain:
            description: DNS domain assigned to the port.
            returned: success
            type: str
        dns_name:
            description: DNS name for the port.
            returned: success
            type: str
        extra_dhcp_opts:
            description: |
                A set of zero or more extra DHCP option pairs.
                An option pair consists of an option value and name.
            returned: success
            type: list
            sample: []
        fixed_ips:
            description: |
                IP addresses for the port. Includes the IP address and subnet
                ID.
            returned: success
            type: list
        id:
            description: The port ID.
            returned: success
            type: str
            sample: "3ec25c97-7052-4ab8-a8ba-92faf84148de"
        ip_allocation:
            description: |
                The ip_allocation indicates when ports use deferred,
                immediate or no IP allocation.
            returned: success
            type: str
        is_admin_state_up:
            description: |
                The administrative state of the port, which is up C(True) or
                down C(False).
            returned: success
            type: bool
            sample: true
        is_port_security_enabled:
            description: |
                The port security status, which is enabled C(True) or disabled
                C(False).
            returned: success
            type: bool
            sample: false
        mac_address:
            description: The MAC address of an allowed address pair.
            returned: success
            type: str
            sample: "00:00:5E:00:53:42"
        name:
            description: The port name.
            returned: success
            type: str
            sample: "port_name"
        network_id:
            description: The ID of the attached network.
            returned: success
            type: str
            sample: "dd1ede4f-3952-4131-aab6-3b8902268c7d"
        numa_affinity_policy:
            description: |
                The NUMA affinity policy defined for this port.
            returned: success
            type: str
            sample: "required"
        project_id:
            description: The ID of the project who owns the network.
            returned: success
            type: str
            sample: "aa1ede4f-3952-4131-aab6-3b8902268c7d"
        propagate_uplink_status:
            description: Whether to propagate uplink status of the port.
            returned: success
            type: bool
            sample: false
        qos_network_policy_id:
            description: |
                The ID of the QoS policy attached to the network where the
                port is bound.
            returned: success
            type: str
            sample: "1e4f3958-c0c9-4dec-82fa-ed2dc1c5cb34"
        qos_policy_id:
            description: The ID of the QoS policy attached to the port.
            returned: success
            type: str
            sample: "b20bb47f-5d6d-45a6-8fe7-2c1b44f0db73"
        resource_request:
            description: |
                The port-resource-request exposes Placement resources
                (i.e.: minimum-bandwidth) and traits (i.e.: vnic-type, physnet)
                requested by a port to Nova and Placement.
            returned: success
            type: str
        revision_number:
            description: The revision number of the resource.
            returned: success
            type: int
            sample: 0
        security_group_ids:
            description: The IDs of any attached security groups.
            returned: success
            type: list
        status:
            description: The port status. Value is C(ACTIVE) or C(DOWN).
            returned: success
            type: str
            sample: "ACTIVE"
        tags:
            description: The list of tags on the resource.
            returned: success
            type: list
            sample: []
        tenant_id:
            description: Same as I(project_id). Deprecated.
            returned: success
            type: str
            sample: "51fce036d7984ba6af4f6c849f65ef00"
        trunk_details:
            description: |
                The trunk referring to this parent port and its subports.
                Present for trunk parent ports if C(trunk-details) extension
                is loaded.
            returned: success
            type: dict
        updated_at:
            description: Timestamp when the port was last updated.
            returned: success
            type: str
            sample: "2022-02-03T13:28:25Z"
'''

from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule


class PortModule(OpenStackModule):
    argument_spec = dict(
        allowed_address_pairs=dict(type='list', elements='dict'),
        binding_profile=dict(type='dict'),
        binding_vnic_type=dict(choices=['normal', 'direct', 'direct-physical',
                                        'macvtap', 'baremetal',
                                        'virtio-forwarder'],
                               aliases=['vnic_type']),
        description=dict(),
        device_id=dict(),
        device_owner=dict(),
        dns_domain=dict(),
        dns_name=dict(),
        extra_dhcp_opts=dict(type='list', elements='dict'),
        fixed_ips=dict(type='list', elements='dict'),
        is_admin_state_up=dict(type='bool', aliases=['admin_state_up']),
        mac_address=dict(),
        name=dict(required=True),
        network=dict(),
        no_security_groups=dict(default=False, type='bool'),
        is_port_security_enabled=dict(type='bool', aliases=['port_security_enabled']),
        security_groups=dict(type='list', elements='str'),
        state=dict(default='present', choices=['absent', 'present']),
    )

    module_kwargs = dict(
        mutually_exclusive=[
            ['no_security_groups', 'security_groups'],
        ],
        required_if=[
            ('state', 'present', ('network',)),
        ],
        supports_check_mode=True
    )

    def run(self):
        network_name_or_id = self.params['network']
        port_name_or_id = self.params['name']
        state = self.params['state']

        network = None
        if network_name_or_id:
            network = self.conn.network.find_network(
                network_name_or_id, ignore_missing=False)

        port = self.conn.network.find_port(
            port_name_or_id,
            # use network id in query if network parameter was specified
            **(dict(network_id=network.id) if network else dict()))

        if self.ansible.check_mode:
            self.exit_json(changed=self._will_change(network, port, state))

        if state == 'present' and not port:
            # create port
            port = self._create(network)
            self.exit_json(changed=True,
                           port=port.to_dict(computed=False))
        elif state == 'present' and port:
            # update port
            update = self._build_update(port)
            if update:
                port = self._update(port, update)

            self.exit_json(changed=bool(update),
                           port=port.to_dict(computed=False))
        elif state == 'absent' and port:
            # delete port
            self._delete(port)
            self.exit_json(changed=True)
        elif state == 'absent' and not port:
            # do nothing
            self.exit_json(changed=False)

    def _build_update(self, port):
        update = {}

        # A port's name cannot be updated by this module because
        # it is used to find ports by name or id.
        # If name is an id, then we do not have a name to update.
        # If name is a name actually, then it was used to find a
        # matching port hence the name is the user defined one
        # already.

        # updateable port attributes in openstacksdk
        # (OpenStack API names in braces):
        # - allowed_address_pairs (allowed_address_pairs)
        # - binding_host_id (binding:host_id)
        # - binding_profile (binding:profile)
        # - binding_vnic_type (binding:vnic_type)
        # - data_plane_status (data_plane_status)
        # - description (description)
        # - device_id (device_id)
        # - device_owner (device_owner)
        # (- device_profile (device_profile))
        # - dns_domain (dns_domain)
        # - dns_name (dns_name)
        # - extra_dhcp_opts (extra_dhcp_opts)
        # - fixed_ips (fixed_ips)
        # - is_admin_state_up (admin_state_up)
        # - is_port_security_enabled (port_security_enabled)
        # - mac_address (mac_address)
        # - name (name)
        # - numa_affinity_policy (numa_affinity_policy)
        # - qos_policy_id (qos_policy_id)
        # - security_group_ids (security_groups)
        # Ref.: https://docs.openstack.org/api-ref/network/v2/index.html#update-port

        # Update all known updateable attributes although
        # our module might not support them yet

        # Update attributes which can be compared straight away
        port_attributes = dict(
            (k, self.params[k])
            for k in ['binding_host_id', 'binding_vnic_type',
                      'data_plane_status', 'description', 'device_id',
                      'device_owner', 'is_admin_state_up',
                      'is_port_security_enabled', 'mac_address',
                      'numa_affinity_policy']
            if k in self.params and self.params[k] is not None
            and self.params[k] != port[k])

        # Compare dictionaries
        for k in ['binding_profile']:
            if self.params[k] is None:
                continue

            if (self.params[k] or port[k]) \
               and self.params[k] != port[k]:
                port_attributes[k] = self.params[k]

        # Attribute qos_policy_id is not supported by this module and would
        # need special handling using self.conn.network.find_qos_policy()

        # Compare attributes which are lists of dictionaries
        for k in ['allowed_address_pairs', 'extra_dhcp_opts', 'fixed_ips']:
            if self.params[k] is None:
                continue

            if (self.params[k] or port[k]) \
               and self.params[k] != port[k]:
                port_attributes[k] = self.params[k]

        # Compare security groups
        if self.params['no_security_groups']:
            security_group_ids = []
        elif self.params['security_groups'] is not None:
            security_group_ids = [
                self.conn.network.find_security_group(
                    security_group_name_or_id, ignore_missing=False).id
                for security_group_name_or_id in self.params['security_groups']
            ]
        else:
            security_group_ids = None

        if security_group_ids is not None \
           and set(security_group_ids) != set(port['security_group_ids']):
            port_attributes['security_group_ids'] = security_group_ids

        # Compare dns attributes
        if self.conn.has_service('dns') and \
           self.conn.network.find_extension('dns-integration'):
            port_attributes.update(dict(
                (k, self.params[k])
                for k in ['dns_name', 'dns_domain']
                if self.params[k] is not None and self.params[k] != port[k]
            ))

        if port_attributes:
            update['port_attributes'] = port_attributes
        return update

    def _create(self, network):
        args = {}
        args['network_id'] = network.id

        # Fetch IDs of security groups next to fail early
        # if any security group does not exist
        if self.params['no_security_groups']:
            args['security_group_ids'] = []
        elif self.params['security_groups'] is not None:
            args['security_group_ids'] = [
                self.conn.network.find_security_group(
                    security_group_name_or_id, ignore_missing=False).id
                for security_group_name_or_id in self.params['security_groups']
            ]

        for k in ['allowed_address_pairs',
                  'binding_profile',
                  'binding_vnic_type',
                  'device_id',
                  'device_owner',
                  'description',
                  'extra_dhcp_opts',
                  'is_admin_state_up',
                  'mac_address',
                  'is_port_security_enabled',
                  'fixed_ips',
                  'name']:
            if self.params[k] is not None:
                args[k] = self.params[k]

        if self.conn.has_service('dns') \
           and self.conn.network.find_extension('dns-integration'):
            for k in ['dns_domain', 'dns_name']:
                if self.params[k] is not None:
                    args[k] = self.params[k]

        return self.conn.network.create_port(**args)

    def _delete(self, port):
        self.conn.network.delete_port(port.id)

    def _update(self, port, update):
        port_attributes = update.get('port_attributes')
        if port_attributes:
            port = self.conn.network.update_port(port, **port_attributes)
        return port

    def _will_change(self, port, state):
        if state == 'present' and not port:
            return True
        elif state == 'present' and port:
            return bool(self._build_update(port))
        elif state == 'absent' and port:
            return True
        else:
            # state == 'absent' and not port:
            return False


def main():
    module = PortModule()
    module()


if __name__ == '__main__':
    main()
