#!/usr/bin/python # 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: "Davide Agnello (@dagnello)" description: - Add, Update or Remove ports from an OpenStack cloud. A I(state) of 'present' will ensure the port is created or updated if required. options: network: description: - Network ID or name this port belongs to. - Required when creating a new port. type: str name: description: - Name that has to be given to the port. type: str fixed_ips: description: - Desired IP and/or subnet for this port. Subnet is referenced by subnet_id and IP is referenced by ip_address. 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 admin_state_up: description: - Sets admin state. type: bool mac_address: description: - MAC address of this port. type: str security_groups: description: - Security group(s) ID(s) or name(s) associated with the port (comma separated string or YAML list) type: list elements: str no_security_groups: description: - Do not associate a security group with this port. type: bool default: 'no' 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: ..." type: list elements: dict suboptions: ip_address: description: The IP address. type: str mac_address: description: The MAC address. 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: - opt_name: opt name1 opt_value: value1 ip_version: 4 - opt_name: ..." type: list elements: dict suboptions: 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 ip_version: description: The IP version this DHCP option is for. type: int required: true device_owner: description: - The ID of the entity that uses this port. type: str device_id: description: - Device ID of device using this port. type: str state: description: - Should the resource be present or absent. choices: [present, absent] default: present type: str vnic_type: description: - The type of the port that should be created choices: [normal, direct, direct-physical, macvtap, baremetal, virtio-forwarder] type: str port_security_enabled: description: - Whether to enable or disable the port security on the network. type: bool requirements: - "python >= 3.6" - "openstacksdk" 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 vnic_type: direct ''' RETURN = ''' id: description: Unique UUID. returned: success type: str name: description: Name given to the port. returned: success type: str network_id: description: Network ID this port belongs in. returned: success type: str security_groups: description: Security group(s) associated with this port. returned: success type: list status: description: Port's status. returned: success type: str fixed_ips: description: Fixed ip(s) associated with this port. returned: success type: list tenant_id: description: Tenant id associated with this port. returned: success type: str allowed_address_pairs: description: Allowed address pairs with this port. returned: success type: list admin_state_up: description: Admin state up flag for this port. returned: success type: bool vnic_type: description: Type of the created port returned: success type: str port_security_enabled: description: Port security state on the network. returned: success type: bool ''' from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.openstack.cloud.plugins.module_utils.openstack import (openstack_full_argument_spec, openstack_module_kwargs, openstack_cloud_from_module) try: from collections import OrderedDict HAS_ORDEREDDICT = True except ImportError: try: from ordereddict import OrderedDict HAS_ORDEREDDICT = True except ImportError: HAS_ORDEREDDICT = False def _needs_update(module, port, cloud): """Check for differences in the updatable values. NOTE: We don't currently allow name updates. """ compare_simple = ['admin_state_up', 'mac_address', 'device_owner', 'device_id', 'binding:vnic_type', 'port_security_enabled'] compare_list_dict = ['allowed_address_pairs', 'extra_dhcp_opts'] compare_list = ['security_groups'] for key in compare_simple: if module.params[key] is not None and module.params[key] != port[key]: return True for key in compare_list: if ( module.params[key] is not None and set(module.params[key]) != set(port[key]) ): return True for key in compare_list_dict: if not module.params[key]: if not port[key]: return True # sort dicts in list port_ordered = [OrderedDict(sorted(d.items())) for d in port[key]] param_ordered = [OrderedDict(sorted(d.items())) for d in module.params[key]] for d in param_ordered: if d not in port_ordered: return True for d in port_ordered: if d not in param_ordered: return True # NOTE: if port was created or updated with 'no_security_groups=True', # subsequent updates without 'no_security_groups' flag or # 'no_security_groups=False' and no specified 'security_groups', will not # result in an update to the port where the default security group is # applied. if module.params['no_security_groups'] and port['security_groups'] != []: return True if module.params['fixed_ips'] is not None: for item in module.params['fixed_ips']: if 'ip_address' in item: # if ip_address in request does not match any in existing port, # update is required. if not any(match['ip_address'] == item['ip_address'] for match in port['fixed_ips']): return True if 'subnet_id' in item: return True for item in port['fixed_ips']: # if ip_address in existing port does not match any in request, # update is required. if not any(match.get('ip_address') == item['ip_address'] for match in module.params['fixed_ips']): return True return False def _system_state_change(module, port, cloud): state = module.params['state'] if state == 'present': if not port: return True return _needs_update(module, port, cloud) if state == 'absent' and port: return True return False def _compose_port_args(module, cloud): port_kwargs = {} optional_parameters = ['name', 'fixed_ips', 'admin_state_up', 'mac_address', 'security_groups', 'allowed_address_pairs', 'extra_dhcp_opts', 'device_owner', 'device_id', 'binding:vnic_type', 'port_security_enabled'] for optional_param in optional_parameters: if module.params[optional_param] is not None: port_kwargs[optional_param] = module.params[optional_param] if module.params['no_security_groups']: port_kwargs['security_groups'] = [] return port_kwargs def get_security_group_id(module, cloud, security_group_name_or_id): security_group = cloud.get_security_group(security_group_name_or_id) if not security_group: module.fail_json(msg="Security group: %s, was not found" % security_group_name_or_id) return security_group['id'] def main(): argument_spec = openstack_full_argument_spec( network=dict(required=False), name=dict(required=False), fixed_ips=dict(type='list', default=None, elements='dict'), admin_state_up=dict(type='bool', default=None), mac_address=dict(default=None), security_groups=dict(default=None, type='list', elements='str'), no_security_groups=dict(default=False, type='bool'), allowed_address_pairs=dict(type='list', default=None, elements='dict'), extra_dhcp_opts=dict(type='list', default=None, elements='dict'), device_owner=dict(default=None), device_id=dict(default=None), state=dict(default='present', choices=['absent', 'present']), vnic_type=dict(default=None, choices=['normal', 'direct', 'direct-physical', 'macvtap', 'baremetal', 'virtio-forwarder']), port_security_enabled=dict(default=None, type='bool') ) module_kwargs = openstack_module_kwargs( mutually_exclusive=[ ['no_security_groups', 'security_groups'], ] ) module = AnsibleModule(argument_spec, supports_check_mode=True, **module_kwargs) if not HAS_ORDEREDDICT: module.fail_json(msg=missing_required_lib('ordereddict')) name = module.params['name'] state = module.params['state'] sdk, cloud = openstack_cloud_from_module(module) try: if module.params['security_groups']: # translate security_groups to UUID's if names where provided module.params['security_groups'] = [ get_security_group_id(module, cloud, v) for v in module.params['security_groups'] ] # Neutron API accept 'binding:vnic_type' as an argument # for the port type. module.params['binding:vnic_type'] = module.params.pop('vnic_type') port = None network_id = None if name: port = cloud.get_port(name) if module.check_mode: module.exit_json(changed=_system_state_change(module, port, cloud)) changed = False if state == 'present': if not port: network = module.params['network'] if not network: module.fail_json( msg="Parameter 'network' is required in Port Create" ) port_kwargs = _compose_port_args(module, cloud) network_object = cloud.get_network(network) if network_object: network_id = network_object['id'] else: module.fail_json( msg="Specified network was not found." ) port = cloud.create_port(network_id, **port_kwargs) changed = True else: if _needs_update(module, port, cloud): port_kwargs = _compose_port_args(module, cloud) port = cloud.update_port(port['id'], **port_kwargs) changed = True module.exit_json(changed=changed, id=port['id'], port=port) if state == 'absent': if port: cloud.delete_port(port['id']) changed = True module.exit_json(changed=changed) except sdk.exceptions.OpenStackCloudException as e: module.fail_json(msg=str(e)) if __name__ == '__main__': main()