94ed95c8b6
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
701 lines
24 KiB
Python
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()
|