#!/usr/bin/python # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # This module is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this software. If not, see . try: import shade HAS_SHADE = True except ImportError: HAS_SHADE = False DOCUMENTATION = ''' --- module: os_port short_description: Add/Update/Delete ports from an OpenStack cloud. extends_documentation_fragment: openstack author: "Davide Agnello (@dagnello)" version_added: "2.0" description: - Add, Update or Remove ports from an OpenStack cloud. A state=present, will ensure the port is created or updated if required. options: network: description: - Network ID or name this port belongs to. required: true name: description: - Name that has to be given to the port. required: false default: None fixed_ips: description: - Desired IP and/or subnet for this port. Subnet is referenced by subnet_id and IP is referenced by ip_address. required: false default: None admin_state_up: description: - Sets admin state. required: false default: None mac_address: description: - MAC address of this port. required: false default: None security_groups: description: - Security group(s) ID(s) or name(s) associated with the port (comma separated string or YAML list) required: false default: None no_security_groups: description: - Do not associate a security group with this port. required: false default: False 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: ..." required: false default: None extra_dhcp_opt: description: - "Extra dhcp options to be assigned to this port. Extra options are supported with dictionary structure. e.g. extra_dhcp_opt: - opt_name: opt name1 opt_value: value1 - opt_name: ..." required: false default: None device_owner: description: - The ID of the entity that uses this port. required: false default: None device_id: description: - Device ID of device using this port. required: false default: None state: description: - Should the resource be present or absent. choices: [present, absent] default: present ''' EXAMPLES = ''' # Create a port - os_port: state: present auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ username: admin password: admin project_name: admin name: port1 network: foo # Create a port with a static IP - os_port: state: present auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ 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 - os_port: state: present auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ 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) - os_port: state: present auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/d 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) - os_port: state: present auth: auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/d username: admin password: admin project_name: admin name: port1 security_groups: - 1496e8c7-4918-482a-9172-f4f00fc4a3a5 - 057d4bdf-6d4d-472... ''' RETURN = ''' id: description: Unique UUID. returned: success type: string name: description: Name given to the port. returned: success type: string network_id: description: Network ID this port belongs in. returned: success type: string security_groups: description: Security group(s) associated with this port. returned: success type: list of strings status: description: Port's status. returned: success type: string fixed_ips: description: Fixed ip(s) associated with this port. returned: success type: list of dicts tenant_id: description: Tenant id associated with this port. returned: success type: string allowed_address_pairs: description: Allowed address pairs with this port. returned: success type: list of dicts admin_state_up: description: Admin state up flag for this port. returned: success type: bool ''' 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'] compare_dict = ['allowed_address_pairs', 'extra_dhcp_opt'] 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_dict: if module.params[key] is not None and cmp(module.params[key], port[key]) != 0: return True for key in compare_list: if module.params[key] is not None and (set(module.params[key]) != set(port[key])): 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_opt', 'device_owner', 'device_id'] 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), admin_state_up=dict(type='bool', default=None), mac_address=dict(default=None), security_groups=dict(default=None, type='list'), no_security_groups=dict(default=False, type='bool'), allowed_address_pairs=dict(type='dict', default=None), extra_dhcp_opt=dict(type='dict', default=None), device_owner=dict(default=None), device_id=dict(default=None), state=dict(default='present', choices=['absent', 'present']), ) 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_SHADE: module.fail_json(msg='shade is required for this module') name = module.params['name'] state = module.params['state'] try: cloud = shade.openstack_cloud(**module.params) 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'] ] 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 shade.OpenStackCloudException as e: module.fail_json(msg=str(e)) # this is magic, see lib/ansible/module_common.py from ansible.module_utils.basic import * from ansible.module_utils.openstack import * if __name__ == '__main__': main()