mirror of https://github.com/ansible/ansible.git
Merge branch 'devel' into feature/add_ec2_elb_lb_idle_timeout
* devel: (84 commits) Document and return an error if httplib2 >= 0.7 is not present. We since find doesn't make changes, support check mode and gather data for other tasks in check mode Correct typo in yum module docs Update doc to reflect password is required if adding a new user Update error message to be more explicit Simplify logic to handle options set to empty string Fix to issue 12912. Supply 'force' to install of python-apt. Note the difference between yum package groups and environment groups. rearranged systemd check, removed redundant systemctl check fixed unused cmd and state var assignements added earlier paths to systemd make os_router return a top level 'id' key Version bump for new beta 2.0.0-0.4.beta2 allow os_port to accept a list of security groups allow os_server to accept a list of security groups Add capability for stat module to use more hash algorithms allow empty description attribute for os_security_group Update hostname.py simpler way to check if systemd is the init system make os_keypair return a top level 'id' key make os_flavor return a top-level 'id' key ...reviewable/pr18780/r1
commit
d4319555a0
@ -0,0 +1,392 @@
|
||||
#!/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(default=None),
|
||||
admin_state_up=dict(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(default=None),
|
||||
extra_dhcp_opt=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=e.message)
|
||||
|
||||
# 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()
|
@ -0,0 +1,154 @@
|
||||
#!/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_subnets_facts
|
||||
short_description: Retrieve facts about one or more OpenStack subnets.
|
||||
version_added: "2.0"
|
||||
author: "Davide Agnello (@dagnello)"
|
||||
description:
|
||||
- Retrieve facts about one or more subnets from OpenStack.
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
options:
|
||||
subnet:
|
||||
description:
|
||||
- Name or ID of the subnet
|
||||
required: false
|
||||
filters:
|
||||
description:
|
||||
- A dictionary of meta data to use for further filtering. Elements of
|
||||
this dictionary may be additional dictionaries.
|
||||
required: false
|
||||
extends_documentation_fragment: openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Gather facts about previously created subnets
|
||||
- os_subnets_facts:
|
||||
auth:
|
||||
auth_url: https://your_api_url.com:9000/v2.0
|
||||
username: user
|
||||
password: password
|
||||
project_name: someproject
|
||||
- debug: var=openstack_subnets
|
||||
|
||||
# Gather facts about a previously created subnet by name
|
||||
- os_subnets_facts:
|
||||
auth:
|
||||
auth_url: https://your_api_url.com:9000/v2.0
|
||||
username: user
|
||||
password: password
|
||||
project_name: someproject
|
||||
name: subnet1
|
||||
- debug: var=openstack_subnets
|
||||
|
||||
# Gather facts about a previously created subnet with filter (note: name and
|
||||
filters parameters are Not mutually exclusive)
|
||||
- os_subnets_facts:
|
||||
auth:
|
||||
auth_url: https://your_api_url.com:9000/v2.0
|
||||
username: user
|
||||
password: password
|
||||
project_name: someproject
|
||||
filters:
|
||||
tenant_id: 55e2ce24b2a245b09f181bf025724cbe
|
||||
- debug: var=openstack_subnets
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
openstack_subnets:
|
||||
description: has all the openstack facts about the subnets
|
||||
returned: always, but can be null
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: Unique UUID.
|
||||
returned: success
|
||||
type: string
|
||||
name:
|
||||
description: Name given to the subnet.
|
||||
returned: success
|
||||
type: string
|
||||
network_id:
|
||||
description: Network ID this subnet belongs in.
|
||||
returned: success
|
||||
type: string
|
||||
cidr:
|
||||
description: Subnet's CIDR.
|
||||
returned: success
|
||||
type: string
|
||||
gateway_ip:
|
||||
description: Subnet's gateway ip.
|
||||
returned: success
|
||||
type: string
|
||||
enable_dhcp:
|
||||
description: DHCP enable flag for this subnet.
|
||||
returned: success
|
||||
type: bool
|
||||
ip_version:
|
||||
description: IP version for this subnet.
|
||||
returned: success
|
||||
type: int
|
||||
tenant_id:
|
||||
description: Tenant id associated with this subnet.
|
||||
returned: success
|
||||
type: string
|
||||
dns_nameservers:
|
||||
description: DNS name servers for this subnet.
|
||||
returned: success
|
||||
type: list of strings
|
||||
allocation_pools:
|
||||
description: Allocation pools associated with this subnet.
|
||||
returned: success
|
||||
type: list of dicts
|
||||
'''
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name=dict(required=False, default=None),
|
||||
filters=dict(required=False, default=None)
|
||||
)
|
||||
module = AnsibleModule(argument_spec)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
subnets = cloud.search_subnets(module.params['name'],
|
||||
module.params['filters'])
|
||||
module.exit_json(changed=False, ansible_facts=dict(
|
||||
openstack_subnets=subnets))
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# 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()
|
@ -0,0 +1,212 @@
|
||||
#!/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_user
|
||||
short_description: Manage OpenStack Identity Users
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Manage OpenStack Identity users. Users can be created,
|
||||
updated or deleted using this module. A user will be updated
|
||||
if I(name) matches an existing user and I(state) is present.
|
||||
The value for I(name) cannot be updated without deleting and
|
||||
re-creating the user.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Username for the user
|
||||
required: true
|
||||
password:
|
||||
description:
|
||||
- Password for the user
|
||||
required: true when I(state) is present
|
||||
default: None
|
||||
email:
|
||||
description:
|
||||
- Email address for the user
|
||||
required: false
|
||||
default: None
|
||||
default_project:
|
||||
description:
|
||||
- Project name or ID that the user should be associated with by default
|
||||
required: false
|
||||
default: None
|
||||
domain:
|
||||
description:
|
||||
- Domain to create the user in if the cloud supports domains
|
||||
required: false
|
||||
default: None
|
||||
enabled:
|
||||
description:
|
||||
- Is the user enabled
|
||||
required: false
|
||||
default: True
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a user
|
||||
- os_user:
|
||||
cloud: mycloud
|
||||
state: present
|
||||
name: demouser
|
||||
password: secret
|
||||
email: demo@example.com
|
||||
domain: default
|
||||
default_project: demo
|
||||
|
||||
# Delete a user
|
||||
- os_user:
|
||||
cloud: mycloud
|
||||
state: absent
|
||||
name: demouser
|
||||
'''
|
||||
|
||||
|
||||
RETURN = '''
|
||||
user:
|
||||
description: Dictionary describing the user.
|
||||
returned: On success when I(state) is 'present'
|
||||
type: dictionary
|
||||
contains:
|
||||
default_project_id:
|
||||
description: User default project ID. Only present with Keystone >= v3.
|
||||
type: string
|
||||
sample: "4427115787be45f08f0ec22a03bfc735"
|
||||
domain_id:
|
||||
description: User domain ID. Only present with Keystone >= v3.
|
||||
type: string
|
||||
sample: "default"
|
||||
email:
|
||||
description: User email address
|
||||
type: string
|
||||
sample: "demo@example.com"
|
||||
id:
|
||||
description: User ID
|
||||
type: string
|
||||
sample: "f59382db809c43139982ca4189404650"
|
||||
name:
|
||||
description: User name
|
||||
type: string
|
||||
sample: "demouser"
|
||||
'''
|
||||
|
||||
def _needs_update(module, user):
|
||||
keys = ('email', 'default_project', 'domain', 'enabled')
|
||||
for key in keys:
|
||||
if module.params[key] is not None and module.params[key] != user.get(key):
|
||||
return True
|
||||
|
||||
# We don't get password back in the user object, so assume any supplied
|
||||
# password is a change.
|
||||
if module.params['password'] is not None:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name=dict(required=True),
|
||||
password=dict(required=False, default=None),
|
||||
email=dict(required=False, default=None),
|
||||
default_project=dict(required=False, default=None),
|
||||
domain=dict(required=False, default=None),
|
||||
enabled=dict(default=True, type='bool'),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(
|
||||
argument_spec,
|
||||
required_if=[
|
||||
('state', 'present', ['password'])
|
||||
],
|
||||
**module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
name = module.params['name']
|
||||
password = module.params['password']
|
||||
email = module.params['email']
|
||||
default_project = module.params['default_project']
|
||||
domain = module.params['domain']
|
||||
enabled = module.params['enabled']
|
||||
state = module.params['state']
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
user = cloud.get_user(name)
|
||||
|
||||
project_id = None
|
||||
if default_project:
|
||||
project = cloud.get_project(default_project)
|
||||
if not project:
|
||||
module.fail_json(msg='Default project %s is not valid' % default_project)
|
||||
project_id = project['id']
|
||||
|
||||
if state == 'present':
|
||||
if user is None:
|
||||
user = cloud.create_user(
|
||||
name=name, password=password, email=email,
|
||||
default_project=default_project, domain_id=domain,
|
||||
enabled=enabled)
|
||||
changed = True
|
||||
else:
|
||||
if _needs_update(module, user):
|
||||
user = cloud.update_user(
|
||||
user['id'], password=password, email=email,
|
||||
default_project=project_id, domain_id=domain,
|
||||
enabled=enabled)
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
module.exit_json(changed=changed, user=user)
|
||||
|
||||
elif state == 'absent':
|
||||
if user is None:
|
||||
changed=False
|
||||
else:
|
||||
cloud.delete_user(user['id'])
|
||||
changed=True
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message, extra_data=e.extra_data)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,2 @@
|
||||
mock
|
||||
pytest
|
@ -0,0 +1,221 @@
|
||||
import mock
|
||||
import pytest
|
||||
import yaml
|
||||
import inspect
|
||||
import collections
|
||||
|
||||
from cloud.openstack import os_server
|
||||
|
||||
|
||||
class AnsibleFail(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleExit(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def params_from_doc(func):
|
||||
'''This function extracts the docstring from the specified function,
|
||||
parses it as a YAML document, and returns parameters for the os_server
|
||||
module.'''
|
||||
|
||||
doc = inspect.getdoc(func)
|
||||
cfg = yaml.load(doc)
|
||||
|
||||
for task in cfg:
|
||||
for module, params in task.items():
|
||||
for k, v in params.items():
|
||||
if k in ['nics'] and type(v) == str:
|
||||
params[k] = [v]
|
||||
task[module] = collections.defaultdict(str,
|
||||
params)
|
||||
|
||||
return cfg[0]['os_server']
|
||||
|
||||
|
||||
class FakeCloud (object):
|
||||
ports = [
|
||||
{'name': 'port1', 'id': '1234'},
|
||||
{'name': 'port2', 'id': '4321'},
|
||||
]
|
||||
|
||||
networks = [
|
||||
{'name': 'network1', 'id': '5678'},
|
||||
{'name': 'network2', 'id': '8765'},
|
||||
]
|
||||
|
||||
images = [
|
||||
{'name': 'cirros', 'id': '1'},
|
||||
{'name': 'fedora', 'id': '2'},
|
||||
]
|
||||
|
||||
flavors = [
|
||||
{'name': 'm1.small', 'id': '1', 'flavor_ram': 1024},
|
||||
{'name': 'm1.tiny', 'id': '2', 'flavor_ram': 512},
|
||||
]
|
||||
|
||||
def _find(self, source, name):
|
||||
for item in source:
|
||||
if item['name'] == name or item['id'] == name:
|
||||
return item
|
||||
|
||||
def get_image_id(self, name, exclude=None):
|
||||
image = self._find(self.images, name)
|
||||
if image:
|
||||
return image['id']
|
||||
|
||||
def get_flavor(self, name):
|
||||
return self._find(self.flavors, name)
|
||||
|
||||
def get_flavor_by_ram(self, ram, include=None):
|
||||
for flavor in self.flavors:
|
||||
if flavor['ram'] >= ram and (include is None or include in
|
||||
flavor['name']):
|
||||
return flavor
|
||||
|
||||
def get_port(self, name):
|
||||
return self._find(self.ports, name)
|
||||
|
||||
def get_network(self, name):
|
||||
return self._find(self.networks, name)
|
||||
|
||||
create_server = mock.MagicMock()
|
||||
|
||||
|
||||
class TestNetworkArgs(object):
|
||||
'''This class exercises the _network_args function of the
|
||||
os_server module. For each test, we parse the YAML document
|
||||
contained in the docstring to retrieve the module parameters for the
|
||||
test.'''
|
||||
|
||||
def setup_method(self, method):
|
||||
self.cloud = FakeCloud()
|
||||
self.module = mock.MagicMock()
|
||||
self.module.params = params_from_doc(method)
|
||||
|
||||
def test_nics_string_net_id(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics: net-id=1234
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['net-id'] == '1234')
|
||||
|
||||
def test_nics_string_net_id_list(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics: net-id=1234,net-id=4321
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['net-id'] == '1234')
|
||||
assert(args[1]['net-id'] == '4321')
|
||||
|
||||
def test_nics_string_port_id(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics: port-id=1234
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['port-id'] == '1234')
|
||||
|
||||
def test_nics_string_net_name(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics: net-name=network1
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['net-id'] == '5678')
|
||||
|
||||
def test_nics_string_port_name(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics: port-name=port1
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['port-id'] == '1234')
|
||||
|
||||
def test_nics_structured_net_id(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics:
|
||||
- net-id: '1234'
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['net-id'] == '1234')
|
||||
|
||||
def test_nics_structured_mixed(self):
|
||||
'''
|
||||
- os_server:
|
||||
nics:
|
||||
- net-id: '1234'
|
||||
- port-name: port1
|
||||
- 'net-name=network1,port-id=4321'
|
||||
'''
|
||||
args = os_server._network_args(self.module, self.cloud)
|
||||
assert(args[0]['net-id'] == '1234')
|
||||
assert(args[1]['port-id'] == '1234')
|
||||
assert(args[2]['net-id'] == '5678')
|
||||
assert(args[3]['port-id'] == '4321')
|
||||
|
||||
|
||||
class TestCreateServer(object):
|
||||
def setup_method(self, method):
|
||||
self.cloud = FakeCloud()
|
||||
self.module = mock.MagicMock()
|
||||
self.module.params = params_from_doc(method)
|
||||
self.module.fail_json.side_effect = AnsibleFail()
|
||||
self.module.exit_json.side_effect = AnsibleExit()
|
||||
|
||||
self.meta = mock.MagicMock()
|
||||
self.meta.gett_hostvars_from_server.return_value = {
|
||||
'id': '1234'
|
||||
}
|
||||
os_server.meta = self.meta
|
||||
|
||||
def test_create_server(self):
|
||||
'''
|
||||
- os_server:
|
||||
image: cirros
|
||||
flavor: m1.tiny
|
||||
nics:
|
||||
- net-name: network1
|
||||
'''
|
||||
with pytest.raises(AnsibleExit):
|
||||
os_server._create_server(self.module, self.cloud)
|
||||
|
||||
assert(self.cloud.create_server.call_count == 1)
|
||||
assert(self.cloud.create_server.call_args[1]['image']
|
||||
== self.cloud.get_image_id('cirros'))
|
||||
assert(self.cloud.create_server.call_args[1]['flavor']
|
||||
== self.cloud.get_flavor('m1.tiny')['id'])
|
||||
assert(self.cloud.create_server.call_args[1]['nics'][0]['net-id']
|
||||
== self.cloud.get_network('network1')['id'])
|
||||
|
||||
def test_create_server_bad_flavor(self):
|
||||
'''
|
||||
- os_server:
|
||||
image: cirros
|
||||
flavor: missing_flavor
|
||||
nics:
|
||||
- net-name: network1
|
||||
'''
|
||||
with pytest.raises(AnsibleFail):
|
||||
os_server._create_server(self.module, self.cloud)
|
||||
|
||||
assert('missing_flavor' in
|
||||
self.module.fail_json.call_args[1]['msg'])
|
||||
|
||||
def test_create_server_bad_nic(self):
|
||||
'''
|
||||
- os_server:
|
||||
image: cirros
|
||||
flavor: m1.tiny
|
||||
nics:
|
||||
- net-name: missing_network
|
||||
'''
|
||||
with pytest.raises(AnsibleFail):
|
||||
os_server._create_server(self.module, self.cloud)
|
||||
|
||||
assert('missing_network' in
|
||||
self.module.fail_json.call_args[1]['msg'])
|
Loading…
Reference in New Issue