ansible-collections-openstack/plugins/modules/port.py
Simon Hensel 94ed95c8b6 Fix port_security_enabled key for port module
Changes to the port_security_enabled parameter are not applied due to
mismatching key names.
In the port module, the input parameter is called `port_security_enabled`,
while the OpenStackSDK is using a field called `is_port_security_enabled`.

When updating an existing port, the port module is comparing the dictionary
keys of the Ansible module parameters with those of the port object
returned by the OpenStackSDK.
Since these keys different, they will not match and changes to
port security are not applied.

Story: 2010687
Task: 47789
Change-Id: I838e9d6ebf1a281269add91724eac240abe35fd4
2023-10-17 08:56:21 +02:00

701 lines
24 KiB
Python

#!/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()