f71542ccf2
A change is coming to Ansible where module params will default to str. Many of our modules were taking advantage of this by not being explicit about the type, so they will break when that change merges. This hopefully catches those cases.
392 lines
12 KiB
Python
392 lines
12 KiB
Python
#!/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 <http://www.gnu.org/licenses/>.
|
|
|
|
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()
|