mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-06-27 10:40:22 -07:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
883
plugins/modules/cloud/rackspace/rax.py
Normal file
883
plugins/modules/cloud/rackspace/rax.py
Normal file
|
@ -0,0 +1,883 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax
|
||||
short_description: create / delete an instance in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud instance and optionally
|
||||
waits for it to be 'running'.
|
||||
options:
|
||||
auto_increment:
|
||||
description:
|
||||
- Whether or not to increment a single number with the name of the
|
||||
created servers. Only applicable when used with the I(group) attribute
|
||||
or meta key.
|
||||
type: bool
|
||||
default: 'yes'
|
||||
boot_from_volume:
|
||||
description:
|
||||
- Whether or not to boot the instance from a Cloud Block Storage volume.
|
||||
If C(yes) and I(image) is specified a new volume will be created at
|
||||
boot time. I(boot_volume_size) is required with I(image) to create a
|
||||
new volume at boot time.
|
||||
type: bool
|
||||
default: 'no'
|
||||
boot_volume:
|
||||
description:
|
||||
- Cloud Block Storage ID or Name to use as the boot volume of the
|
||||
instance
|
||||
boot_volume_size:
|
||||
description:
|
||||
- Size of the volume to create in Gigabytes. This is only required with
|
||||
I(image) and I(boot_from_volume).
|
||||
default: 100
|
||||
boot_volume_terminate:
|
||||
description:
|
||||
- Whether the I(boot_volume) or newly created volume from I(image) will
|
||||
be terminated when the server is terminated
|
||||
type: bool
|
||||
default: 'no'
|
||||
config_drive:
|
||||
description:
|
||||
- Attach read-only configuration drive to server as label config-2
|
||||
type: bool
|
||||
default: 'no'
|
||||
count:
|
||||
description:
|
||||
- number of instances to launch
|
||||
default: 1
|
||||
count_offset:
|
||||
description:
|
||||
- number count to start at
|
||||
default: 1
|
||||
disk_config:
|
||||
description:
|
||||
- Disk partitioning strategy
|
||||
choices:
|
||||
- auto
|
||||
- manual
|
||||
default: auto
|
||||
exact_count:
|
||||
description:
|
||||
- Explicitly ensure an exact count of instances, used with
|
||||
state=active/present. If specified as C(yes) and I(count) is less than
|
||||
the servers matched, servers will be deleted to match the count. If
|
||||
the number of matched servers is fewer than specified in I(count)
|
||||
additional servers will be added.
|
||||
type: bool
|
||||
default: 'no'
|
||||
extra_client_args:
|
||||
description:
|
||||
- A hash of key/value pairs to be used when creating the cloudservers
|
||||
client. This is considered an advanced option, use it wisely and
|
||||
with caution.
|
||||
extra_create_args:
|
||||
description:
|
||||
- A hash of key/value pairs to be used when creating a new server.
|
||||
This is considered an advanced option, use it wisely and with caution.
|
||||
files:
|
||||
description:
|
||||
- Files to insert into the instance. remotefilename:localcontent
|
||||
flavor:
|
||||
description:
|
||||
- flavor to use for the instance
|
||||
group:
|
||||
description:
|
||||
- host group to assign to server, is also used for idempotent operations
|
||||
to ensure a specific number of instances
|
||||
image:
|
||||
description:
|
||||
- image to use for the instance. Can be an C(id), C(human_id) or C(name).
|
||||
With I(boot_from_volume), a Cloud Block Storage volume will be created
|
||||
with this image
|
||||
instance_ids:
|
||||
description:
|
||||
- list of instance ids, currently only used when state='absent' to
|
||||
remove instances
|
||||
key_name:
|
||||
description:
|
||||
- key pair to use on the instance
|
||||
aliases:
|
||||
- keypair
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
name:
|
||||
description:
|
||||
- Name to give the instance
|
||||
networks:
|
||||
description:
|
||||
- The network to attach to the instances. If specified, you must include
|
||||
ALL networks including the public and private interfaces. Can be C(id)
|
||||
or C(label).
|
||||
default:
|
||||
- public
|
||||
- private
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
user_data:
|
||||
description:
|
||||
- Data to be uploaded to the servers config drive. This option implies
|
||||
I(config_drive). Can be a file path or a string
|
||||
wait:
|
||||
description:
|
||||
- wait for the instance to be in state 'running' before returning
|
||||
type: bool
|
||||
default: 'no'
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author:
|
||||
- "Jesse Keating (@omgjlk)"
|
||||
- "Matt Martz (@sivel)"
|
||||
notes:
|
||||
- I(exact_count) can be "destructive" if the number of running servers in
|
||||
the I(group) is larger than that specified in I(count). In such a case, the
|
||||
I(state) is effectively set to C(absent) and the extra servers are deleted.
|
||||
In the case of deletion, the returned data structure will have C(action)
|
||||
set to C(delete), and the oldest servers in the group will be deleted.
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Cloud Server
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Server build request
|
||||
local_action:
|
||||
module: rax
|
||||
credentials: ~/.raxpub
|
||||
name: rax-test1
|
||||
flavor: 5
|
||||
image: b11d9567-e412-4255-96b9-bd63ab23bcfe
|
||||
key_name: my_rackspace_key
|
||||
files:
|
||||
/root/test.txt: /home/localuser/test.txt
|
||||
wait: yes
|
||||
state: present
|
||||
networks:
|
||||
- private
|
||||
- public
|
||||
register: rax
|
||||
|
||||
- name: Build an exact count of cloud servers with incremented names
|
||||
hosts: local
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Server build requests
|
||||
local_action:
|
||||
module: rax
|
||||
credentials: ~/.raxpub
|
||||
name: test%03d.example.org
|
||||
flavor: performance1-1
|
||||
image: ubuntu-1204-lts-precise-pangolin
|
||||
state: present
|
||||
count: 10
|
||||
count_offset: 10
|
||||
exact_count: yes
|
||||
group: test
|
||||
wait: yes
|
||||
register: rax
|
||||
'''
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (FINAL_STATUSES, rax_argument_spec, rax_find_bootable_volume,
|
||||
rax_find_image, rax_find_network, rax_find_volume,
|
||||
rax_required_together, rax_to_dict, setup_rax_module)
|
||||
from ansible.module_utils.six.moves import xrange
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
|
||||
def rax_find_server_image(module, server, image, boot_volume):
|
||||
if not image and boot_volume:
|
||||
vol = rax_find_bootable_volume(module, pyrax, server,
|
||||
exit=False)
|
||||
if not vol:
|
||||
return None
|
||||
volume_image_metadata = vol.volume_image_metadata
|
||||
vol_image_id = volume_image_metadata.get('image_id')
|
||||
if vol_image_id:
|
||||
server_image = rax_find_image(module, pyrax,
|
||||
vol_image_id, exit=False)
|
||||
if server_image:
|
||||
server.image = dict(id=server_image)
|
||||
|
||||
# Match image IDs taking care of boot from volume
|
||||
if image and not server.image:
|
||||
vol = rax_find_bootable_volume(module, pyrax, server)
|
||||
volume_image_metadata = vol.volume_image_metadata
|
||||
vol_image_id = volume_image_metadata.get('image_id')
|
||||
if not vol_image_id:
|
||||
return None
|
||||
server_image = rax_find_image(module, pyrax,
|
||||
vol_image_id, exit=False)
|
||||
if image != server_image:
|
||||
return None
|
||||
|
||||
server.image = dict(id=server_image)
|
||||
elif image and server.image['id'] != image:
|
||||
return None
|
||||
|
||||
return server.image
|
||||
|
||||
|
||||
def create(module, names=None, flavor=None, image=None, meta=None, key_name=None,
|
||||
files=None, wait=True, wait_timeout=300, disk_config=None,
|
||||
group=None, nics=None, extra_create_args=None, user_data=None,
|
||||
config_drive=False, existing=None, block_device_mapping_v2=None):
|
||||
names = [] if names is None else names
|
||||
meta = {} if meta is None else meta
|
||||
files = {} if files is None else files
|
||||
nics = [] if nics is None else nics
|
||||
extra_create_args = {} if extra_create_args is None else extra_create_args
|
||||
existing = [] if existing is None else existing
|
||||
block_device_mapping_v2 = [] if block_device_mapping_v2 is None else block_device_mapping_v2
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
changed = False
|
||||
|
||||
if user_data:
|
||||
config_drive = True
|
||||
|
||||
if user_data and os.path.isfile(os.path.expanduser(user_data)):
|
||||
try:
|
||||
user_data = os.path.expanduser(user_data)
|
||||
f = open(user_data)
|
||||
user_data = f.read()
|
||||
f.close()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Failed to load %s' % user_data)
|
||||
|
||||
# Handle the file contents
|
||||
for rpath in files.keys():
|
||||
lpath = os.path.expanduser(files[rpath])
|
||||
try:
|
||||
fileobj = open(lpath, 'r')
|
||||
files[rpath] = fileobj.read()
|
||||
fileobj.close()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Failed to load %s' % lpath)
|
||||
try:
|
||||
servers = []
|
||||
bdmv2 = block_device_mapping_v2
|
||||
for name in names:
|
||||
servers.append(cs.servers.create(name=name, image=image,
|
||||
flavor=flavor, meta=meta,
|
||||
key_name=key_name,
|
||||
files=files, nics=nics,
|
||||
disk_config=disk_config,
|
||||
config_drive=config_drive,
|
||||
userdata=user_data,
|
||||
block_device_mapping_v2=bdmv2,
|
||||
**extra_create_args))
|
||||
except Exception as e:
|
||||
if e.message:
|
||||
msg = str(e.message)
|
||||
else:
|
||||
msg = repr(e)
|
||||
module.fail_json(msg=msg)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
if wait:
|
||||
end_time = time.time() + wait_timeout
|
||||
infinite = wait_timeout == 0
|
||||
while infinite or time.time() < end_time:
|
||||
for server in servers:
|
||||
try:
|
||||
server.get()
|
||||
except Exception:
|
||||
server.status = 'ERROR'
|
||||
|
||||
if not filter(lambda s: s.status not in FINAL_STATUSES,
|
||||
servers):
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
success = []
|
||||
error = []
|
||||
timeout = []
|
||||
for server in servers:
|
||||
try:
|
||||
server.get()
|
||||
except Exception:
|
||||
server.status = 'ERROR'
|
||||
instance = rax_to_dict(server, 'server')
|
||||
if server.status == 'ACTIVE' or not wait:
|
||||
success.append(instance)
|
||||
elif server.status == 'ERROR':
|
||||
error.append(instance)
|
||||
elif wait:
|
||||
timeout.append(instance)
|
||||
|
||||
untouched = [rax_to_dict(s, 'server') for s in existing]
|
||||
instances = success + untouched
|
||||
|
||||
results = {
|
||||
'changed': changed,
|
||||
'action': 'create',
|
||||
'instances': instances,
|
||||
'success': success,
|
||||
'error': error,
|
||||
'timeout': timeout,
|
||||
'instance_ids': {
|
||||
'instances': [i['id'] for i in instances],
|
||||
'success': [i['id'] for i in success],
|
||||
'error': [i['id'] for i in error],
|
||||
'timeout': [i['id'] for i in timeout]
|
||||
}
|
||||
}
|
||||
|
||||
if timeout:
|
||||
results['msg'] = 'Timeout waiting for all servers to build'
|
||||
elif error:
|
||||
results['msg'] = 'Failed to build all servers'
|
||||
|
||||
if 'msg' in results:
|
||||
module.fail_json(**results)
|
||||
else:
|
||||
module.exit_json(**results)
|
||||
|
||||
|
||||
def delete(module, instance_ids=None, wait=True, wait_timeout=300, kept=None):
|
||||
instance_ids = [] if instance_ids is None else instance_ids
|
||||
kept = [] if kept is None else kept
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
changed = False
|
||||
instances = {}
|
||||
servers = []
|
||||
|
||||
for instance_id in instance_ids:
|
||||
servers.append(cs.servers.get(instance_id))
|
||||
|
||||
for server in servers:
|
||||
try:
|
||||
server.delete()
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
instance = rax_to_dict(server, 'server')
|
||||
instances[instance['id']] = instance
|
||||
|
||||
# If requested, wait for server deletion
|
||||
if wait:
|
||||
end_time = time.time() + wait_timeout
|
||||
infinite = wait_timeout == 0
|
||||
while infinite or time.time() < end_time:
|
||||
for server in servers:
|
||||
instance_id = server.id
|
||||
try:
|
||||
server.get()
|
||||
except Exception:
|
||||
instances[instance_id]['status'] = 'DELETED'
|
||||
instances[instance_id]['rax_status'] = 'DELETED'
|
||||
|
||||
if not filter(lambda s: s['status'] not in ('', 'DELETED',
|
||||
'ERROR'),
|
||||
instances.values()):
|
||||
break
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
timeout = filter(lambda s: s['status'] not in ('', 'DELETED', 'ERROR'),
|
||||
instances.values())
|
||||
error = filter(lambda s: s['status'] in ('ERROR'),
|
||||
instances.values())
|
||||
success = filter(lambda s: s['status'] in ('', 'DELETED'),
|
||||
instances.values())
|
||||
|
||||
instances = [rax_to_dict(s, 'server') for s in kept]
|
||||
|
||||
results = {
|
||||
'changed': changed,
|
||||
'action': 'delete',
|
||||
'instances': instances,
|
||||
'success': success,
|
||||
'error': error,
|
||||
'timeout': timeout,
|
||||
'instance_ids': {
|
||||
'instances': [i['id'] for i in instances],
|
||||
'success': [i['id'] for i in success],
|
||||
'error': [i['id'] for i in error],
|
||||
'timeout': [i['id'] for i in timeout]
|
||||
}
|
||||
}
|
||||
|
||||
if timeout:
|
||||
results['msg'] = 'Timeout waiting for all servers to delete'
|
||||
elif error:
|
||||
results['msg'] = 'Failed to delete all servers'
|
||||
|
||||
if 'msg' in results:
|
||||
module.fail_json(**results)
|
||||
else:
|
||||
module.exit_json(**results)
|
||||
|
||||
|
||||
def cloudservers(module, state=None, name=None, flavor=None, image=None,
|
||||
meta=None, key_name=None, files=None, wait=True, wait_timeout=300,
|
||||
disk_config=None, count=1, group=None, instance_ids=None,
|
||||
exact_count=False, networks=None, count_offset=0,
|
||||
auto_increment=False, extra_create_args=None, user_data=None,
|
||||
config_drive=False, boot_from_volume=False,
|
||||
boot_volume=None, boot_volume_size=None,
|
||||
boot_volume_terminate=False):
|
||||
meta = {} if meta is None else meta
|
||||
files = {} if files is None else files
|
||||
instance_ids = [] if instance_ids is None else instance_ids
|
||||
networks = [] if networks is None else networks
|
||||
extra_create_args = {} if extra_create_args is None else extra_create_args
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
cnw = pyrax.cloud_networks
|
||||
if not cnw:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present' or (state == 'absent' and instance_ids is None):
|
||||
if not boot_from_volume and not boot_volume and not image:
|
||||
module.fail_json(msg='image is required for the "rax" module')
|
||||
|
||||
for arg, value in dict(name=name, flavor=flavor).items():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax" module' %
|
||||
arg)
|
||||
|
||||
if boot_from_volume and not image and not boot_volume:
|
||||
module.fail_json(msg='image or boot_volume are required for the '
|
||||
'"rax" with boot_from_volume')
|
||||
|
||||
if boot_from_volume and image and not boot_volume_size:
|
||||
module.fail_json(msg='boot_volume_size is required for the "rax" '
|
||||
'module with boot_from_volume and image')
|
||||
|
||||
if boot_from_volume and image and boot_volume:
|
||||
image = None
|
||||
|
||||
servers = []
|
||||
|
||||
# Add the group meta key
|
||||
if group and 'group' not in meta:
|
||||
meta['group'] = group
|
||||
elif 'group' in meta and group is None:
|
||||
group = meta['group']
|
||||
|
||||
# Normalize and ensure all metadata values are strings
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, list):
|
||||
meta[k] = ','.join(['%s' % i for i in v])
|
||||
elif isinstance(v, dict):
|
||||
meta[k] = json.dumps(v)
|
||||
elif not isinstance(v, string_types):
|
||||
meta[k] = '%s' % v
|
||||
|
||||
# When using state=absent with group, the absent block won't match the
|
||||
# names properly. Use the exact_count functionality to decrease the count
|
||||
# to the desired level
|
||||
was_absent = False
|
||||
if group is not None and state == 'absent':
|
||||
exact_count = True
|
||||
state = 'present'
|
||||
was_absent = True
|
||||
|
||||
if image:
|
||||
image = rax_find_image(module, pyrax, image)
|
||||
|
||||
nics = []
|
||||
if networks:
|
||||
for network in networks:
|
||||
nics.extend(rax_find_network(module, pyrax, network))
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
# Idempotent ensurance of a specific count of servers
|
||||
if exact_count is not False:
|
||||
# See if we can find servers that match our options
|
||||
if group is None:
|
||||
module.fail_json(msg='"group" must be provided when using '
|
||||
'"exact_count"')
|
||||
|
||||
if auto_increment:
|
||||
numbers = set()
|
||||
|
||||
# See if the name is a printf like string, if not append
|
||||
# %d to the end
|
||||
try:
|
||||
name % 0
|
||||
except TypeError as e:
|
||||
if e.message.startswith('not all'):
|
||||
name = '%s%%d' % name
|
||||
else:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# regex pattern to match printf formatting
|
||||
pattern = re.sub(r'%\d*[sd]', r'(\d+)', name)
|
||||
for server in cs.servers.list():
|
||||
# Ignore DELETED servers
|
||||
if server.status == 'DELETED':
|
||||
continue
|
||||
if server.metadata.get('group') == group:
|
||||
servers.append(server)
|
||||
match = re.search(pattern, server.name)
|
||||
if match:
|
||||
number = int(match.group(1))
|
||||
numbers.add(number)
|
||||
|
||||
number_range = xrange(count_offset, count_offset + count)
|
||||
available_numbers = list(set(number_range)
|
||||
.difference(numbers))
|
||||
else: # Not auto incrementing
|
||||
for server in cs.servers.list():
|
||||
# Ignore DELETED servers
|
||||
if server.status == 'DELETED':
|
||||
continue
|
||||
if server.metadata.get('group') == group:
|
||||
servers.append(server)
|
||||
# available_numbers not needed here, we inspect auto_increment
|
||||
# again later
|
||||
|
||||
# If state was absent but the count was changed,
|
||||
# assume we only wanted to remove that number of instances
|
||||
if was_absent:
|
||||
diff = len(servers) - count
|
||||
if diff < 0:
|
||||
count = 0
|
||||
else:
|
||||
count = diff
|
||||
|
||||
if len(servers) > count:
|
||||
# We have more servers than we need, set state='absent'
|
||||
# and delete the extras, this should delete the oldest
|
||||
state = 'absent'
|
||||
kept = servers[:count]
|
||||
del servers[:count]
|
||||
instance_ids = []
|
||||
for server in servers:
|
||||
instance_ids.append(server.id)
|
||||
delete(module, instance_ids=instance_ids, wait=wait,
|
||||
wait_timeout=wait_timeout, kept=kept)
|
||||
elif len(servers) < count:
|
||||
# we have fewer servers than we need
|
||||
if auto_increment:
|
||||
# auto incrementing server numbers
|
||||
names = []
|
||||
name_slice = count - len(servers)
|
||||
numbers_to_use = available_numbers[:name_slice]
|
||||
for number in numbers_to_use:
|
||||
names.append(name % number)
|
||||
else:
|
||||
# We are not auto incrementing server numbers,
|
||||
# create a list of 'name' that matches how many we need
|
||||
names = [name] * (count - len(servers))
|
||||
else:
|
||||
# we have the right number of servers, just return info
|
||||
# about all of the matched servers
|
||||
instances = []
|
||||
instance_ids = []
|
||||
for server in servers:
|
||||
instances.append(rax_to_dict(server, 'server'))
|
||||
instance_ids.append(server.id)
|
||||
module.exit_json(changed=False, action=None,
|
||||
instances=instances,
|
||||
success=[], error=[], timeout=[],
|
||||
instance_ids={'instances': instance_ids,
|
||||
'success': [], 'error': [],
|
||||
'timeout': []})
|
||||
else: # not called with exact_count=True
|
||||
if group is not None:
|
||||
if auto_increment:
|
||||
# we are auto incrementing server numbers, but not with
|
||||
# exact_count
|
||||
numbers = set()
|
||||
|
||||
# See if the name is a printf like string, if not append
|
||||
# %d to the end
|
||||
try:
|
||||
name % 0
|
||||
except TypeError as e:
|
||||
if e.message.startswith('not all'):
|
||||
name = '%s%%d' % name
|
||||
else:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# regex pattern to match printf formatting
|
||||
pattern = re.sub(r'%\d*[sd]', r'(\d+)', name)
|
||||
for server in cs.servers.list():
|
||||
# Ignore DELETED servers
|
||||
if server.status == 'DELETED':
|
||||
continue
|
||||
if server.metadata.get('group') == group:
|
||||
servers.append(server)
|
||||
match = re.search(pattern, server.name)
|
||||
if match:
|
||||
number = int(match.group(1))
|
||||
numbers.add(number)
|
||||
|
||||
number_range = xrange(count_offset,
|
||||
count_offset + count + len(numbers))
|
||||
available_numbers = list(set(number_range)
|
||||
.difference(numbers))
|
||||
names = []
|
||||
numbers_to_use = available_numbers[:count]
|
||||
for number in numbers_to_use:
|
||||
names.append(name % number)
|
||||
else:
|
||||
# Not auto incrementing
|
||||
names = [name] * count
|
||||
else:
|
||||
# No group was specified, and not using exact_count
|
||||
# Perform more simplistic matching
|
||||
search_opts = {
|
||||
'name': '^%s$' % name,
|
||||
'flavor': flavor
|
||||
}
|
||||
servers = []
|
||||
for server in cs.servers.list(search_opts=search_opts):
|
||||
# Ignore DELETED servers
|
||||
if server.status == 'DELETED':
|
||||
continue
|
||||
|
||||
if not rax_find_server_image(module, server, image,
|
||||
boot_volume):
|
||||
continue
|
||||
|
||||
# Ignore servers with non matching metadata
|
||||
if server.metadata != meta:
|
||||
continue
|
||||
servers.append(server)
|
||||
|
||||
if len(servers) >= count:
|
||||
# We have more servers than were requested, don't do
|
||||
# anything. Not running with exact_count=True, so we assume
|
||||
# more is OK
|
||||
instances = []
|
||||
for server in servers:
|
||||
instances.append(rax_to_dict(server, 'server'))
|
||||
|
||||
instance_ids = [i['id'] for i in instances]
|
||||
module.exit_json(changed=False, action=None,
|
||||
instances=instances, success=[], error=[],
|
||||
timeout=[],
|
||||
instance_ids={'instances': instance_ids,
|
||||
'success': [], 'error': [],
|
||||
'timeout': []})
|
||||
|
||||
# We need more servers to reach out target, create names for
|
||||
# them, we aren't performing auto_increment here
|
||||
names = [name] * (count - len(servers))
|
||||
|
||||
block_device_mapping_v2 = []
|
||||
if boot_from_volume:
|
||||
mapping = {
|
||||
'boot_index': '0',
|
||||
'delete_on_termination': boot_volume_terminate,
|
||||
'destination_type': 'volume',
|
||||
}
|
||||
if image:
|
||||
mapping.update({
|
||||
'uuid': image,
|
||||
'source_type': 'image',
|
||||
'volume_size': boot_volume_size,
|
||||
})
|
||||
image = None
|
||||
elif boot_volume:
|
||||
volume = rax_find_volume(module, pyrax, boot_volume)
|
||||
mapping.update({
|
||||
'uuid': pyrax.utils.get_id(volume),
|
||||
'source_type': 'volume',
|
||||
})
|
||||
block_device_mapping_v2.append(mapping)
|
||||
|
||||
create(module, names=names, flavor=flavor, image=image,
|
||||
meta=meta, key_name=key_name, files=files, wait=wait,
|
||||
wait_timeout=wait_timeout, disk_config=disk_config, group=group,
|
||||
nics=nics, extra_create_args=extra_create_args,
|
||||
user_data=user_data, config_drive=config_drive,
|
||||
existing=servers,
|
||||
block_device_mapping_v2=block_device_mapping_v2)
|
||||
|
||||
elif state == 'absent':
|
||||
if instance_ids is None:
|
||||
# We weren't given an explicit list of server IDs to delete
|
||||
# Let's match instead
|
||||
search_opts = {
|
||||
'name': '^%s$' % name,
|
||||
'flavor': flavor
|
||||
}
|
||||
for server in cs.servers.list(search_opts=search_opts):
|
||||
# Ignore DELETED servers
|
||||
if server.status == 'DELETED':
|
||||
continue
|
||||
|
||||
if not rax_find_server_image(module, server, image,
|
||||
boot_volume):
|
||||
continue
|
||||
|
||||
# Ignore servers with non matching metadata
|
||||
if meta != server.metadata:
|
||||
continue
|
||||
|
||||
servers.append(server)
|
||||
|
||||
# Build a list of server IDs to delete
|
||||
instance_ids = []
|
||||
for server in servers:
|
||||
if len(instance_ids) < count:
|
||||
instance_ids.append(server.id)
|
||||
else:
|
||||
break
|
||||
|
||||
if not instance_ids:
|
||||
# No server IDs were matched for deletion, or no IDs were
|
||||
# explicitly provided, just exit and don't do anything
|
||||
module.exit_json(changed=False, action=None, instances=[],
|
||||
success=[], error=[], timeout=[],
|
||||
instance_ids={'instances': [],
|
||||
'success': [], 'error': [],
|
||||
'timeout': []})
|
||||
|
||||
delete(module, instance_ids=instance_ids, wait=wait,
|
||||
wait_timeout=wait_timeout)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
auto_increment=dict(default=True, type='bool'),
|
||||
boot_from_volume=dict(default=False, type='bool'),
|
||||
boot_volume=dict(type='str'),
|
||||
boot_volume_size=dict(type='int', default=100),
|
||||
boot_volume_terminate=dict(type='bool', default=False),
|
||||
config_drive=dict(default=False, type='bool'),
|
||||
count=dict(default=1, type='int'),
|
||||
count_offset=dict(default=1, type='int'),
|
||||
disk_config=dict(choices=['auto', 'manual']),
|
||||
exact_count=dict(default=False, type='bool'),
|
||||
extra_client_args=dict(type='dict', default={}),
|
||||
extra_create_args=dict(type='dict', default={}),
|
||||
files=dict(type='dict', default={}),
|
||||
flavor=dict(),
|
||||
group=dict(),
|
||||
image=dict(),
|
||||
instance_ids=dict(type='list'),
|
||||
key_name=dict(aliases=['keypair']),
|
||||
meta=dict(type='dict', default={}),
|
||||
name=dict(),
|
||||
networks=dict(type='list', default=['public', 'private']),
|
||||
service=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
user_data=dict(no_log=True),
|
||||
wait=dict(default=False, type='bool'),
|
||||
wait_timeout=dict(default=300, type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
service = module.params.get('service')
|
||||
|
||||
if service is not None:
|
||||
module.fail_json(msg='The "service" attribute has been deprecated, '
|
||||
'please remove "service: cloudservers" from your '
|
||||
'playbook pertaining to the "rax" module')
|
||||
|
||||
auto_increment = module.params.get('auto_increment')
|
||||
boot_from_volume = module.params.get('boot_from_volume')
|
||||
boot_volume = module.params.get('boot_volume')
|
||||
boot_volume_size = module.params.get('boot_volume_size')
|
||||
boot_volume_terminate = module.params.get('boot_volume_terminate')
|
||||
config_drive = module.params.get('config_drive')
|
||||
count = module.params.get('count')
|
||||
count_offset = module.params.get('count_offset')
|
||||
disk_config = module.params.get('disk_config')
|
||||
if disk_config:
|
||||
disk_config = disk_config.upper()
|
||||
exact_count = module.params.get('exact_count', False)
|
||||
extra_client_args = module.params.get('extra_client_args')
|
||||
extra_create_args = module.params.get('extra_create_args')
|
||||
files = module.params.get('files')
|
||||
flavor = module.params.get('flavor')
|
||||
group = module.params.get('group')
|
||||
image = module.params.get('image')
|
||||
instance_ids = module.params.get('instance_ids')
|
||||
key_name = module.params.get('key_name')
|
||||
meta = module.params.get('meta')
|
||||
name = module.params.get('name')
|
||||
networks = module.params.get('networks')
|
||||
state = module.params.get('state')
|
||||
user_data = module.params.get('user_data')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
if extra_client_args:
|
||||
pyrax.cloudservers = pyrax.connect_to_cloudservers(
|
||||
region=pyrax.cloudservers.client.region_name,
|
||||
**extra_client_args)
|
||||
client = pyrax.cloudservers.client
|
||||
if 'bypass_url' in extra_client_args:
|
||||
client.management_url = extra_client_args['bypass_url']
|
||||
|
||||
if pyrax.cloudservers is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
cloudservers(module, state=state, name=name, flavor=flavor,
|
||||
image=image, meta=meta, key_name=key_name, files=files,
|
||||
wait=wait, wait_timeout=wait_timeout, disk_config=disk_config,
|
||||
count=count, group=group, instance_ids=instance_ids,
|
||||
exact_count=exact_count, networks=networks,
|
||||
count_offset=count_offset, auto_increment=auto_increment,
|
||||
extra_create_args=extra_create_args, user_data=user_data,
|
||||
config_drive=config_drive, boot_from_volume=boot_from_volume,
|
||||
boot_volume=boot_volume, boot_volume_size=boot_volume_size,
|
||||
boot_volume_terminate=boot_volume_terminate)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
225
plugins/modules/cloud/rackspace/rax_cbs.py
Normal file
225
plugins/modules/cloud/rackspace/rax_cbs.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cbs
|
||||
short_description: Manipulate Rackspace Cloud Block Storage Volumes
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Block Storage Volumes
|
||||
options:
|
||||
description:
|
||||
description:
|
||||
- Description to give the volume being created
|
||||
image:
|
||||
description:
|
||||
- image to use for bootable volumes. Can be an C(id), C(human_id) or
|
||||
C(name). This option requires C(pyrax>=1.9.3)
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the volume
|
||||
name:
|
||||
description:
|
||||
- Name to give the volume being created
|
||||
required: true
|
||||
size:
|
||||
description:
|
||||
- Size of the volume to create in Gigabytes
|
||||
default: 100
|
||||
required: true
|
||||
snapshot_id:
|
||||
description:
|
||||
- The id of the snapshot to create the volume from
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
required: true
|
||||
volume_type:
|
||||
description:
|
||||
- Type of the volume being created
|
||||
choices:
|
||||
- SATA
|
||||
- SSD
|
||||
default: SATA
|
||||
required: true
|
||||
wait:
|
||||
description:
|
||||
- wait for the volume to be in state 'available' before returning
|
||||
type: bool
|
||||
default: 'no'
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author:
|
||||
- "Christopher H. Laco (@claco)"
|
||||
- "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Block Storage Volume
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Storage volume create request
|
||||
local_action:
|
||||
module: rax_cbs
|
||||
credentials: ~/.raxpub
|
||||
name: my-volume
|
||||
description: My Volume
|
||||
volume_type: SSD
|
||||
size: 150
|
||||
region: DFW
|
||||
wait: yes
|
||||
state: present
|
||||
meta:
|
||||
app: my-cool-app
|
||||
register: my_volume
|
||||
'''
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (VOLUME_STATUS, rax_argument_spec, rax_find_image, rax_find_volume,
|
||||
rax_required_together, rax_to_dict, setup_rax_module)
|
||||
|
||||
|
||||
def cloud_block_storage(module, state, name, description, meta, size,
|
||||
snapshot_id, volume_type, wait, wait_timeout,
|
||||
image):
|
||||
changed = False
|
||||
volume = None
|
||||
instance = {}
|
||||
|
||||
cbs = pyrax.cloud_blockstorage
|
||||
|
||||
if cbs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if image:
|
||||
# pyrax<1.9.3 did not have support for specifying an image when
|
||||
# creating a volume which is required for bootable volumes
|
||||
if LooseVersion(pyrax.version.version) < LooseVersion('1.9.3'):
|
||||
module.fail_json(msg='Creating a bootable volume requires '
|
||||
'pyrax>=1.9.3')
|
||||
image = rax_find_image(module, pyrax, image)
|
||||
|
||||
volume = rax_find_volume(module, pyrax, name)
|
||||
|
||||
if state == 'present':
|
||||
if not volume:
|
||||
kwargs = dict()
|
||||
if image:
|
||||
kwargs['image'] = image
|
||||
try:
|
||||
volume = cbs.create(name, size=size, volume_type=volume_type,
|
||||
description=description,
|
||||
metadata=meta,
|
||||
snapshot_id=snapshot_id, **kwargs)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
if wait:
|
||||
attempts = wait_timeout // 5
|
||||
pyrax.utils.wait_for_build(volume, interval=5,
|
||||
attempts=attempts)
|
||||
|
||||
volume.get()
|
||||
instance = rax_to_dict(volume)
|
||||
|
||||
result = dict(changed=changed, volume=instance)
|
||||
|
||||
if volume.status == 'error':
|
||||
result['msg'] = '%s failed to build' % volume.id
|
||||
elif wait and volume.status not in VOLUME_STATUS:
|
||||
result['msg'] = 'Timeout waiting on %s' % volume.id
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
if volume:
|
||||
instance = rax_to_dict(volume)
|
||||
try:
|
||||
volume.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, volume=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
description=dict(type='str'),
|
||||
image=dict(type='str'),
|
||||
meta=dict(type='dict', default={}),
|
||||
name=dict(required=True),
|
||||
size=dict(type='int', default=100),
|
||||
snapshot_id=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
volume_type=dict(choices=['SSD', 'SATA'], default='SATA'),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300)
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
description = module.params.get('description')
|
||||
image = module.params.get('image')
|
||||
meta = module.params.get('meta')
|
||||
name = module.params.get('name')
|
||||
size = module.params.get('size')
|
||||
snapshot_id = module.params.get('snapshot_id')
|
||||
state = module.params.get('state')
|
||||
volume_type = module.params.get('volume_type')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_block_storage(module, state, name, description, meta, size,
|
||||
snapshot_id, volume_type, wait, wait_timeout,
|
||||
image)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
219
plugins/modules/cloud/rackspace/rax_cbs_attachments.py
Normal file
219
plugins/modules/cloud/rackspace/rax_cbs_attachments.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cbs_attachments
|
||||
short_description: Manipulate Rackspace Cloud Block Storage Volume Attachments
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Block Storage Volume Attachments
|
||||
options:
|
||||
device:
|
||||
description:
|
||||
- The device path to attach the volume to, e.g. /dev/xvde.
|
||||
- Before 2.4 this was a required field. Now it can be left to null to auto assign the device name.
|
||||
volume:
|
||||
description:
|
||||
- Name or id of the volume to attach/detach
|
||||
required: true
|
||||
server:
|
||||
description:
|
||||
- Name or id of the server to attach/detach
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
required: true
|
||||
wait:
|
||||
description:
|
||||
- wait for the volume to be in 'in-use'/'available' state before returning
|
||||
type: bool
|
||||
default: 'no'
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author:
|
||||
- "Christopher H. Laco (@claco)"
|
||||
- "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Attach a Block Storage Volume
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Storage volume attach request
|
||||
local_action:
|
||||
module: rax_cbs_attachments
|
||||
credentials: ~/.raxpub
|
||||
volume: my-volume
|
||||
server: my-server
|
||||
device: /dev/xvdd
|
||||
region: DFW
|
||||
wait: yes
|
||||
state: present
|
||||
register: my_volume
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (NON_CALLABLES,
|
||||
rax_argument_spec,
|
||||
rax_find_server,
|
||||
rax_find_volume,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def cloud_block_storage_attachments(module, state, volume, server, device,
|
||||
wait, wait_timeout):
|
||||
cbs = pyrax.cloud_blockstorage
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cbs is None or cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
changed = False
|
||||
instance = {}
|
||||
|
||||
volume = rax_find_volume(module, pyrax, volume)
|
||||
|
||||
if not volume:
|
||||
module.fail_json(msg='No matching storage volumes were found')
|
||||
|
||||
if state == 'present':
|
||||
server = rax_find_server(module, pyrax, server)
|
||||
|
||||
if (volume.attachments and
|
||||
volume.attachments[0]['server_id'] == server.id):
|
||||
changed = False
|
||||
elif volume.attachments:
|
||||
module.fail_json(msg='Volume is attached to another server')
|
||||
else:
|
||||
try:
|
||||
volume.attach_to_instance(server, mountpoint=device)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
volume.get()
|
||||
|
||||
for key, value in vars(volume).items():
|
||||
if (isinstance(value, NON_CALLABLES) and
|
||||
not key.startswith('_')):
|
||||
instance[key] = value
|
||||
|
||||
result = dict(changed=changed)
|
||||
|
||||
if volume.status == 'error':
|
||||
result['msg'] = '%s failed to build' % volume.id
|
||||
elif wait:
|
||||
attempts = wait_timeout // 5
|
||||
pyrax.utils.wait_until(volume, 'status', 'in-use',
|
||||
interval=5, attempts=attempts)
|
||||
|
||||
volume.get()
|
||||
result['volume'] = rax_to_dict(volume)
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
server = rax_find_server(module, pyrax, server)
|
||||
|
||||
if (volume.attachments and
|
||||
volume.attachments[0]['server_id'] == server.id):
|
||||
try:
|
||||
volume.detach()
|
||||
if wait:
|
||||
pyrax.utils.wait_until(volume, 'status', 'available',
|
||||
interval=3, attempts=0,
|
||||
verbose=False)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
volume.get()
|
||||
changed = True
|
||||
elif volume.attachments:
|
||||
module.fail_json(msg='Volume is attached to another server')
|
||||
|
||||
result = dict(changed=changed, volume=rax_to_dict(volume))
|
||||
|
||||
if volume.status == 'error':
|
||||
result['msg'] = '%s failed to build' % volume.id
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
module.exit_json(changed=changed, volume=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
device=dict(required=False),
|
||||
volume=dict(required=True),
|
||||
server=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300)
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
device = module.params.get('device')
|
||||
volume = module.params.get('volume')
|
||||
server = module.params.get('server')
|
||||
state = module.params.get('state')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_block_storage_attachments(module, state, volume, server, device,
|
||||
wait, wait_timeout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
254
plugins/modules/cloud/rackspace/rax_cdb.py
Normal file
254
plugins/modules/cloud/rackspace/rax_cdb.py
Normal file
|
@ -0,0 +1,254 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cdb
|
||||
short_description: create/delete or resize a Rackspace Cloud Databases instance
|
||||
description:
|
||||
- creates / deletes or resize a Rackspace Cloud Databases instance
|
||||
and optionally waits for it to be 'running'. The name option needs to be
|
||||
unique since it's used to identify the instance.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the databases server instance
|
||||
flavor:
|
||||
description:
|
||||
- flavor to use for the instance 1 to 6 (i.e. 512MB to 16GB)
|
||||
default: 1
|
||||
volume:
|
||||
description:
|
||||
- Volume size of the database 1-150GB
|
||||
default: 2
|
||||
cdb_type:
|
||||
description:
|
||||
- type of instance (i.e. MySQL, MariaDB, Percona)
|
||||
default: MySQL
|
||||
aliases: ['type']
|
||||
cdb_version:
|
||||
description:
|
||||
- version of database (MySQL supports 5.1 and 5.6, MariaDB supports 10, Percona supports 5.6)
|
||||
choices: ['5.1', '5.6', '10']
|
||||
aliases: ['version']
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
wait:
|
||||
description:
|
||||
- wait for the instance to be in state 'running' before returning
|
||||
type: bool
|
||||
default: 'no'
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: "Simon JAILLET (@jails)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Cloud Databases
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Server build request
|
||||
local_action:
|
||||
module: rax_cdb
|
||||
credentials: ~/.raxpub
|
||||
region: IAD
|
||||
name: db-server1
|
||||
flavor: 1
|
||||
volume: 2
|
||||
cdb_type: MySQL
|
||||
cdb_version: 5.6
|
||||
wait: yes
|
||||
state: present
|
||||
register: rax_db_server
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, rax_to_dict, setup_rax_module
|
||||
|
||||
|
||||
def find_instance(name):
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
instances = cdb.list()
|
||||
if instances:
|
||||
for instance in instances:
|
||||
if instance.name == name:
|
||||
return instance
|
||||
return False
|
||||
|
||||
|
||||
def save_instance(module, name, flavor, volume, cdb_type, cdb_version, wait,
|
||||
wait_timeout):
|
||||
|
||||
for arg, value in dict(name=name, flavor=flavor,
|
||||
volume=volume, type=cdb_type, version=cdb_version
|
||||
).items():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb"'
|
||||
' module' % arg)
|
||||
|
||||
if not (volume >= 1 and volume <= 150):
|
||||
module.fail_json(msg='volume is required to be between 1 and 150')
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
flavors = []
|
||||
for item in cdb.list_flavors():
|
||||
flavors.append(item.id)
|
||||
|
||||
if not (flavor in flavors):
|
||||
module.fail_json(msg='unexisting flavor reference "%s"' % str(flavor))
|
||||
|
||||
changed = False
|
||||
|
||||
instance = find_instance(name)
|
||||
|
||||
if not instance:
|
||||
action = 'create'
|
||||
try:
|
||||
instance = cdb.create(name=name, flavor=flavor, volume=volume,
|
||||
type=cdb_type, version=cdb_version)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
else:
|
||||
action = None
|
||||
|
||||
if instance.volume.size != volume:
|
||||
action = 'resize'
|
||||
if instance.volume.size > volume:
|
||||
module.fail_json(changed=False, action=action,
|
||||
msg='The new volume size must be larger than '
|
||||
'the current volume size',
|
||||
cdb=rax_to_dict(instance))
|
||||
instance.resize_volume(volume)
|
||||
changed = True
|
||||
|
||||
if int(instance.flavor.id) != flavor:
|
||||
action = 'resize'
|
||||
pyrax.utils.wait_until(instance, 'status', 'ACTIVE',
|
||||
attempts=wait_timeout)
|
||||
instance.resize(flavor)
|
||||
changed = True
|
||||
|
||||
if wait:
|
||||
pyrax.utils.wait_until(instance, 'status', 'ACTIVE',
|
||||
attempts=wait_timeout)
|
||||
|
||||
if wait and instance.status != 'ACTIVE':
|
||||
module.fail_json(changed=changed, action=action,
|
||||
cdb=rax_to_dict(instance),
|
||||
msg='Timeout waiting for "%s" databases instance to '
|
||||
'be created' % name)
|
||||
|
||||
module.exit_json(changed=changed, action=action, cdb=rax_to_dict(instance))
|
||||
|
||||
|
||||
def delete_instance(module, name, wait, wait_timeout):
|
||||
|
||||
if not name:
|
||||
module.fail_json(msg='name is required for the "rax_cdb" module')
|
||||
|
||||
changed = False
|
||||
|
||||
instance = find_instance(name)
|
||||
if not instance:
|
||||
module.exit_json(changed=False, action='delete')
|
||||
|
||||
try:
|
||||
instance.delete()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
if wait:
|
||||
pyrax.utils.wait_until(instance, 'status', 'SHUTDOWN',
|
||||
attempts=wait_timeout)
|
||||
|
||||
if wait and instance.status != 'SHUTDOWN':
|
||||
module.fail_json(changed=changed, action='delete',
|
||||
cdb=rax_to_dict(instance),
|
||||
msg='Timeout waiting for "%s" databases instance to '
|
||||
'be deleted' % name)
|
||||
|
||||
module.exit_json(changed=changed, action='delete',
|
||||
cdb=rax_to_dict(instance))
|
||||
|
||||
|
||||
def rax_cdb(module, state, name, flavor, volume, cdb_type, cdb_version, wait,
|
||||
wait_timeout):
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
save_instance(module, name, flavor, volume, cdb_type, cdb_version, wait,
|
||||
wait_timeout)
|
||||
elif state == 'absent':
|
||||
delete_instance(module, name, wait, wait_timeout)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(type='str', required=True),
|
||||
flavor=dict(type='int', default=1),
|
||||
volume=dict(type='int', default=2),
|
||||
cdb_type=dict(type='str', default='MySQL', aliases=['type']),
|
||||
cdb_version=dict(type='str', default='5.6', aliases=['version']),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
name = module.params.get('name')
|
||||
flavor = module.params.get('flavor')
|
||||
volume = module.params.get('volume')
|
||||
cdb_type = module.params.get('cdb_type')
|
||||
cdb_version = module.params.get('cdb_version')
|
||||
state = module.params.get('state')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
rax_cdb(module, state, name, flavor, volume, cdb_type, cdb_version, wait, wait_timeout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
169
plugins/modules/cloud/rackspace/rax_cdb_database.py
Normal file
169
plugins/modules/cloud/rackspace/rax_cdb_database.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
module: rax_cdb_database
|
||||
short_description: 'create / delete a database in the Cloud Databases'
|
||||
description:
|
||||
- create / delete a database in the Cloud Databases.
|
||||
options:
|
||||
cdb_id:
|
||||
description:
|
||||
- The databases server UUID
|
||||
name:
|
||||
description:
|
||||
- Name to give to the database
|
||||
character_set:
|
||||
description:
|
||||
- Set of symbols and encodings
|
||||
default: 'utf8'
|
||||
collate:
|
||||
description:
|
||||
- Set of rules for comparing characters in a character set
|
||||
default: 'utf8_general_ci'
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
author: "Simon JAILLET (@jails)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a database in Cloud Databases
|
||||
tasks:
|
||||
- name: Database build request
|
||||
local_action:
|
||||
module: rax_cdb_database
|
||||
credentials: ~/.raxpub
|
||||
region: IAD
|
||||
cdb_id: 323e7ce0-9cb0-11e3-a5e2-0800200c9a66
|
||||
name: db1
|
||||
state: present
|
||||
register: rax_db_database
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, rax_to_dict, setup_rax_module
|
||||
|
||||
|
||||
def find_database(instance, name):
|
||||
try:
|
||||
database = instance.get_database(name)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return database
|
||||
|
||||
|
||||
def save_database(module, cdb_id, name, character_set, collate):
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
database = find_database(instance, name)
|
||||
|
||||
if not database:
|
||||
try:
|
||||
database = instance.create_database(name=name,
|
||||
character_set=character_set,
|
||||
collate=collate)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action='create',
|
||||
database=rax_to_dict(database))
|
||||
|
||||
|
||||
def delete_database(module, cdb_id, name):
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
database = find_database(instance, name)
|
||||
|
||||
if database:
|
||||
try:
|
||||
database.delete()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action='delete',
|
||||
database=rax_to_dict(database))
|
||||
|
||||
|
||||
def rax_cdb_database(module, state, cdb_id, name, character_set, collate):
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
save_database(module, cdb_id, name, character_set, collate)
|
||||
elif state == 'absent':
|
||||
delete_database(module, cdb_id, name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
cdb_id=dict(type='str', required=True),
|
||||
name=dict(type='str', required=True),
|
||||
character_set=dict(type='str', default='utf8'),
|
||||
collate=dict(type='str', default='utf8_general_ci'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
cdb_id = module.params.get('cdb_id')
|
||||
name = module.params.get('name')
|
||||
character_set = module.params.get('character_set')
|
||||
collate = module.params.get('collate')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
rax_cdb_database(module, state, cdb_id, name, character_set, collate)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
214
plugins/modules/cloud/rackspace/rax_cdb_user.py
Normal file
214
plugins/modules/cloud/rackspace/rax_cdb_user.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cdb_user
|
||||
short_description: create / delete a Rackspace Cloud Database
|
||||
description:
|
||||
- create / delete a database in the Cloud Databases.
|
||||
options:
|
||||
cdb_id:
|
||||
description:
|
||||
- The databases server UUID
|
||||
db_username:
|
||||
description:
|
||||
- Name of the database user
|
||||
db_password:
|
||||
description:
|
||||
- Database user password
|
||||
databases:
|
||||
description:
|
||||
- Name of the databases that the user can access
|
||||
default: []
|
||||
host:
|
||||
description:
|
||||
- Specifies the host from which a user is allowed to connect to
|
||||
the database. Possible values are a string containing an IPv4 address
|
||||
or "%" to allow connecting from any host
|
||||
default: '%'
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
author: "Simon JAILLET (@jails)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a user in Cloud Databases
|
||||
tasks:
|
||||
- name: User build request
|
||||
local_action:
|
||||
module: rax_cdb_user
|
||||
credentials: ~/.raxpub
|
||||
region: IAD
|
||||
cdb_id: 323e7ce0-9cb0-11e3-a5e2-0800200c9a66
|
||||
db_username: user1
|
||||
db_password: user1
|
||||
databases: ['db1']
|
||||
state: present
|
||||
register: rax_db_user
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, rax_to_dict, setup_rax_module
|
||||
|
||||
|
||||
def find_user(instance, name):
|
||||
try:
|
||||
user = instance.get_user(name)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def save_user(module, cdb_id, name, password, databases, host):
|
||||
|
||||
for arg, value in dict(cdb_id=cdb_id, name=name).items():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb_user" '
|
||||
'module' % arg)
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
user = find_user(instance, name)
|
||||
|
||||
if not user:
|
||||
action = 'create'
|
||||
try:
|
||||
user = instance.create_user(name=name,
|
||||
password=password,
|
||||
database_names=databases,
|
||||
host=host)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
else:
|
||||
action = 'update'
|
||||
|
||||
if user.host != host:
|
||||
changed = True
|
||||
|
||||
user.update(password=password, host=host)
|
||||
|
||||
former_dbs = set([item.name for item in user.list_user_access()])
|
||||
databases = set(databases)
|
||||
|
||||
if databases != former_dbs:
|
||||
try:
|
||||
revoke_dbs = [db for db in former_dbs if db not in databases]
|
||||
user.revoke_user_access(db_names=revoke_dbs)
|
||||
|
||||
new_dbs = [db for db in databases if db not in former_dbs]
|
||||
user.grant_user_access(db_names=new_dbs)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action=action, user=rax_to_dict(user))
|
||||
|
||||
|
||||
def delete_user(module, cdb_id, name):
|
||||
|
||||
for arg, value in dict(cdb_id=cdb_id, name=name).items():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb_user"'
|
||||
' module' % arg)
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
user = find_user(instance, name)
|
||||
|
||||
if user:
|
||||
try:
|
||||
user.delete()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action='delete')
|
||||
|
||||
|
||||
def rax_cdb_user(module, state, cdb_id, name, password, databases, host):
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
save_user(module, cdb_id, name, password, databases, host)
|
||||
elif state == 'absent':
|
||||
delete_user(module, cdb_id, name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
cdb_id=dict(type='str', required=True),
|
||||
db_username=dict(type='str', required=True),
|
||||
db_password=dict(type='str', required=True, no_log=True),
|
||||
databases=dict(type='list', default=[]),
|
||||
host=dict(type='str', default='%'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
cdb_id = module.params.get('cdb_id')
|
||||
name = module.params.get('db_username')
|
||||
password = module.params.get('db_password')
|
||||
databases = module.params.get('databases')
|
||||
host = to_text(module.params.get('host'), errors='surrogate_or_strict')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
rax_cdb_user(module, state, cdb_id, name, password, databases, host)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
305
plugins/modules/cloud/rackspace/rax_clb.py
Normal file
305
plugins/modules/cloud/rackspace/rax_clb.py
Normal file
|
@ -0,0 +1,305 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_clb
|
||||
short_description: create / delete a load balancer in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud load balancer.
|
||||
options:
|
||||
algorithm:
|
||||
description:
|
||||
- algorithm for the balancer being created
|
||||
choices:
|
||||
- RANDOM
|
||||
- LEAST_CONNECTIONS
|
||||
- ROUND_ROBIN
|
||||
- WEIGHTED_LEAST_CONNECTIONS
|
||||
- WEIGHTED_ROUND_ROBIN
|
||||
default: LEAST_CONNECTIONS
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
name:
|
||||
description:
|
||||
- Name to give the load balancer
|
||||
port:
|
||||
description:
|
||||
- Port for the balancer being created
|
||||
default: 80
|
||||
protocol:
|
||||
description:
|
||||
- Protocol for the balancer being created
|
||||
choices:
|
||||
- DNS_TCP
|
||||
- DNS_UDP
|
||||
- FTP
|
||||
- HTTP
|
||||
- HTTPS
|
||||
- IMAPS
|
||||
- IMAPv4
|
||||
- LDAP
|
||||
- LDAPS
|
||||
- MYSQL
|
||||
- POP3
|
||||
- POP3S
|
||||
- SMTP
|
||||
- TCP
|
||||
- TCP_CLIENT_FIRST
|
||||
- UDP
|
||||
- UDP_STREAM
|
||||
- SFTP
|
||||
default: HTTP
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
timeout:
|
||||
description:
|
||||
- timeout for communication between the balancer and the node
|
||||
default: 30
|
||||
type:
|
||||
description:
|
||||
- type of interface for the balancer being created
|
||||
choices:
|
||||
- PUBLIC
|
||||
- SERVICENET
|
||||
default: PUBLIC
|
||||
vip_id:
|
||||
description:
|
||||
- Virtual IP ID to use when creating the load balancer for purposes of
|
||||
sharing an IP with another load balancer of another protocol
|
||||
wait:
|
||||
description:
|
||||
- wait for the balancer to be in state 'running' before returning
|
||||
type: bool
|
||||
default: 'no'
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author:
|
||||
- "Christopher H. Laco (@claco)"
|
||||
- "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Load Balancer
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Load Balancer create request
|
||||
local_action:
|
||||
module: rax_clb
|
||||
credentials: ~/.raxpub
|
||||
name: my-lb
|
||||
port: 8080
|
||||
protocol: HTTP
|
||||
type: SERVICENET
|
||||
timeout: 30
|
||||
region: DFW
|
||||
wait: yes
|
||||
state: present
|
||||
meta:
|
||||
app: my-cool-app
|
||||
register: my_lb
|
||||
'''
|
||||
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (CLB_ALGORITHMS,
|
||||
CLB_PROTOCOLS,
|
||||
rax_argument_spec,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def cloud_load_balancer(module, state, name, meta, algorithm, port, protocol,
|
||||
vip_type, timeout, wait, wait_timeout, vip_id):
|
||||
if int(timeout) < 30:
|
||||
module.fail_json(msg='"timeout" must be greater than or equal to 30')
|
||||
|
||||
changed = False
|
||||
balancers = []
|
||||
|
||||
clb = pyrax.cloud_loadbalancers
|
||||
if not clb:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
balancer_list = clb.list()
|
||||
while balancer_list:
|
||||
retrieved = clb.list(marker=balancer_list.pop().id)
|
||||
balancer_list.extend(retrieved)
|
||||
if len(retrieved) < 2:
|
||||
break
|
||||
|
||||
for balancer in balancer_list:
|
||||
if name != balancer.name and name != balancer.id:
|
||||
continue
|
||||
|
||||
balancers.append(balancer)
|
||||
|
||||
if len(balancers) > 1:
|
||||
module.fail_json(msg='Multiple Load Balancers were matched by name, '
|
||||
'try using the Load Balancer ID instead')
|
||||
|
||||
if state == 'present':
|
||||
if isinstance(meta, dict):
|
||||
metadata = [dict(key=k, value=v) for k, v in meta.items()]
|
||||
|
||||
if not balancers:
|
||||
try:
|
||||
virtual_ips = [clb.VirtualIP(type=vip_type, id=vip_id)]
|
||||
balancer = clb.create(name, metadata=metadata, port=port,
|
||||
algorithm=algorithm, protocol=protocol,
|
||||
timeout=timeout, virtual_ips=virtual_ips)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
balancer = balancers[0]
|
||||
setattr(balancer, 'metadata',
|
||||
[dict(key=k, value=v) for k, v in
|
||||
balancer.get_metadata().items()])
|
||||
atts = {
|
||||
'name': name,
|
||||
'algorithm': algorithm,
|
||||
'port': port,
|
||||
'protocol': protocol,
|
||||
'timeout': timeout
|
||||
}
|
||||
for att, value in atts.items():
|
||||
current = getattr(balancer, att)
|
||||
if current != value:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
balancer.update(**atts)
|
||||
|
||||
if balancer.metadata != metadata:
|
||||
balancer.set_metadata(meta)
|
||||
changed = True
|
||||
|
||||
virtual_ips = [clb.VirtualIP(type=vip_type)]
|
||||
current_vip_types = set([v.type for v in balancer.virtual_ips])
|
||||
vip_types = set([v.type for v in virtual_ips])
|
||||
if current_vip_types != vip_types:
|
||||
module.fail_json(msg='Load balancer Virtual IP type cannot '
|
||||
'be changed')
|
||||
|
||||
if wait:
|
||||
attempts = wait_timeout // 5
|
||||
pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts)
|
||||
|
||||
balancer.get()
|
||||
instance = rax_to_dict(balancer, 'clb')
|
||||
|
||||
result = dict(changed=changed, balancer=instance)
|
||||
|
||||
if balancer.status == 'ERROR':
|
||||
result['msg'] = '%s failed to build' % balancer.id
|
||||
elif wait and balancer.status not in ('ACTIVE', 'ERROR'):
|
||||
result['msg'] = 'Timeout waiting on %s' % balancer.id
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
if balancers:
|
||||
balancer = balancers[0]
|
||||
try:
|
||||
balancer.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
instance = rax_to_dict(balancer, 'clb')
|
||||
|
||||
if wait:
|
||||
attempts = wait_timeout // 5
|
||||
pyrax.utils.wait_until(balancer, 'status', ('DELETED'),
|
||||
interval=5, attempts=attempts)
|
||||
else:
|
||||
instance = {}
|
||||
|
||||
module.exit_json(changed=changed, balancer=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
algorithm=dict(choices=CLB_ALGORITHMS,
|
||||
default='LEAST_CONNECTIONS'),
|
||||
meta=dict(type='dict', default={}),
|
||||
name=dict(required=True),
|
||||
port=dict(type='int', default=80),
|
||||
protocol=dict(choices=CLB_PROTOCOLS, default='HTTP'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
timeout=dict(type='int', default=30),
|
||||
type=dict(choices=['PUBLIC', 'SERVICENET'], default='PUBLIC'),
|
||||
vip_id=dict(),
|
||||
wait=dict(type='bool'),
|
||||
wait_timeout=dict(type='int', default=300),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
algorithm = module.params.get('algorithm')
|
||||
meta = module.params.get('meta')
|
||||
name = module.params.get('name')
|
||||
port = module.params.get('port')
|
||||
protocol = module.params.get('protocol')
|
||||
state = module.params.get('state')
|
||||
timeout = int(module.params.get('timeout'))
|
||||
vip_id = module.params.get('vip_id')
|
||||
vip_type = module.params.get('type')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_load_balancer(module, state, name, meta, algorithm, port, protocol,
|
||||
vip_type, timeout, wait, wait_timeout, vip_id)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
274
plugins/modules/cloud/rackspace/rax_clb_nodes.py
Normal file
274
plugins/modules/cloud/rackspace/rax_clb_nodes.py
Normal file
|
@ -0,0 +1,274 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_clb_nodes
|
||||
short_description: add, modify and remove nodes from a Rackspace Cloud Load Balancer
|
||||
description:
|
||||
- Adds, modifies and removes nodes from a Rackspace Cloud Load Balancer
|
||||
options:
|
||||
address:
|
||||
required: false
|
||||
description:
|
||||
- IP address or domain name of the node
|
||||
condition:
|
||||
required: false
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
- draining
|
||||
description:
|
||||
- Condition for the node, which determines its role within the load
|
||||
balancer
|
||||
load_balancer_id:
|
||||
required: true
|
||||
description:
|
||||
- Load balancer id
|
||||
node_id:
|
||||
required: false
|
||||
description:
|
||||
- Node id
|
||||
port:
|
||||
required: false
|
||||
description:
|
||||
- Port number of the load balanced service on the node
|
||||
state:
|
||||
required: false
|
||||
default: "present"
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
description:
|
||||
- Indicate desired state of the node
|
||||
type:
|
||||
required: false
|
||||
choices:
|
||||
- primary
|
||||
- secondary
|
||||
description:
|
||||
- Type of node
|
||||
wait:
|
||||
required: false
|
||||
default: "no"
|
||||
type: bool
|
||||
description:
|
||||
- Wait for the load balancer to become active before returning
|
||||
wait_timeout:
|
||||
required: false
|
||||
default: 30
|
||||
description:
|
||||
- How long to wait before giving up and returning an error
|
||||
weight:
|
||||
required: false
|
||||
description:
|
||||
- Weight of node
|
||||
author: "Lukasz Kawczynski (@neuroid)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add a new node to the load balancer
|
||||
- local_action:
|
||||
module: rax_clb_nodes
|
||||
load_balancer_id: 71
|
||||
address: 10.2.2.3
|
||||
port: 80
|
||||
condition: enabled
|
||||
type: primary
|
||||
wait: yes
|
||||
credentials: /path/to/credentials
|
||||
|
||||
# Drain connections from a node
|
||||
- local_action:
|
||||
module: rax_clb_nodes
|
||||
load_balancer_id: 71
|
||||
node_id: 410
|
||||
condition: draining
|
||||
wait: yes
|
||||
credentials: /path/to/credentials
|
||||
|
||||
# Remove a node from the load balancer
|
||||
- local_action:
|
||||
module: rax_clb_nodes
|
||||
load_balancer_id: 71
|
||||
node_id: 410
|
||||
state: absent
|
||||
wait: yes
|
||||
credentials: /path/to/credentials
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_clb_node_to_dict, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def _activate_virtualenv(path):
|
||||
activate_this = os.path.join(path, 'bin', 'activate_this.py')
|
||||
with open(activate_this) as f:
|
||||
code = compile(f.read(), activate_this, 'exec')
|
||||
exec(code)
|
||||
|
||||
|
||||
def _get_node(lb, node_id=None, address=None, port=None):
|
||||
"""Return a matching node"""
|
||||
for node in getattr(lb, 'nodes', []):
|
||||
match_list = []
|
||||
if node_id is not None:
|
||||
match_list.append(getattr(node, 'id', None) == node_id)
|
||||
if address is not None:
|
||||
match_list.append(getattr(node, 'address', None) == address)
|
||||
if port is not None:
|
||||
match_list.append(getattr(node, 'port', None) == port)
|
||||
|
||||
if match_list and all(match_list):
|
||||
return node
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
address=dict(),
|
||||
condition=dict(choices=['enabled', 'disabled', 'draining']),
|
||||
load_balancer_id=dict(required=True, type='int'),
|
||||
node_id=dict(type='int'),
|
||||
port=dict(type='int'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
type=dict(choices=['primary', 'secondary']),
|
||||
virtualenv=dict(type='path'),
|
||||
wait=dict(default=False, type='bool'),
|
||||
wait_timeout=dict(default=30, type='int'),
|
||||
weight=dict(type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
address = module.params['address']
|
||||
condition = (module.params['condition'] and
|
||||
module.params['condition'].upper())
|
||||
load_balancer_id = module.params['load_balancer_id']
|
||||
node_id = module.params['node_id']
|
||||
port = module.params['port']
|
||||
state = module.params['state']
|
||||
typ = module.params['type'] and module.params['type'].upper()
|
||||
virtualenv = module.params['virtualenv']
|
||||
wait = module.params['wait']
|
||||
wait_timeout = module.params['wait_timeout'] or 1
|
||||
weight = module.params['weight']
|
||||
|
||||
if virtualenv:
|
||||
try:
|
||||
_activate_virtualenv(virtualenv)
|
||||
except IOError as e:
|
||||
module.fail_json(msg='Failed to activate virtualenv %s (%s)' % (
|
||||
virtualenv, e))
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
if not pyrax.cloud_loadbalancers:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
try:
|
||||
lb = pyrax.cloud_loadbalancers.get(load_balancer_id)
|
||||
except pyrax.exc.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
node = _get_node(lb, node_id, address, port)
|
||||
|
||||
result = rax_clb_node_to_dict(node)
|
||||
|
||||
if state == 'absent':
|
||||
if not node: # Removing a non-existent node
|
||||
module.exit_json(changed=False, state=state)
|
||||
try:
|
||||
lb.delete_node(node)
|
||||
result = {}
|
||||
except pyrax.exc.NotFound:
|
||||
module.exit_json(changed=False, state=state)
|
||||
except pyrax.exc.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else: # present
|
||||
if not node:
|
||||
if node_id: # Updating a non-existent node
|
||||
msg = 'Node %d not found' % node_id
|
||||
if lb.nodes:
|
||||
msg += (' (available nodes: %s)' %
|
||||
', '.join([str(x.id) for x in lb.nodes]))
|
||||
module.fail_json(msg=msg)
|
||||
else: # Creating a new node
|
||||
try:
|
||||
node = pyrax.cloudloadbalancers.Node(
|
||||
address=address, port=port, condition=condition,
|
||||
weight=weight, type=typ)
|
||||
resp, body = lb.add_nodes([node])
|
||||
result.update(body['nodes'][0])
|
||||
except pyrax.exc.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else: # Updating an existing node
|
||||
mutable = {
|
||||
'condition': condition,
|
||||
'type': typ,
|
||||
'weight': weight,
|
||||
}
|
||||
|
||||
for name, value in mutable.items():
|
||||
if value is None or value == getattr(node, name):
|
||||
mutable.pop(name)
|
||||
|
||||
if not mutable:
|
||||
module.exit_json(changed=False, state=state, node=result)
|
||||
|
||||
try:
|
||||
# The diff has to be set explicitly to update node's weight and
|
||||
# type; this should probably be fixed in pyrax
|
||||
lb.update_node(node, diff=mutable)
|
||||
result.update(mutable)
|
||||
except pyrax.exc.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if wait:
|
||||
pyrax.utils.wait_until(lb, "status", "ACTIVE", interval=1,
|
||||
attempts=wait_timeout)
|
||||
if lb.status != 'ACTIVE':
|
||||
module.fail_json(
|
||||
msg='Load balancer not active after %ds (current status: %s)' %
|
||||
(wait_timeout, lb.status.lower()))
|
||||
|
||||
kwargs = {'node': result} if result else {}
|
||||
module.exit_json(changed=True, state=state, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
279
plugins/modules/cloud/rackspace/rax_clb_ssl.py
Normal file
279
plugins/modules/cloud/rackspace/rax_clb_ssl.py
Normal file
|
@ -0,0 +1,279 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
module: rax_clb_ssl
|
||||
short_description: Manage SSL termination for a Rackspace Cloud Load Balancer.
|
||||
description:
|
||||
- Set up, reconfigure, or remove SSL termination for an existing load balancer.
|
||||
options:
|
||||
loadbalancer:
|
||||
description:
|
||||
- Name or ID of the load balancer on which to manage SSL termination.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- If set to "present", SSL termination will be added to this load balancer.
|
||||
- If "absent", SSL termination will be removed instead.
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
enabled:
|
||||
description:
|
||||
- If set to "false", temporarily disable SSL termination without discarding
|
||||
- existing credentials.
|
||||
default: true
|
||||
type: bool
|
||||
private_key:
|
||||
description:
|
||||
- The private SSL key as a string in PEM format.
|
||||
certificate:
|
||||
description:
|
||||
- The public SSL certificates as a string in PEM format.
|
||||
intermediate_certificate:
|
||||
description:
|
||||
- One or more intermediate certificate authorities as a string in PEM
|
||||
- format, concatenated into a single string.
|
||||
secure_port:
|
||||
description:
|
||||
- The port to listen for secure traffic.
|
||||
default: 443
|
||||
secure_traffic_only:
|
||||
description:
|
||||
- If "true", the load balancer will *only* accept secure traffic.
|
||||
default: false
|
||||
type: bool
|
||||
https_redirect:
|
||||
description:
|
||||
- If "true", the load balancer will redirect HTTP traffic to HTTPS.
|
||||
- Requires "secure_traffic_only" to be true. Incurs an implicit wait if SSL
|
||||
- termination is also applied or removed.
|
||||
type: bool
|
||||
wait:
|
||||
description:
|
||||
- Wait for the balancer to be in state "running" before turning.
|
||||
default: false
|
||||
type: bool
|
||||
wait_timeout:
|
||||
description:
|
||||
- How long before "wait" gives up, in seconds.
|
||||
default: 300
|
||||
author: Ash Wilson (@smashwilson)
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Enable SSL termination on a load balancer
|
||||
rax_clb_ssl:
|
||||
loadbalancer: the_loadbalancer
|
||||
state: present
|
||||
private_key: "{{ lookup('file', 'credentials/server.key' ) }}"
|
||||
certificate: "{{ lookup('file', 'credentials/server.crt' ) }}"
|
||||
intermediate_certificate: "{{ lookup('file', 'credentials/trust-chain.crt') }}"
|
||||
secure_traffic_only: true
|
||||
wait: true
|
||||
|
||||
- name: Disable SSL termination
|
||||
rax_clb_ssl:
|
||||
loadbalancer: "{{ registered_lb.balancer.id }}"
|
||||
state: absent
|
||||
wait: true
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec,
|
||||
rax_find_loadbalancer,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key,
|
||||
certificate, intermediate_certificate, secure_port,
|
||||
secure_traffic_only, https_redirect,
|
||||
wait, wait_timeout):
|
||||
# Validate arguments.
|
||||
|
||||
if state == 'present':
|
||||
if not private_key:
|
||||
module.fail_json(msg="private_key must be provided.")
|
||||
else:
|
||||
private_key = private_key.strip()
|
||||
|
||||
if not certificate:
|
||||
module.fail_json(msg="certificate must be provided.")
|
||||
else:
|
||||
certificate = certificate.strip()
|
||||
|
||||
attempts = wait_timeout // 5
|
||||
|
||||
# Locate the load balancer.
|
||||
|
||||
balancer = rax_find_loadbalancer(module, pyrax, loadbalancer)
|
||||
existing_ssl = balancer.get_ssl_termination()
|
||||
|
||||
changed = False
|
||||
|
||||
if state == 'present':
|
||||
# Apply or reconfigure SSL termination on the load balancer.
|
||||
ssl_attrs = dict(
|
||||
securePort=secure_port,
|
||||
privatekey=private_key,
|
||||
certificate=certificate,
|
||||
intermediateCertificate=intermediate_certificate,
|
||||
enabled=enabled,
|
||||
secureTrafficOnly=secure_traffic_only
|
||||
)
|
||||
|
||||
needs_change = False
|
||||
|
||||
if existing_ssl:
|
||||
for ssl_attr, value in ssl_attrs.items():
|
||||
if ssl_attr == 'privatekey':
|
||||
# The private key is not included in get_ssl_termination's
|
||||
# output (as it shouldn't be). Also, if you're changing the
|
||||
# private key, you'll also be changing the certificate,
|
||||
# so we don't lose anything by not checking it.
|
||||
continue
|
||||
|
||||
if value is not None and existing_ssl.get(ssl_attr) != value:
|
||||
# module.fail_json(msg='Unnecessary change', attr=ssl_attr, value=value, existing=existing_ssl.get(ssl_attr))
|
||||
needs_change = True
|
||||
else:
|
||||
needs_change = True
|
||||
|
||||
if needs_change:
|
||||
try:
|
||||
balancer.add_ssl_termination(**ssl_attrs)
|
||||
except pyrax.exceptions.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
changed = True
|
||||
elif state == 'absent':
|
||||
# Remove SSL termination if it's already configured.
|
||||
if existing_ssl:
|
||||
try:
|
||||
balancer.delete_ssl_termination()
|
||||
except pyrax.exceptions.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
changed = True
|
||||
|
||||
if https_redirect is not None and balancer.httpsRedirect != https_redirect:
|
||||
if changed:
|
||||
# This wait is unavoidable because load balancers are immutable
|
||||
# while the SSL termination changes above are being applied.
|
||||
pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts)
|
||||
|
||||
try:
|
||||
balancer.update(httpsRedirect=https_redirect)
|
||||
except pyrax.exceptions.PyraxException as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
changed = True
|
||||
|
||||
if changed and wait:
|
||||
pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts)
|
||||
|
||||
balancer.get()
|
||||
new_ssl_termination = balancer.get_ssl_termination()
|
||||
|
||||
# Intentionally omit the private key from the module output, so you don't
|
||||
# accidentally echo it with `ansible-playbook -v` or `debug`, and the
|
||||
# certificate, which is just long. Convert other attributes to snake_case
|
||||
# and include https_redirect at the top-level.
|
||||
if new_ssl_termination:
|
||||
new_ssl = dict(
|
||||
enabled=new_ssl_termination['enabled'],
|
||||
secure_port=new_ssl_termination['securePort'],
|
||||
secure_traffic_only=new_ssl_termination['secureTrafficOnly']
|
||||
)
|
||||
else:
|
||||
new_ssl = None
|
||||
|
||||
result = dict(
|
||||
changed=changed,
|
||||
https_redirect=balancer.httpsRedirect,
|
||||
ssl_termination=new_ssl,
|
||||
balancer=rax_to_dict(balancer, 'clb')
|
||||
)
|
||||
success = True
|
||||
|
||||
if balancer.status == 'ERROR':
|
||||
result['msg'] = '%s failed to build' % balancer.id
|
||||
success = False
|
||||
elif wait and balancer.status not in ('ACTIVE', 'ERROR'):
|
||||
result['msg'] = 'Timeout waiting on %s' % balancer.id
|
||||
success = False
|
||||
|
||||
if success:
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
module.fail_json(**result)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
loadbalancer=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
enabled=dict(type='bool', default=True),
|
||||
private_key=dict(),
|
||||
certificate=dict(),
|
||||
intermediate_certificate=dict(),
|
||||
secure_port=dict(type='int', default=443),
|
||||
secure_traffic_only=dict(type='bool', default=False),
|
||||
https_redirect=dict(type='bool'),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300)
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module.')
|
||||
|
||||
loadbalancer = module.params.get('loadbalancer')
|
||||
state = module.params.get('state')
|
||||
enabled = module.boolean(module.params.get('enabled'))
|
||||
private_key = module.params.get('private_key')
|
||||
certificate = module.params.get('certificate')
|
||||
intermediate_certificate = module.params.get('intermediate_certificate')
|
||||
secure_port = module.params.get('secure_port')
|
||||
secure_traffic_only = module.boolean(module.params.get('secure_traffic_only'))
|
||||
https_redirect = module.boolean(module.params.get('https_redirect'))
|
||||
wait = module.boolean(module.params.get('wait'))
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_load_balancer_ssl(
|
||||
module, loadbalancer, state, enabled, private_key, certificate,
|
||||
intermediate_certificate, secure_port, secure_traffic_only,
|
||||
https_redirect, wait, wait_timeout
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
172
plugins/modules/cloud/rackspace/rax_dns.py
Normal file
172
plugins/modules/cloud/rackspace/rax_dns.py
Normal file
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_dns
|
||||
short_description: Manage domains on Rackspace Cloud DNS
|
||||
description:
|
||||
- Manage domains on Rackspace Cloud DNS
|
||||
options:
|
||||
comment:
|
||||
description:
|
||||
- Brief description of the domain. Maximum length of 160 characters
|
||||
email:
|
||||
description:
|
||||
- Email address of the domain administrator
|
||||
name:
|
||||
description:
|
||||
- Domain name to create
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
ttl:
|
||||
description:
|
||||
- Time to live of domain in seconds
|
||||
default: 3600
|
||||
notes:
|
||||
- "It is recommended that plays utilizing this module be run with
|
||||
C(serial: 1) to avoid exceeding the API request limit imposed by
|
||||
the Rackspace CloudDNS API"
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create domain
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Domain create request
|
||||
local_action:
|
||||
module: rax_dns
|
||||
credentials: ~/.raxpub
|
||||
name: example.org
|
||||
email: admin@example.org
|
||||
register: rax_dns
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def rax_dns(module, comment, email, name, state, ttl):
|
||||
changed = False
|
||||
|
||||
dns = pyrax.cloud_dns
|
||||
if not dns:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
if not email:
|
||||
module.fail_json(msg='An "email" attribute is required for '
|
||||
'creating a domain')
|
||||
|
||||
try:
|
||||
domain = dns.find(name=name)
|
||||
except pyrax.exceptions.NoUniqueMatch as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except pyrax.exceptions.NotFound:
|
||||
try:
|
||||
domain = dns.create(name=name, emailAddress=email, ttl=ttl,
|
||||
comment=comment)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
update = {}
|
||||
if comment != getattr(domain, 'comment', None):
|
||||
update['comment'] = comment
|
||||
if ttl != getattr(domain, 'ttl', None):
|
||||
update['ttl'] = ttl
|
||||
if email != getattr(domain, 'emailAddress', None):
|
||||
update['emailAddress'] = email
|
||||
|
||||
if update:
|
||||
try:
|
||||
domain.update(**update)
|
||||
changed = True
|
||||
domain.get()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
domain = dns.find(name=name)
|
||||
except pyrax.exceptions.NotFound:
|
||||
domain = {}
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if domain:
|
||||
try:
|
||||
domain.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, domain=rax_to_dict(domain))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
comment=dict(),
|
||||
email=dict(),
|
||||
name=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
ttl=dict(type='int', default=3600),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
comment = module.params.get('comment')
|
||||
email = module.params.get('email')
|
||||
name = module.params.get('name')
|
||||
state = module.params.get('state')
|
||||
ttl = module.params.get('ttl')
|
||||
|
||||
setup_rax_module(module, pyrax, False)
|
||||
|
||||
rax_dns(module, comment, email, name, state, ttl)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
347
plugins/modules/cloud/rackspace/rax_dns_record.py
Normal file
347
plugins/modules/cloud/rackspace/rax_dns_record.py
Normal file
|
@ -0,0 +1,347 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_dns_record
|
||||
short_description: Manage DNS records on Rackspace Cloud DNS
|
||||
description:
|
||||
- Manage DNS records on Rackspace Cloud DNS
|
||||
options:
|
||||
comment:
|
||||
description:
|
||||
- Brief description of the domain. Maximum length of 160 characters
|
||||
data:
|
||||
description:
|
||||
- IP address for A/AAAA record, FQDN for CNAME/MX/NS, or text data for
|
||||
SRV/TXT
|
||||
required: True
|
||||
domain:
|
||||
description:
|
||||
- Domain name to create the record in. This is an invalid option when
|
||||
type=PTR
|
||||
loadbalancer:
|
||||
description:
|
||||
- Load Balancer ID to create a PTR record for. Only used with type=PTR
|
||||
name:
|
||||
description:
|
||||
- FQDN record name to create
|
||||
required: True
|
||||
overwrite:
|
||||
description:
|
||||
- Add new records if data doesn't match, instead of updating existing
|
||||
record with matching name. If there are already multiple records with
|
||||
matching name and overwrite=true, this module will fail.
|
||||
default: true
|
||||
type: bool
|
||||
priority:
|
||||
description:
|
||||
- Required for MX and SRV records, but forbidden for other record types.
|
||||
If specified, must be an integer from 0 to 65535.
|
||||
server:
|
||||
description:
|
||||
- Server ID to create a PTR record for. Only used with type=PTR
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
ttl:
|
||||
description:
|
||||
- Time to live of record in seconds
|
||||
default: 3600
|
||||
type:
|
||||
description:
|
||||
- DNS record type
|
||||
choices:
|
||||
- A
|
||||
- AAAA
|
||||
- CNAME
|
||||
- MX
|
||||
- NS
|
||||
- SRV
|
||||
- TXT
|
||||
- PTR
|
||||
required: true
|
||||
notes:
|
||||
- "It is recommended that plays utilizing this module be run with
|
||||
C(serial: 1) to avoid exceeding the API request limit imposed by
|
||||
the Rackspace CloudDNS API"
|
||||
- To manipulate a C(PTR) record either C(loadbalancer) or C(server) must be
|
||||
supplied
|
||||
- As of version 1.7, the C(type) field is required and no longer defaults to an C(A) record.
|
||||
- C(PTR) record support was added in version 1.7
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create DNS Records
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Create A record
|
||||
local_action:
|
||||
module: rax_dns_record
|
||||
credentials: ~/.raxpub
|
||||
domain: example.org
|
||||
name: www.example.org
|
||||
data: "{{ rax_accessipv4 }}"
|
||||
type: A
|
||||
register: a_record
|
||||
|
||||
- name: Create PTR record
|
||||
local_action:
|
||||
module: rax_dns_record
|
||||
credentials: ~/.raxpub
|
||||
server: "{{ rax_id }}"
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
register: ptr_record
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec,
|
||||
rax_find_loadbalancer,
|
||||
rax_find_server,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def rax_dns_record_ptr(module, data=None, comment=None, loadbalancer=None,
|
||||
name=None, server=None, state='present', ttl=7200):
|
||||
changed = False
|
||||
results = []
|
||||
|
||||
dns = pyrax.cloud_dns
|
||||
|
||||
if not dns:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if loadbalancer:
|
||||
item = rax_find_loadbalancer(module, pyrax, loadbalancer)
|
||||
elif server:
|
||||
item = rax_find_server(module, pyrax, server)
|
||||
|
||||
if state == 'present':
|
||||
current = dns.list_ptr_records(item)
|
||||
for record in current:
|
||||
if record.data == data:
|
||||
if record.ttl != ttl or record.name != name:
|
||||
try:
|
||||
dns.update_ptr_record(item, record, name, data, ttl)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
record.ttl = ttl
|
||||
record.name = name
|
||||
results.append(rax_to_dict(record))
|
||||
break
|
||||
else:
|
||||
results.append(rax_to_dict(record))
|
||||
break
|
||||
|
||||
if not results:
|
||||
record = dict(name=name, type='PTR', data=data, ttl=ttl,
|
||||
comment=comment)
|
||||
try:
|
||||
results = dns.add_ptr_records(item, [record])
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, records=results)
|
||||
|
||||
elif state == 'absent':
|
||||
current = dns.list_ptr_records(item)
|
||||
for record in current:
|
||||
if record.data == data:
|
||||
results.append(rax_to_dict(record))
|
||||
break
|
||||
|
||||
if results:
|
||||
try:
|
||||
dns.delete_ptr_records(item, data)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, records=results)
|
||||
|
||||
|
||||
def rax_dns_record(module, comment=None, data=None, domain=None, name=None,
|
||||
overwrite=True, priority=None, record_type='A',
|
||||
state='present', ttl=7200):
|
||||
"""Function for manipulating record types other than PTR"""
|
||||
|
||||
changed = False
|
||||
|
||||
dns = pyrax.cloud_dns
|
||||
if not dns:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
if not priority and record_type in ['MX', 'SRV']:
|
||||
module.fail_json(msg='A "priority" attribute is required for '
|
||||
'creating a MX or SRV record')
|
||||
|
||||
try:
|
||||
domain = dns.find(name=domain)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
try:
|
||||
if overwrite:
|
||||
record = domain.find_record(record_type, name=name)
|
||||
else:
|
||||
record = domain.find_record(record_type, name=name, data=data)
|
||||
except pyrax.exceptions.DomainRecordNotUnique as e:
|
||||
module.fail_json(msg='overwrite=true and there are multiple matching records')
|
||||
except pyrax.exceptions.DomainRecordNotFound as e:
|
||||
try:
|
||||
record_data = {
|
||||
'type': record_type,
|
||||
'name': name,
|
||||
'data': data,
|
||||
'ttl': ttl
|
||||
}
|
||||
if comment:
|
||||
record_data.update(dict(comment=comment))
|
||||
if priority and record_type.upper() in ['MX', 'SRV']:
|
||||
record_data.update(dict(priority=priority))
|
||||
|
||||
record = domain.add_records([record_data])[0]
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
update = {}
|
||||
if comment != getattr(record, 'comment', None):
|
||||
update['comment'] = comment
|
||||
if ttl != getattr(record, 'ttl', None):
|
||||
update['ttl'] = ttl
|
||||
if priority != getattr(record, 'priority', None):
|
||||
update['priority'] = priority
|
||||
if data != getattr(record, 'data', None):
|
||||
update['data'] = data
|
||||
|
||||
if update:
|
||||
try:
|
||||
record.update(**update)
|
||||
changed = True
|
||||
record.get()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
domain = dns.find(name=domain)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
try:
|
||||
record = domain.find_record(record_type, name=name, data=data)
|
||||
except pyrax.exceptions.DomainRecordNotFound as e:
|
||||
record = {}
|
||||
except pyrax.exceptions.DomainRecordNotUnique as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if record:
|
||||
try:
|
||||
record.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, record=rax_to_dict(record))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
comment=dict(),
|
||||
data=dict(required=True),
|
||||
domain=dict(),
|
||||
loadbalancer=dict(),
|
||||
name=dict(required=True),
|
||||
overwrite=dict(type='bool', default=True),
|
||||
priority=dict(type='int'),
|
||||
server=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
ttl=dict(type='int', default=3600),
|
||||
type=dict(required=True, choices=['A', 'AAAA', 'CNAME', 'MX', 'NS',
|
||||
'SRV', 'TXT', 'PTR'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[
|
||||
['server', 'loadbalancer', 'domain'],
|
||||
],
|
||||
required_one_of=[
|
||||
['server', 'loadbalancer', 'domain'],
|
||||
],
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
comment = module.params.get('comment')
|
||||
data = module.params.get('data')
|
||||
domain = module.params.get('domain')
|
||||
loadbalancer = module.params.get('loadbalancer')
|
||||
name = module.params.get('name')
|
||||
overwrite = module.params.get('overwrite')
|
||||
priority = module.params.get('priority')
|
||||
server = module.params.get('server')
|
||||
state = module.params.get('state')
|
||||
ttl = module.params.get('ttl')
|
||||
record_type = module.params.get('type')
|
||||
|
||||
setup_rax_module(module, pyrax, False)
|
||||
|
||||
if record_type.upper() == 'PTR':
|
||||
if not server and not loadbalancer:
|
||||
module.fail_json(msg='one of the following is required: '
|
||||
'server,loadbalancer')
|
||||
rax_dns_record_ptr(module, data=data, comment=comment,
|
||||
loadbalancer=loadbalancer, name=name, server=server,
|
||||
state=state, ttl=ttl)
|
||||
else:
|
||||
rax_dns_record(module, comment=comment, data=data, domain=domain,
|
||||
name=name, overwrite=overwrite, priority=priority,
|
||||
record_type=record_type, state=state, ttl=ttl)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
144
plugins/modules/cloud/rackspace/rax_facts.py
Normal file
144
plugins/modules/cloud/rackspace/rax_facts.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_facts
|
||||
short_description: Gather facts for Rackspace Cloud Servers
|
||||
description:
|
||||
- Gather facts for Rackspace Cloud Servers.
|
||||
options:
|
||||
address:
|
||||
description:
|
||||
- Server IP address to retrieve facts for, will match any IP assigned to
|
||||
the server
|
||||
id:
|
||||
description:
|
||||
- Server ID to retrieve facts for
|
||||
name:
|
||||
description:
|
||||
- Server name to retrieve facts for
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Gather info about servers
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Get facts about servers
|
||||
local_action:
|
||||
module: rax_facts
|
||||
credentials: ~/.raxpub
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
- name: Map some facts
|
||||
set_fact:
|
||||
ansible_ssh_host: "{{ rax_accessipv4 }}"
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def rax_facts(module, address, name, server_id):
|
||||
changed = False
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
ansible_facts = {}
|
||||
|
||||
search_opts = {}
|
||||
if name:
|
||||
search_opts = dict(name='^%s$' % name)
|
||||
try:
|
||||
servers = cs.servers.list(search_opts=search_opts)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif address:
|
||||
servers = []
|
||||
try:
|
||||
for server in cs.servers.list():
|
||||
for addresses in server.networks.values():
|
||||
if address in addresses:
|
||||
servers.append(server)
|
||||
break
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif server_id:
|
||||
servers = []
|
||||
try:
|
||||
servers.append(cs.servers.get(server_id))
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
servers[:] = [server for server in servers if server.status != "DELETED"]
|
||||
|
||||
if len(servers) > 1:
|
||||
module.fail_json(msg='Multiple servers found matching provided '
|
||||
'search parameters')
|
||||
elif len(servers) == 1:
|
||||
ansible_facts = rax_to_dict(servers[0], 'server')
|
||||
|
||||
module.exit_json(changed=changed, ansible_facts=ansible_facts)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
address=dict(),
|
||||
id=dict(),
|
||||
name=dict(),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[['address', 'id', 'name']],
|
||||
required_one_of=[['address', 'id', 'name']],
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
address = module.params.get('address')
|
||||
server_id = module.params.get('id')
|
||||
name = module.params.get('name')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_facts(module, address, name, server_id)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
389
plugins/modules/cloud/rackspace/rax_files.py
Normal file
389
plugins/modules/cloud/rackspace/rax_files.py
Normal file
|
@ -0,0 +1,389 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Paul Durivage <paul.durivage@rackspace.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_files
|
||||
short_description: Manipulate Rackspace Cloud Files Containers
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Files Containers
|
||||
options:
|
||||
clear_meta:
|
||||
description:
|
||||
- Optionally clear existing metadata when applying metadata to existing containers.
|
||||
Selecting this option is only appropriate when setting type=meta
|
||||
type: bool
|
||||
default: "no"
|
||||
container:
|
||||
description:
|
||||
- The container to use for container or metadata operations.
|
||||
required: true
|
||||
meta:
|
||||
description:
|
||||
- A hash of items to set as metadata values on a container
|
||||
private:
|
||||
description:
|
||||
- Used to set a container as private, removing it from the CDN. B(Warning!)
|
||||
Private containers, if previously made public, can have live objects
|
||||
available until the TTL on cached objects expires
|
||||
type: bool
|
||||
public:
|
||||
description:
|
||||
- Used to set a container as public, available via the Cloud Files CDN
|
||||
type: bool
|
||||
region:
|
||||
description:
|
||||
- Region to create an instance in
|
||||
default: DFW
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
ttl:
|
||||
description:
|
||||
- In seconds, set a container-wide TTL for all objects cached on CDN edge nodes.
|
||||
Setting a TTL is only appropriate for containers that are public
|
||||
type:
|
||||
description:
|
||||
- Type of object to do work on, i.e. metadata object or a container object
|
||||
choices:
|
||||
- file
|
||||
- meta
|
||||
default: file
|
||||
web_error:
|
||||
description:
|
||||
- Sets an object to be presented as the HTTP error page when accessed by the CDN URL
|
||||
web_index:
|
||||
description:
|
||||
- Sets an object to be presented as the HTTP index page when accessed by the CDN URL
|
||||
author: "Paul Durivage (@angstwad)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "Test Cloud Files Containers"
|
||||
hosts: local
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "List all containers"
|
||||
rax_files:
|
||||
state: list
|
||||
|
||||
- name: "Create container called 'mycontainer'"
|
||||
rax_files:
|
||||
container: mycontainer
|
||||
|
||||
- name: "Create container 'mycontainer2' with metadata"
|
||||
rax_files:
|
||||
container: mycontainer2
|
||||
meta:
|
||||
key: value
|
||||
file_for: someuser@example.com
|
||||
|
||||
- name: "Set a container's web index page"
|
||||
rax_files:
|
||||
container: mycontainer
|
||||
web_index: index.html
|
||||
|
||||
- name: "Set a container's web error page"
|
||||
rax_files:
|
||||
container: mycontainer
|
||||
web_error: error.html
|
||||
|
||||
- name: "Make container public"
|
||||
rax_files:
|
||||
container: mycontainer
|
||||
public: yes
|
||||
|
||||
- name: "Make container public with a 24 hour TTL"
|
||||
rax_files:
|
||||
container: mycontainer
|
||||
public: yes
|
||||
ttl: 86400
|
||||
|
||||
- name: "Make container private"
|
||||
rax_files:
|
||||
container: mycontainer
|
||||
private: yes
|
||||
|
||||
- name: "Test Cloud Files Containers Metadata Storage"
|
||||
hosts: local
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Get mycontainer2 metadata"
|
||||
rax_files:
|
||||
container: mycontainer2
|
||||
type: meta
|
||||
|
||||
- name: "Set mycontainer2 metadata"
|
||||
rax_files:
|
||||
container: mycontainer2
|
||||
type: meta
|
||||
meta:
|
||||
uploaded_by: someuser@example.com
|
||||
|
||||
- name: "Remove mycontainer2 metadata"
|
||||
rax_files:
|
||||
container: "mycontainer2"
|
||||
type: meta
|
||||
state: absent
|
||||
meta:
|
||||
key: ""
|
||||
file_for: ""
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError as e:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
EXIT_DICT = dict(success=True)
|
||||
META_PREFIX = 'x-container-meta-'
|
||||
|
||||
|
||||
def _get_container(module, cf, container):
|
||||
try:
|
||||
return cf.get_container(container)
|
||||
except pyrax.exc.NoSuchContainer as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
def _fetch_meta(module, container):
|
||||
EXIT_DICT['meta'] = dict()
|
||||
try:
|
||||
for k, v in container.get_metadata().items():
|
||||
split_key = k.split(META_PREFIX)[-1]
|
||||
EXIT_DICT['meta'][split_key] = v
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
def meta(cf, module, container_, state, meta_, clear_meta):
|
||||
c = _get_container(module, cf, container_)
|
||||
|
||||
if meta_ and state == 'present':
|
||||
try:
|
||||
meta_set = c.set_metadata(meta_, clear=clear_meta)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
elif meta_ and state == 'absent':
|
||||
remove_results = []
|
||||
for k, v in meta_.items():
|
||||
c.remove_metadata_key(k)
|
||||
remove_results.append(k)
|
||||
EXIT_DICT['deleted_meta_keys'] = remove_results
|
||||
elif state == 'absent':
|
||||
remove_results = []
|
||||
for k, v in c.get_metadata().items():
|
||||
c.remove_metadata_key(k)
|
||||
remove_results.append(k)
|
||||
EXIT_DICT['deleted_meta_keys'] = remove_results
|
||||
|
||||
_fetch_meta(module, c)
|
||||
_locals = locals().keys()
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
if 'meta_set' in _locals or 'remove_results' in _locals:
|
||||
EXIT_DICT['changed'] = True
|
||||
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def container(cf, module, container_, state, meta_, clear_meta, ttl, public,
|
||||
private, web_index, web_error):
|
||||
if public and private:
|
||||
module.fail_json(msg='container cannot be simultaneously '
|
||||
'set to public and private')
|
||||
|
||||
if state == 'absent' and (meta_ or clear_meta or public or private or web_index or web_error):
|
||||
module.fail_json(msg='state cannot be omitted when setting/removing '
|
||||
'attributes on a container')
|
||||
|
||||
if state == 'list':
|
||||
# We don't care if attributes are specified, let's list containers
|
||||
EXIT_DICT['containers'] = cf.list_containers()
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
try:
|
||||
c = cf.get_container(container_)
|
||||
except pyrax.exc.NoSuchContainer as e:
|
||||
# Make the container if state=present, otherwise bomb out
|
||||
if state == 'present':
|
||||
try:
|
||||
c = cf.create_container(container_)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['changed'] = True
|
||||
EXIT_DICT['created'] = True
|
||||
else:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
# Successfully grabbed a container object
|
||||
# Delete if state is absent
|
||||
if state == 'absent':
|
||||
try:
|
||||
cont_deleted = c.delete()
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['deleted'] = True
|
||||
|
||||
if meta_:
|
||||
try:
|
||||
meta_set = c.set_metadata(meta_, clear=clear_meta)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
finally:
|
||||
_fetch_meta(module, c)
|
||||
|
||||
if ttl:
|
||||
try:
|
||||
c.cdn_ttl = ttl
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['ttl'] = c.cdn_ttl
|
||||
|
||||
if public:
|
||||
try:
|
||||
cont_public = c.make_public()
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['container_urls'] = dict(url=c.cdn_uri,
|
||||
ssl_url=c.cdn_ssl_uri,
|
||||
streaming_url=c.cdn_streaming_uri,
|
||||
ios_uri=c.cdn_ios_uri)
|
||||
|
||||
if private:
|
||||
try:
|
||||
cont_private = c.make_private()
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['set_private'] = True
|
||||
|
||||
if web_index:
|
||||
try:
|
||||
cont_web_index = c.set_web_index_page(web_index)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['set_index'] = True
|
||||
finally:
|
||||
_fetch_meta(module, c)
|
||||
|
||||
if web_error:
|
||||
try:
|
||||
cont_err_index = c.set_web_error_page(web_error)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['set_error'] = True
|
||||
finally:
|
||||
_fetch_meta(module, c)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['objs_in_container'] = c.object_count
|
||||
EXIT_DICT['total_bytes'] = c.total_bytes
|
||||
|
||||
_locals = locals().keys()
|
||||
if ('cont_deleted' in _locals
|
||||
or 'meta_set' in _locals
|
||||
or 'cont_public' in _locals
|
||||
or 'cont_private' in _locals
|
||||
or 'cont_web_index' in _locals
|
||||
or 'cont_err_index' in _locals):
|
||||
EXIT_DICT['changed'] = True
|
||||
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public,
|
||||
private, web_index, web_error):
|
||||
""" Dispatch from here to work with metadata or file objects """
|
||||
cf = pyrax.cloudfiles
|
||||
|
||||
if cf is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if typ == "container":
|
||||
container(cf, module, container_, state, meta_, clear_meta, ttl,
|
||||
public, private, web_index, web_error)
|
||||
else:
|
||||
meta(cf, module, container_, state, meta_, clear_meta)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
container=dict(),
|
||||
state=dict(choices=['present', 'absent', 'list'],
|
||||
default='present'),
|
||||
meta=dict(type='dict', default=dict()),
|
||||
clear_meta=dict(default=False, type='bool'),
|
||||
type=dict(choices=['container', 'meta'], default='container'),
|
||||
ttl=dict(type='int'),
|
||||
public=dict(default=False, type='bool'),
|
||||
private=dict(default=False, type='bool'),
|
||||
web_index=dict(),
|
||||
web_error=dict()
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
container_ = module.params.get('container')
|
||||
state = module.params.get('state')
|
||||
meta_ = module.params.get('meta')
|
||||
clear_meta = module.params.get('clear_meta')
|
||||
typ = module.params.get('type')
|
||||
ttl = module.params.get('ttl')
|
||||
public = module.params.get('public')
|
||||
private = module.params.get('private')
|
||||
web_index = module.params.get('web_index')
|
||||
web_error = module.params.get('web_error')
|
||||
|
||||
if state in ['present', 'absent'] and not container_:
|
||||
module.fail_json(msg='please specify a container name')
|
||||
if clear_meta and not typ == 'meta':
|
||||
module.fail_json(msg='clear_meta can only be used when setting '
|
||||
'metadata')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public,
|
||||
private, web_index, web_error)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
611
plugins/modules/cloud/rackspace/rax_files_objects.py
Normal file
611
plugins/modules/cloud/rackspace/rax_files_objects.py
Normal file
|
@ -0,0 +1,611 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Paul Durivage <paul.durivage@rackspace.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_files_objects
|
||||
short_description: Upload, download, and delete objects in Rackspace Cloud Files
|
||||
description:
|
||||
- Upload, download, and delete objects in Rackspace Cloud Files
|
||||
options:
|
||||
clear_meta:
|
||||
description:
|
||||
- Optionally clear existing metadata when applying metadata to existing objects.
|
||||
Selecting this option is only appropriate when setting type=meta
|
||||
type: bool
|
||||
default: 'no'
|
||||
container:
|
||||
description:
|
||||
- The container to use for file object operations.
|
||||
required: true
|
||||
dest:
|
||||
description:
|
||||
- The destination of a "get" operation; i.e. a local directory, "/home/user/myfolder".
|
||||
Used to specify the destination of an operation on a remote object; i.e. a file name,
|
||||
"file1", or a comma-separated list of remote objects, "file1,file2,file17"
|
||||
expires:
|
||||
description:
|
||||
- Used to set an expiration on a file or folder uploaded to Cloud Files.
|
||||
Requires an integer, specifying expiration in seconds
|
||||
meta:
|
||||
description:
|
||||
- A hash of items to set as metadata values on an uploaded file or folder
|
||||
method:
|
||||
description:
|
||||
- The method of operation to be performed. For example, put to upload files
|
||||
to Cloud Files, get to download files from Cloud Files or delete to delete
|
||||
remote objects in Cloud Files
|
||||
choices:
|
||||
- get
|
||||
- put
|
||||
- delete
|
||||
default: get
|
||||
src:
|
||||
description:
|
||||
- Source from which to upload files. Used to specify a remote object as a source for
|
||||
an operation, i.e. a file name, "file1", or a comma-separated list of remote objects,
|
||||
"file1,file2,file17". src and dest are mutually exclusive on remote-only object operations
|
||||
structure:
|
||||
description:
|
||||
- Used to specify whether to maintain nested directory structure when downloading objects
|
||||
from Cloud Files. Setting to false downloads the contents of a container to a single,
|
||||
flat directory
|
||||
type: bool
|
||||
default: 'yes'
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type:
|
||||
description:
|
||||
- Type of object to do work on
|
||||
- Metadata object or a file object
|
||||
choices:
|
||||
- file
|
||||
- meta
|
||||
default: file
|
||||
author: "Paul Durivage (@angstwad)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "Test Cloud Files Objects"
|
||||
hosts: local
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: "Get objects from test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
dest: ~/Downloads/testcont
|
||||
|
||||
- name: "Get single object from test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
src: file1
|
||||
dest: ~/Downloads/testcont
|
||||
|
||||
- name: "Get several objects from test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
src: file1,file2,file3
|
||||
dest: ~/Downloads/testcont
|
||||
|
||||
- name: "Delete one object in test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: delete
|
||||
dest: file1
|
||||
|
||||
- name: "Delete several objects in test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: delete
|
||||
dest: file2,file3,file4
|
||||
|
||||
- name: "Delete all objects in test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: delete
|
||||
|
||||
- name: "Upload all files to test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: put
|
||||
src: ~/Downloads/onehundred
|
||||
|
||||
- name: "Upload one file to test container"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: put
|
||||
src: ~/Downloads/testcont/file1
|
||||
|
||||
- name: "Upload one file to test container with metadata"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
src: ~/Downloads/testcont/file2
|
||||
method: put
|
||||
meta:
|
||||
testkey: testdata
|
||||
who_uploaded_this: someuser@example.com
|
||||
|
||||
- name: "Upload one file to test container with TTL of 60 seconds"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: put
|
||||
src: ~/Downloads/testcont/file3
|
||||
expires: 60
|
||||
|
||||
- name: "Attempt to get remote object that does not exist"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: get
|
||||
src: FileThatDoesNotExist.jpg
|
||||
dest: ~/Downloads/testcont
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Attempt to delete remote object that does not exist"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
method: delete
|
||||
dest: FileThatDoesNotExist.jpg
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Test Cloud Files Objects Metadata"
|
||||
hosts: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: "Get metadata on one object"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
dest: file2
|
||||
|
||||
- name: "Get metadata on several objects"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
src: file2,file1
|
||||
|
||||
- name: "Set metadata on an object"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
dest: file17
|
||||
method: put
|
||||
meta:
|
||||
key1: value1
|
||||
key2: value2
|
||||
clear_meta: true
|
||||
|
||||
- name: "Verify metadata is set"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
src: file17
|
||||
|
||||
- name: "Delete metadata"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
dest: file17
|
||||
method: delete
|
||||
meta:
|
||||
key1: ''
|
||||
key2: ''
|
||||
|
||||
- name: "Get metadata on all objects"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
EXIT_DICT = dict(success=False)
|
||||
META_PREFIX = 'x-object-meta-'
|
||||
|
||||
|
||||
def _get_container(module, cf, container):
|
||||
try:
|
||||
return cf.get_container(container)
|
||||
except pyrax.exc.NoSuchContainer as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
def _upload_folder(cf, folder, container, ttl=None, headers=None):
|
||||
""" Uploads a folder to Cloud Files.
|
||||
"""
|
||||
total_bytes = 0
|
||||
for root, dirs, files in os.walk(folder):
|
||||
for fname in files:
|
||||
full_path = os.path.join(root, fname)
|
||||
obj_name = os.path.relpath(full_path, folder)
|
||||
obj_size = os.path.getsize(full_path)
|
||||
cf.upload_file(container, full_path,
|
||||
obj_name=obj_name, return_none=True, ttl=ttl, headers=headers)
|
||||
total_bytes += obj_size
|
||||
return total_bytes
|
||||
|
||||
|
||||
def upload(module, cf, container, src, dest, meta, expires):
|
||||
""" Uploads a single object or a folder to Cloud Files Optionally sets an
|
||||
metadata, TTL value (expires), or Content-Disposition and Content-Encoding
|
||||
headers.
|
||||
"""
|
||||
if not src:
|
||||
module.fail_json(msg='src must be specified when uploading')
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
src = os.path.abspath(os.path.expanduser(src))
|
||||
is_dir = os.path.isdir(src)
|
||||
|
||||
if not is_dir and not os.path.isfile(src) or not os.path.exists(src):
|
||||
module.fail_json(msg='src must be a file or a directory')
|
||||
if dest and is_dir:
|
||||
module.fail_json(msg='dest cannot be set when whole '
|
||||
'directories are uploaded')
|
||||
|
||||
cont_obj = None
|
||||
total_bytes = 0
|
||||
if dest and not is_dir:
|
||||
try:
|
||||
cont_obj = c.upload_file(src, obj_name=dest, ttl=expires, headers=meta)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
elif is_dir:
|
||||
try:
|
||||
total_bytes = _upload_folder(cf, src, c, ttl=expires, headers=meta)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
try:
|
||||
cont_obj = c.upload_file(src, ttl=expires, headers=meta)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
EXIT_DICT['success'] = True
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['msg'] = "Uploaded %s to container: %s" % (src, c.name)
|
||||
if cont_obj or total_bytes > 0:
|
||||
EXIT_DICT['changed'] = True
|
||||
if meta:
|
||||
EXIT_DICT['meta'] = dict(updated=True)
|
||||
|
||||
if cont_obj:
|
||||
EXIT_DICT['bytes'] = cont_obj.total_bytes
|
||||
EXIT_DICT['etag'] = cont_obj.etag
|
||||
else:
|
||||
EXIT_DICT['bytes'] = total_bytes
|
||||
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def download(module, cf, container, src, dest, structure):
|
||||
""" Download objects from Cloud Files to a local path specified by "dest".
|
||||
Optionally disable maintaining a directory structure by by passing a
|
||||
false value to "structure".
|
||||
"""
|
||||
# Looking for an explicit destination
|
||||
if not dest:
|
||||
module.fail_json(msg='dest is a required argument when '
|
||||
'downloading from Cloud Files')
|
||||
|
||||
# Attempt to fetch the container by name
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
# Accept a single object name or a comma-separated list of objs
|
||||
# If not specified, get the entire container
|
||||
if src:
|
||||
objs = src.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
else:
|
||||
objs = c.get_object_names()
|
||||
|
||||
dest = os.path.abspath(os.path.expanduser(dest))
|
||||
is_dir = os.path.isdir(dest)
|
||||
|
||||
if not is_dir:
|
||||
module.fail_json(msg='dest must be a directory')
|
||||
|
||||
results = []
|
||||
for obj in objs:
|
||||
try:
|
||||
c.download_object(obj, dest, structure=structure)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(obj)
|
||||
|
||||
len_results = len(results)
|
||||
len_objs = len(objs)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['requested_downloaded'] = results
|
||||
if results:
|
||||
EXIT_DICT['changed'] = True
|
||||
if len_results == len_objs:
|
||||
EXIT_DICT['success'] = True
|
||||
EXIT_DICT['msg'] = "%s objects downloaded to %s" % (len_results, dest)
|
||||
else:
|
||||
EXIT_DICT['msg'] = "Error: only %s of %s objects were " \
|
||||
"downloaded" % (len_results, len_objs)
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def delete(module, cf, container, src, dest):
|
||||
""" Delete specific objects by proving a single file name or a
|
||||
comma-separated list to src OR dest (but not both). Omitting file name(s)
|
||||
assumes the entire container is to be deleted.
|
||||
"""
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; files to be deleted "
|
||||
"have been specified on both src and dest args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
if objs:
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
else:
|
||||
objs = c.get_object_names()
|
||||
|
||||
num_objs = len(objs)
|
||||
|
||||
results = []
|
||||
for obj in objs:
|
||||
try:
|
||||
result = c.delete_object(obj)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(result)
|
||||
|
||||
num_deleted = results.count(True)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['deleted'] = num_deleted
|
||||
EXIT_DICT['requested_deleted'] = objs
|
||||
|
||||
if num_deleted:
|
||||
EXIT_DICT['changed'] = True
|
||||
|
||||
if num_objs == num_deleted:
|
||||
EXIT_DICT['success'] = True
|
||||
EXIT_DICT['msg'] = "%s objects deleted" % num_deleted
|
||||
else:
|
||||
EXIT_DICT['msg'] = ("Error: only %s of %s objects "
|
||||
"deleted" % (num_deleted, num_objs))
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def get_meta(module, cf, container, src, dest):
|
||||
""" Get metadata for a single file, comma-separated list, or entire
|
||||
container
|
||||
"""
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; files to be deleted "
|
||||
"have been specified on both src and dest args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
if objs:
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
else:
|
||||
objs = c.get_object_names()
|
||||
|
||||
results = dict()
|
||||
for obj in objs:
|
||||
try:
|
||||
meta = c.get_object(obj).get_metadata()
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results[obj] = dict()
|
||||
for k, v in meta.items():
|
||||
meta_key = k.split(META_PREFIX)[-1]
|
||||
results[obj][meta_key] = v
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
if results:
|
||||
EXIT_DICT['meta_results'] = results
|
||||
EXIT_DICT['success'] = True
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def put_meta(module, cf, container, src, dest, meta, clear_meta):
|
||||
""" Set metadata on a container, single file, or comma-separated list.
|
||||
Passing a true value to clear_meta clears the metadata stored in Cloud
|
||||
Files before setting the new metadata to the value of "meta".
|
||||
"""
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; files to set meta"
|
||||
" have been specified on both src and dest args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
results = []
|
||||
for obj in objs:
|
||||
try:
|
||||
result = c.get_object(obj).set_metadata(meta, clear=clear_meta)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(result)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['success'] = True
|
||||
if results:
|
||||
EXIT_DICT['changed'] = True
|
||||
EXIT_DICT['num_changed'] = True
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def delete_meta(module, cf, container, src, dest, meta):
|
||||
""" Removes metadata keys and values specified in meta, if any. Deletes on
|
||||
all objects specified by src or dest (but not both), if any; otherwise it
|
||||
deletes keys on all objects in the container
|
||||
"""
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; meta keys to be "
|
||||
"deleted have been specified on both src and dest"
|
||||
" args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
results = [] # Num of metadata keys removed, not objects affected
|
||||
for obj in objs:
|
||||
if meta:
|
||||
for k, v in meta.items():
|
||||
try:
|
||||
result = c.get_object(obj).remove_metadata_key(k)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(result)
|
||||
else:
|
||||
try:
|
||||
o = c.get_object(obj)
|
||||
except pyrax.exc.NoSuchObject as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
for k, v in o.get_metadata().items():
|
||||
try:
|
||||
result = o.remove_metadata_key(k)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message)
|
||||
results.append(result)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['success'] = True
|
||||
if results:
|
||||
EXIT_DICT['changed'] = True
|
||||
EXIT_DICT['num_deleted'] = len(results)
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def cloudfiles(module, container, src, dest, method, typ, meta, clear_meta,
|
||||
structure, expires):
|
||||
""" Dispatch from here to work with metadata or file objects """
|
||||
cf = pyrax.cloudfiles
|
||||
|
||||
if cf is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if typ == "file":
|
||||
if method == 'put':
|
||||
upload(module, cf, container, src, dest, meta, expires)
|
||||
|
||||
elif method == 'get':
|
||||
download(module, cf, container, src, dest, structure)
|
||||
|
||||
elif method == 'delete':
|
||||
delete(module, cf, container, src, dest)
|
||||
|
||||
else:
|
||||
if method == 'get':
|
||||
get_meta(module, cf, container, src, dest)
|
||||
|
||||
if method == 'put':
|
||||
put_meta(module, cf, container, src, dest, meta, clear_meta)
|
||||
|
||||
if method == 'delete':
|
||||
delete_meta(module, cf, container, src, dest, meta)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
container=dict(required=True),
|
||||
src=dict(),
|
||||
dest=dict(),
|
||||
method=dict(default='get', choices=['put', 'get', 'delete']),
|
||||
type=dict(default='file', choices=['file', 'meta']),
|
||||
meta=dict(type='dict', default=dict()),
|
||||
clear_meta=dict(default=False, type='bool'),
|
||||
structure=dict(default=True, type='bool'),
|
||||
expires=dict(type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
container = module.params.get('container')
|
||||
src = module.params.get('src')
|
||||
dest = module.params.get('dest')
|
||||
method = module.params.get('method')
|
||||
typ = module.params.get('type')
|
||||
meta = module.params.get('meta')
|
||||
clear_meta = module.params.get('clear_meta')
|
||||
structure = module.params.get('structure')
|
||||
expires = module.params.get('expires')
|
||||
|
||||
if clear_meta and not typ == 'meta':
|
||||
module.fail_json(msg='clear_meta can only be used when setting metadata')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
cloudfiles(module, container, src, dest, method, typ, meta, clear_meta, structure, expires)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
106
plugins/modules/cloud/rackspace/rax_identity.py
Normal file
106
plugins/modules/cloud/rackspace/rax_identity.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_identity
|
||||
short_description: Load Rackspace Cloud Identity
|
||||
description:
|
||||
- Verifies Rackspace Cloud credentials and returns identity information
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
required: false
|
||||
author:
|
||||
- "Christopher H. Laco (@claco)"
|
||||
- "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Load Rackspace Cloud Identity
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Load Identity
|
||||
local_action:
|
||||
module: rax_identity
|
||||
credentials: ~/.raxpub
|
||||
region: DFW
|
||||
register: rackspace_identity
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec, rax_required_together, rax_to_dict,
|
||||
setup_rax_module)
|
||||
|
||||
|
||||
def cloud_identity(module, state, identity):
|
||||
instance = dict(
|
||||
authenticated=identity.authenticated,
|
||||
credentials=identity._creds_file
|
||||
)
|
||||
changed = False
|
||||
|
||||
instance.update(rax_to_dict(identity))
|
||||
instance['services'] = instance.get('services', {}).keys()
|
||||
|
||||
if state == 'present':
|
||||
if not identity.authenticated:
|
||||
module.fail_json(msg='Credentials could not be verified!')
|
||||
|
||||
module.exit_json(changed=changed, identity=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present', choices=['present'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
if not pyrax.identity:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
cloud_identity(module, state, pyrax.identity)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
173
plugins/modules/cloud/rackspace/rax_keypair.py
Normal file
173
plugins/modules/cloud/rackspace/rax_keypair.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_keypair
|
||||
short_description: Create a keypair for use with Rackspace Cloud Servers
|
||||
description:
|
||||
- Create a keypair for use with Rackspace Cloud Servers
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of keypair
|
||||
required: true
|
||||
public_key:
|
||||
description:
|
||||
- Public Key string to upload. Can be a file path or string
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author: "Matt Martz (@sivel)"
|
||||
notes:
|
||||
- Keypairs cannot be manipulated, only created and deleted. To "update" a
|
||||
keypair you must first delete and then recreate.
|
||||
- The ability to specify a file path for the public key was added in 1.7
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a keypair
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: keypair request
|
||||
local_action:
|
||||
module: rax_keypair
|
||||
credentials: ~/.raxpub
|
||||
name: my_keypair
|
||||
region: DFW
|
||||
register: keypair
|
||||
- name: Create local public key
|
||||
local_action:
|
||||
module: copy
|
||||
content: "{{ keypair.keypair.public_key }}"
|
||||
dest: "{{ inventory_dir }}/{{ keypair.keypair.name }}.pub"
|
||||
- name: Create local private key
|
||||
local_action:
|
||||
module: copy
|
||||
content: "{{ keypair.keypair.private_key }}"
|
||||
dest: "{{ inventory_dir }}/{{ keypair.keypair.name }}"
|
||||
|
||||
- name: Create a keypair
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: keypair request
|
||||
local_action:
|
||||
module: rax_keypair
|
||||
credentials: ~/.raxpub
|
||||
name: my_keypair
|
||||
public_key: "{{ lookup('file', 'authorized_keys/id_rsa.pub') }}"
|
||||
region: DFW
|
||||
register: keypair
|
||||
'''
|
||||
import os
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec,
|
||||
rax_required_together,
|
||||
rax_to_dict,
|
||||
setup_rax_module,
|
||||
)
|
||||
|
||||
|
||||
def rax_keypair(module, name, public_key, state):
|
||||
changed = False
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
keypair = {}
|
||||
|
||||
if state == 'present':
|
||||
if public_key and os.path.isfile(public_key):
|
||||
try:
|
||||
f = open(public_key)
|
||||
public_key = f.read()
|
||||
f.close()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Failed to load %s' % public_key)
|
||||
|
||||
try:
|
||||
keypair = cs.keypairs.find(name=name)
|
||||
except cs.exceptions.NotFound:
|
||||
try:
|
||||
keypair = cs.keypairs.create(name, public_key)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
keypair = cs.keypairs.find(name=name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if keypair:
|
||||
try:
|
||||
keypair.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, keypair=rax_to_dict(keypair))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(required=True),
|
||||
public_key=dict(),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
name = module.params.get('name')
|
||||
public_key = module.params.get('public_key')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_keypair(module, name, public_key, state)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
174
plugins/modules/cloud/rackspace/rax_meta.py
Normal file
174
plugins/modules/cloud/rackspace/rax_meta.py
Normal file
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_meta
|
||||
short_description: Manipulate metadata for Rackspace Cloud Servers
|
||||
description:
|
||||
- Manipulate metadata for Rackspace Cloud Servers
|
||||
options:
|
||||
address:
|
||||
description:
|
||||
- Server IP address to modify metadata for, will match any IP assigned to
|
||||
the server
|
||||
id:
|
||||
description:
|
||||
- Server ID to modify metadata for
|
||||
name:
|
||||
description:
|
||||
- Server name to modify metadata for
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set metadata for a server
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Set metadata
|
||||
local_action:
|
||||
module: rax_meta
|
||||
credentials: ~/.raxpub
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
meta:
|
||||
group: primary_group
|
||||
groups:
|
||||
- group_two
|
||||
- group_three
|
||||
app: my_app
|
||||
|
||||
- name: Clear metadata
|
||||
local_action:
|
||||
module: rax_meta
|
||||
credentials: ~/.raxpub
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
'''
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
|
||||
def rax_meta(module, address, name, server_id, meta):
|
||||
changed = False
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
search_opts = {}
|
||||
if name:
|
||||
search_opts = dict(name='^%s$' % name)
|
||||
try:
|
||||
servers = cs.servers.list(search_opts=search_opts)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif address:
|
||||
servers = []
|
||||
try:
|
||||
for server in cs.servers.list():
|
||||
for addresses in server.networks.values():
|
||||
if address in addresses:
|
||||
servers.append(server)
|
||||
break
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif server_id:
|
||||
servers = []
|
||||
try:
|
||||
servers.append(cs.servers.get(server_id))
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if len(servers) > 1:
|
||||
module.fail_json(msg='Multiple servers found matching provided '
|
||||
'search parameters')
|
||||
elif not servers:
|
||||
module.fail_json(msg='Failed to find a server matching provided '
|
||||
'search parameters')
|
||||
|
||||
# Normalize and ensure all metadata values are strings
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, list):
|
||||
meta[k] = ','.join(['%s' % i for i in v])
|
||||
elif isinstance(v, dict):
|
||||
meta[k] = json.dumps(v)
|
||||
elif not isinstance(v, string_types):
|
||||
meta[k] = '%s' % v
|
||||
|
||||
server = servers[0]
|
||||
if server.metadata == meta:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
removed = set(server.metadata.keys()).difference(meta.keys())
|
||||
cs.servers.delete_meta(server, list(removed))
|
||||
cs.servers.set_meta(server, meta)
|
||||
server.get()
|
||||
|
||||
module.exit_json(changed=changed, meta=server.metadata)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
address=dict(),
|
||||
id=dict(),
|
||||
name=dict(),
|
||||
meta=dict(type='dict', default=dict()),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[['address', 'id', 'name']],
|
||||
required_one_of=[['address', 'id', 'name']],
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
address = module.params.get('address')
|
||||
server_id = module.params.get('id')
|
||||
name = module.params.get('name')
|
||||
meta = module.params.get('meta')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_meta(module, address, name, server_id, meta)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
224
plugins/modules/cloud/rackspace/rax_mon_alarm.py
Normal file
224
plugins/modules/cloud/rackspace/rax_mon_alarm.py
Normal file
|
@ -0,0 +1,224 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_mon_alarm
|
||||
short_description: Create or delete a Rackspace Cloud Monitoring alarm.
|
||||
description:
|
||||
- Create or delete a Rackspace Cloud Monitoring alarm that associates an
|
||||
existing rax_mon_entity, rax_mon_check, and rax_mon_notification_plan with
|
||||
criteria that specify what conditions will trigger which levels of
|
||||
notifications. Rackspace monitoring module flow | rax_mon_entity ->
|
||||
rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan ->
|
||||
*rax_mon_alarm*
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Ensure that the alarm with this C(label) exists or does not exist.
|
||||
choices: [ "present", "absent" ]
|
||||
required: false
|
||||
default: present
|
||||
label:
|
||||
description:
|
||||
- Friendly name for this alarm, used to achieve idempotence. Must be a String
|
||||
between 1 and 255 characters long.
|
||||
required: true
|
||||
entity_id:
|
||||
description:
|
||||
- ID of the entity this alarm is attached to. May be acquired by registering
|
||||
the value of a rax_mon_entity task.
|
||||
required: true
|
||||
check_id:
|
||||
description:
|
||||
- ID of the check that should be alerted on. May be acquired by registering
|
||||
the value of a rax_mon_check task.
|
||||
required: true
|
||||
notification_plan_id:
|
||||
description:
|
||||
- ID of the notification plan to trigger if this alarm fires. May be acquired
|
||||
by registering the value of a rax_mon_notification_plan task.
|
||||
required: true
|
||||
criteria:
|
||||
description:
|
||||
- Alarm DSL that describes alerting conditions and their output states. Must
|
||||
be between 1 and 16384 characters long. See
|
||||
http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/alerts-language.html
|
||||
for a reference on the alerting language.
|
||||
disabled:
|
||||
description:
|
||||
- If yes, create this alarm, but leave it in an inactive state. Defaults to
|
||||
no.
|
||||
type: bool
|
||||
metadata:
|
||||
description:
|
||||
- Arbitrary key/value pairs to accompany the alarm. Must be a hash of String
|
||||
keys and values between 1 and 255 characters long.
|
||||
author: Ash Wilson (@smashwilson)
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Alarm example
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Ensure that a specific alarm exists.
|
||||
rax_mon_alarm:
|
||||
credentials: ~/.rax_pub
|
||||
state: present
|
||||
label: uhoh
|
||||
entity_id: "{{ the_entity['entity']['id'] }}"
|
||||
check_id: "{{ the_check['check']['id'] }}"
|
||||
notification_plan_id: "{{ defcon1['notification_plan']['id'] }}"
|
||||
criteria: >
|
||||
if (rate(metric['average']) > 10) {
|
||||
return new AlarmStatus(WARNING);
|
||||
}
|
||||
return new AlarmStatus(OK);
|
||||
register: the_alarm
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def alarm(module, state, label, entity_id, check_id, notification_plan_id, criteria,
|
||||
disabled, metadata):
|
||||
|
||||
if len(label) < 1 or len(label) > 255:
|
||||
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||
|
||||
if criteria and len(criteria) < 1 or len(criteria) > 16384:
|
||||
module.fail_json(msg='criteria must be between 1 and 16384 characters long')
|
||||
|
||||
# Coerce attributes.
|
||||
|
||||
changed = False
|
||||
alarm = None
|
||||
|
||||
cm = pyrax.cloud_monitoring
|
||||
if not cm:
|
||||
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||
'indicates an invalid region or an incorrectly '
|
||||
'capitalized region name.')
|
||||
|
||||
existing = [a for a in cm.list_alarms(entity_id) if a.label == label]
|
||||
|
||||
if existing:
|
||||
alarm = existing[0]
|
||||
|
||||
if state == 'present':
|
||||
should_create = False
|
||||
should_update = False
|
||||
should_delete = False
|
||||
|
||||
if len(existing) > 1:
|
||||
module.fail_json(msg='%s existing alarms have the label %s.' %
|
||||
(len(existing), label))
|
||||
|
||||
if alarm:
|
||||
if check_id != alarm.check_id or notification_plan_id != alarm.notification_plan_id:
|
||||
should_delete = should_create = True
|
||||
|
||||
should_update = (disabled and disabled != alarm.disabled) or \
|
||||
(metadata and metadata != alarm.metadata) or \
|
||||
(criteria and criteria != alarm.criteria)
|
||||
|
||||
if should_update and not should_delete:
|
||||
cm.update_alarm(entity=entity_id, alarm=alarm,
|
||||
criteria=criteria, disabled=disabled,
|
||||
label=label, metadata=metadata)
|
||||
changed = True
|
||||
|
||||
if should_delete:
|
||||
alarm.delete()
|
||||
changed = True
|
||||
else:
|
||||
should_create = True
|
||||
|
||||
if should_create:
|
||||
alarm = cm.create_alarm(entity=entity_id, check=check_id,
|
||||
notification_plan=notification_plan_id,
|
||||
criteria=criteria, disabled=disabled, label=label,
|
||||
metadata=metadata)
|
||||
changed = True
|
||||
else:
|
||||
for a in existing:
|
||||
a.delete()
|
||||
changed = True
|
||||
|
||||
if alarm:
|
||||
alarm_dict = {
|
||||
"id": alarm.id,
|
||||
"label": alarm.label,
|
||||
"check_id": alarm.check_id,
|
||||
"notification_plan_id": alarm.notification_plan_id,
|
||||
"criteria": alarm.criteria,
|
||||
"disabled": alarm.disabled,
|
||||
"metadata": alarm.metadata
|
||||
}
|
||||
module.exit_json(changed=changed, alarm=alarm_dict)
|
||||
else:
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
label=dict(required=True),
|
||||
entity_id=dict(required=True),
|
||||
check_id=dict(required=True),
|
||||
notification_plan_id=dict(required=True),
|
||||
criteria=dict(),
|
||||
disabled=dict(type='bool', default=False),
|
||||
metadata=dict(type='dict')
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
label = module.params.get('label')
|
||||
entity_id = module.params.get('entity_id')
|
||||
check_id = module.params.get('check_id')
|
||||
notification_plan_id = module.params.get('notification_plan_id')
|
||||
criteria = module.params.get('criteria')
|
||||
disabled = module.boolean(module.params.get('disabled'))
|
||||
metadata = module.params.get('metadata')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
alarm(module, state, label, entity_id, check_id, notification_plan_id,
|
||||
criteria, disabled, metadata)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
310
plugins/modules/cloud/rackspace/rax_mon_check.py
Normal file
310
plugins/modules/cloud/rackspace/rax_mon_check.py
Normal file
|
@ -0,0 +1,310 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_mon_check
|
||||
short_description: Create or delete a Rackspace Cloud Monitoring check for an
|
||||
existing entity.
|
||||
description:
|
||||
- Create or delete a Rackspace Cloud Monitoring check associated with an
|
||||
existing rax_mon_entity. A check is a specific test or measurement that is
|
||||
performed, possibly from different monitoring zones, on the systems you
|
||||
monitor. Rackspace monitoring module flow | rax_mon_entity ->
|
||||
*rax_mon_check* -> rax_mon_notification -> rax_mon_notification_plan ->
|
||||
rax_mon_alarm
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Ensure that a check with this C(label) exists or does not exist.
|
||||
choices: ["present", "absent"]
|
||||
entity_id:
|
||||
description:
|
||||
- ID of the rax_mon_entity to target with this check.
|
||||
required: true
|
||||
label:
|
||||
description:
|
||||
- Defines a label for this check, between 1 and 64 characters long.
|
||||
required: true
|
||||
check_type:
|
||||
description:
|
||||
- The type of check to create. C(remote.) checks may be created on any
|
||||
rax_mon_entity. C(agent.) checks may only be created on rax_mon_entities
|
||||
that have a non-null C(agent_id).
|
||||
choices:
|
||||
- remote.dns
|
||||
- remote.ftp-banner
|
||||
- remote.http
|
||||
- remote.imap-banner
|
||||
- remote.mssql-banner
|
||||
- remote.mysql-banner
|
||||
- remote.ping
|
||||
- remote.pop3-banner
|
||||
- remote.postgresql-banner
|
||||
- remote.smtp-banner
|
||||
- remote.smtp
|
||||
- remote.ssh
|
||||
- remote.tcp
|
||||
- remote.telnet-banner
|
||||
- agent.filesystem
|
||||
- agent.memory
|
||||
- agent.load_average
|
||||
- agent.cpu
|
||||
- agent.disk
|
||||
- agent.network
|
||||
- agent.plugin
|
||||
required: true
|
||||
monitoring_zones_poll:
|
||||
description:
|
||||
- Comma-separated list of the names of the monitoring zones the check should
|
||||
run from. Available monitoring zones include mzdfw, mzhkg, mziad, mzlon,
|
||||
mzord and mzsyd. Required for remote.* checks; prohibited for agent.* checks.
|
||||
target_hostname:
|
||||
description:
|
||||
- One of `target_hostname` and `target_alias` is required for remote.* checks,
|
||||
but prohibited for agent.* checks. The hostname this check should target.
|
||||
Must be a valid IPv4, IPv6, or FQDN.
|
||||
target_alias:
|
||||
description:
|
||||
- One of `target_alias` and `target_hostname` is required for remote.* checks,
|
||||
but prohibited for agent.* checks. Use the corresponding key in the entity's
|
||||
`ip_addresses` hash to resolve an IP address to target.
|
||||
details:
|
||||
description:
|
||||
- Additional details specific to the check type. Must be a hash of strings
|
||||
between 1 and 255 characters long, or an array or object containing 0 to
|
||||
256 items.
|
||||
disabled:
|
||||
description:
|
||||
- If "yes", ensure the check is created, but don't actually use it yet.
|
||||
type: bool
|
||||
metadata:
|
||||
description:
|
||||
- Hash of arbitrary key-value pairs to accompany this check if it fires.
|
||||
Keys and values must be strings between 1 and 255 characters long.
|
||||
period:
|
||||
description:
|
||||
- The number of seconds between each time the check is performed. Must be
|
||||
greater than the minimum period set on your account.
|
||||
timeout:
|
||||
description:
|
||||
- The number of seconds this check will wait when attempting to collect
|
||||
results. Must be less than the period.
|
||||
author: Ash Wilson (@smashwilson)
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a monitoring check
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Associate a check with an existing entity.
|
||||
rax_mon_check:
|
||||
credentials: ~/.rax_pub
|
||||
state: present
|
||||
entity_id: "{{ the_entity['entity']['id'] }}"
|
||||
label: the_check
|
||||
check_type: remote.ping
|
||||
monitoring_zones_poll: mziad,mzord,mzdfw
|
||||
details:
|
||||
count: 10
|
||||
meta:
|
||||
hurf: durf
|
||||
register: the_check
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def cloud_check(module, state, entity_id, label, check_type,
|
||||
monitoring_zones_poll, target_hostname, target_alias, details,
|
||||
disabled, metadata, period, timeout):
|
||||
|
||||
# Coerce attributes.
|
||||
|
||||
if monitoring_zones_poll and not isinstance(monitoring_zones_poll, list):
|
||||
monitoring_zones_poll = [monitoring_zones_poll]
|
||||
|
||||
if period:
|
||||
period = int(period)
|
||||
|
||||
if timeout:
|
||||
timeout = int(timeout)
|
||||
|
||||
changed = False
|
||||
check = None
|
||||
|
||||
cm = pyrax.cloud_monitoring
|
||||
if not cm:
|
||||
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||
'indicates an invalid region or an incorrectly '
|
||||
'capitalized region name.')
|
||||
|
||||
entity = cm.get_entity(entity_id)
|
||||
if not entity:
|
||||
module.fail_json(msg='Failed to instantiate entity. "%s" may not be'
|
||||
' a valid entity id.' % entity_id)
|
||||
|
||||
existing = [e for e in entity.list_checks() if e.label == label]
|
||||
|
||||
if existing:
|
||||
check = existing[0]
|
||||
|
||||
if state == 'present':
|
||||
if len(existing) > 1:
|
||||
module.fail_json(msg='%s existing checks have a label of %s.' %
|
||||
(len(existing), label))
|
||||
|
||||
should_delete = False
|
||||
should_create = False
|
||||
should_update = False
|
||||
|
||||
if check:
|
||||
# Details may include keys set to default values that are not
|
||||
# included in the initial creation.
|
||||
#
|
||||
# Only force a recreation of the check if one of the *specified*
|
||||
# keys is missing or has a different value.
|
||||
if details:
|
||||
for (key, value) in details.items():
|
||||
if key not in check.details:
|
||||
should_delete = should_create = True
|
||||
elif value != check.details[key]:
|
||||
should_delete = should_create = True
|
||||
|
||||
should_update = label != check.label or \
|
||||
(target_hostname and target_hostname != check.target_hostname) or \
|
||||
(target_alias and target_alias != check.target_alias) or \
|
||||
(disabled != check.disabled) or \
|
||||
(metadata and metadata != check.metadata) or \
|
||||
(period and period != check.period) or \
|
||||
(timeout and timeout != check.timeout) or \
|
||||
(monitoring_zones_poll and monitoring_zones_poll != check.monitoring_zones_poll)
|
||||
|
||||
if should_update and not should_delete:
|
||||
check.update(label=label,
|
||||
disabled=disabled,
|
||||
metadata=metadata,
|
||||
monitoring_zones_poll=monitoring_zones_poll,
|
||||
timeout=timeout,
|
||||
period=period,
|
||||
target_alias=target_alias,
|
||||
target_hostname=target_hostname)
|
||||
changed = True
|
||||
else:
|
||||
# The check doesn't exist yet.
|
||||
should_create = True
|
||||
|
||||
if should_delete:
|
||||
check.delete()
|
||||
|
||||
if should_create:
|
||||
check = cm.create_check(entity,
|
||||
label=label,
|
||||
check_type=check_type,
|
||||
target_hostname=target_hostname,
|
||||
target_alias=target_alias,
|
||||
monitoring_zones_poll=monitoring_zones_poll,
|
||||
details=details,
|
||||
disabled=disabled,
|
||||
metadata=metadata,
|
||||
period=period,
|
||||
timeout=timeout)
|
||||
changed = True
|
||||
elif state == 'absent':
|
||||
if check:
|
||||
check.delete()
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg='state must be either present or absent.')
|
||||
|
||||
if check:
|
||||
check_dict = {
|
||||
"id": check.id,
|
||||
"label": check.label,
|
||||
"type": check.type,
|
||||
"target_hostname": check.target_hostname,
|
||||
"target_alias": check.target_alias,
|
||||
"monitoring_zones_poll": check.monitoring_zones_poll,
|
||||
"details": check.details,
|
||||
"disabled": check.disabled,
|
||||
"metadata": check.metadata,
|
||||
"period": check.period,
|
||||
"timeout": check.timeout
|
||||
}
|
||||
module.exit_json(changed=changed, check=check_dict)
|
||||
else:
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
entity_id=dict(required=True),
|
||||
label=dict(required=True),
|
||||
check_type=dict(required=True),
|
||||
monitoring_zones_poll=dict(),
|
||||
target_hostname=dict(),
|
||||
target_alias=dict(),
|
||||
details=dict(type='dict', default={}),
|
||||
disabled=dict(type='bool', default=False),
|
||||
metadata=dict(type='dict', default={}),
|
||||
period=dict(type='int'),
|
||||
timeout=dict(type='int'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
entity_id = module.params.get('entity_id')
|
||||
label = module.params.get('label')
|
||||
check_type = module.params.get('check_type')
|
||||
monitoring_zones_poll = module.params.get('monitoring_zones_poll')
|
||||
target_hostname = module.params.get('target_hostname')
|
||||
target_alias = module.params.get('target_alias')
|
||||
details = module.params.get('details')
|
||||
disabled = module.boolean(module.params.get('disabled'))
|
||||
metadata = module.params.get('metadata')
|
||||
period = module.params.get('period')
|
||||
timeout = module.params.get('timeout')
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_check(module, state, entity_id, label, check_type,
|
||||
monitoring_zones_poll, target_hostname, target_alias, details,
|
||||
disabled, metadata, period, timeout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
190
plugins/modules/cloud/rackspace/rax_mon_entity.py
Normal file
190
plugins/modules/cloud/rackspace/rax_mon_entity.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_mon_entity
|
||||
short_description: Create or delete a Rackspace Cloud Monitoring entity
|
||||
description:
|
||||
- Create or delete a Rackspace Cloud Monitoring entity, which represents a device
|
||||
to monitor. Entities associate checks and alarms with a target system and
|
||||
provide a convenient, centralized place to store IP addresses. Rackspace
|
||||
monitoring module flow | *rax_mon_entity* -> rax_mon_check ->
|
||||
rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm
|
||||
options:
|
||||
label:
|
||||
description:
|
||||
- Defines a name for this entity. Must be a non-empty string between 1 and
|
||||
255 characters long.
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Ensure that an entity with this C(name) exists or does not exist.
|
||||
choices: ["present", "absent"]
|
||||
agent_id:
|
||||
description:
|
||||
- Rackspace monitoring agent on the target device to which this entity is
|
||||
bound. Necessary to collect C(agent.) rax_mon_checks against this entity.
|
||||
named_ip_addresses:
|
||||
description:
|
||||
- Hash of IP addresses that may be referenced by name by rax_mon_checks
|
||||
added to this entity. Must be a dictionary of with keys that are names
|
||||
between 1 and 64 characters long, and values that are valid IPv4 or IPv6
|
||||
addresses.
|
||||
metadata:
|
||||
description:
|
||||
- Hash of arbitrary C(name), C(value) pairs that are passed to associated
|
||||
rax_mon_alarms. Names and values must all be between 1 and 255 characters
|
||||
long.
|
||||
author: Ash Wilson (@smashwilson)
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Entity example
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Ensure an entity exists
|
||||
rax_mon_entity:
|
||||
credentials: ~/.rax_pub
|
||||
state: present
|
||||
label: my_entity
|
||||
named_ip_addresses:
|
||||
web_box: 192.0.2.4
|
||||
db_box: 192.0.2.5
|
||||
meta:
|
||||
hurf: durf
|
||||
register: the_entity
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def cloud_monitoring(module, state, label, agent_id, named_ip_addresses,
|
||||
metadata):
|
||||
|
||||
if len(label) < 1 or len(label) > 255:
|
||||
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||
|
||||
changed = False
|
||||
|
||||
cm = pyrax.cloud_monitoring
|
||||
if not cm:
|
||||
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||
'indicates an invalid region or an incorrectly '
|
||||
'capitalized region name.')
|
||||
|
||||
existing = []
|
||||
for entity in cm.list_entities():
|
||||
if label == entity.label:
|
||||
existing.append(entity)
|
||||
|
||||
entity = None
|
||||
|
||||
if existing:
|
||||
entity = existing[0]
|
||||
|
||||
if state == 'present':
|
||||
should_update = False
|
||||
should_delete = False
|
||||
should_create = False
|
||||
|
||||
if len(existing) > 1:
|
||||
module.fail_json(msg='%s existing entities have the label %s.' %
|
||||
(len(existing), label))
|
||||
|
||||
if entity:
|
||||
if named_ip_addresses and named_ip_addresses != entity.ip_addresses:
|
||||
should_delete = should_create = True
|
||||
|
||||
# Change an existing Entity, unless there's nothing to do.
|
||||
should_update = agent_id and agent_id != entity.agent_id or \
|
||||
(metadata and metadata != entity.metadata)
|
||||
|
||||
if should_update and not should_delete:
|
||||
entity.update(agent_id, metadata)
|
||||
changed = True
|
||||
|
||||
if should_delete:
|
||||
entity.delete()
|
||||
else:
|
||||
should_create = True
|
||||
|
||||
if should_create:
|
||||
# Create a new Entity.
|
||||
entity = cm.create_entity(label=label, agent=agent_id,
|
||||
ip_addresses=named_ip_addresses,
|
||||
metadata=metadata)
|
||||
changed = True
|
||||
else:
|
||||
# Delete the existing Entities.
|
||||
for e in existing:
|
||||
e.delete()
|
||||
changed = True
|
||||
|
||||
if entity:
|
||||
entity_dict = {
|
||||
"id": entity.id,
|
||||
"name": entity.name,
|
||||
"agent_id": entity.agent_id,
|
||||
}
|
||||
module.exit_json(changed=changed, entity=entity_dict)
|
||||
else:
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
label=dict(required=True),
|
||||
agent_id=dict(),
|
||||
named_ip_addresses=dict(type='dict', default={}),
|
||||
metadata=dict(type='dict', default={})
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
label = module.params.get('label')
|
||||
agent_id = module.params.get('agent_id')
|
||||
named_ip_addresses = module.params.get('named_ip_addresses')
|
||||
metadata = module.params.get('metadata')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_monitoring(module, state, label, agent_id, named_ip_addresses, metadata)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
174
plugins/modules/cloud/rackspace/rax_mon_notification.py
Normal file
174
plugins/modules/cloud/rackspace/rax_mon_notification.py
Normal file
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_mon_notification
|
||||
short_description: Create or delete a Rackspace Cloud Monitoring notification.
|
||||
description:
|
||||
- Create or delete a Rackspace Cloud Monitoring notification that specifies a
|
||||
channel that can be used to communicate alarms, such as email, webhooks, or
|
||||
PagerDuty. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check ->
|
||||
*rax_mon_notification* -> rax_mon_notification_plan -> rax_mon_alarm
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Ensure that the notification with this C(label) exists or does not exist.
|
||||
choices: ['present', 'absent']
|
||||
label:
|
||||
description:
|
||||
- Defines a friendly name for this notification. String between 1 and 255
|
||||
characters long.
|
||||
required: true
|
||||
notification_type:
|
||||
description:
|
||||
- A supported notification type.
|
||||
choices: ["webhook", "email", "pagerduty"]
|
||||
required: true
|
||||
details:
|
||||
description:
|
||||
- Dictionary of key-value pairs used to initialize the notification.
|
||||
Required keys and meanings vary with notification type. See
|
||||
http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/
|
||||
service-notification-types-crud.html for details.
|
||||
required: true
|
||||
author: Ash Wilson (@smashwilson)
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Monitoring notification example
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Email me when something goes wrong.
|
||||
rax_mon_entity:
|
||||
credentials: ~/.rax_pub
|
||||
label: omg
|
||||
type: email
|
||||
details:
|
||||
address: me@mailhost.com
|
||||
register: the_notification
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def notification(module, state, label, notification_type, details):
|
||||
|
||||
if len(label) < 1 or len(label) > 255:
|
||||
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||
|
||||
changed = False
|
||||
notification = None
|
||||
|
||||
cm = pyrax.cloud_monitoring
|
||||
if not cm:
|
||||
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||
'indicates an invalid region or an incorrectly '
|
||||
'capitalized region name.')
|
||||
|
||||
existing = []
|
||||
for n in cm.list_notifications():
|
||||
if n.label == label:
|
||||
existing.append(n)
|
||||
|
||||
if existing:
|
||||
notification = existing[0]
|
||||
|
||||
if state == 'present':
|
||||
should_update = False
|
||||
should_delete = False
|
||||
should_create = False
|
||||
|
||||
if len(existing) > 1:
|
||||
module.fail_json(msg='%s existing notifications are labelled %s.' %
|
||||
(len(existing), label))
|
||||
|
||||
if notification:
|
||||
should_delete = (notification_type != notification.type)
|
||||
|
||||
should_update = (details != notification.details)
|
||||
|
||||
if should_update and not should_delete:
|
||||
notification.update(details=notification.details)
|
||||
changed = True
|
||||
|
||||
if should_delete:
|
||||
notification.delete()
|
||||
else:
|
||||
should_create = True
|
||||
|
||||
if should_create:
|
||||
notification = cm.create_notification(notification_type,
|
||||
label=label, details=details)
|
||||
changed = True
|
||||
else:
|
||||
for n in existing:
|
||||
n.delete()
|
||||
changed = True
|
||||
|
||||
if notification:
|
||||
notification_dict = {
|
||||
"id": notification.id,
|
||||
"type": notification.type,
|
||||
"label": notification.label,
|
||||
"details": notification.details
|
||||
}
|
||||
module.exit_json(changed=changed, notification=notification_dict)
|
||||
else:
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
label=dict(required=True),
|
||||
notification_type=dict(required=True, choices=['webhook', 'email', 'pagerduty']),
|
||||
details=dict(required=True, type='dict')
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
label = module.params.get('label')
|
||||
notification_type = module.params.get('notification_type')
|
||||
details = module.params.get('details')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
notification(module, state, label, notification_type, details)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
179
plugins/modules/cloud/rackspace/rax_mon_notification_plan.py
Normal file
179
plugins/modules/cloud/rackspace/rax_mon_notification_plan.py
Normal file
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_mon_notification_plan
|
||||
short_description: Create or delete a Rackspace Cloud Monitoring notification
|
||||
plan.
|
||||
description:
|
||||
- Create or delete a Rackspace Cloud Monitoring notification plan by
|
||||
associating existing rax_mon_notifications with severity levels. Rackspace
|
||||
monitoring module flow | rax_mon_entity -> rax_mon_check ->
|
||||
rax_mon_notification -> *rax_mon_notification_plan* -> rax_mon_alarm
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Ensure that the notification plan with this C(label) exists or does not
|
||||
exist.
|
||||
choices: ['present', 'absent']
|
||||
label:
|
||||
description:
|
||||
- Defines a friendly name for this notification plan. String between 1 and
|
||||
255 characters long.
|
||||
required: true
|
||||
critical_state:
|
||||
description:
|
||||
- Notification list to use when the alarm state is CRITICAL. Must be an
|
||||
array of valid rax_mon_notification ids.
|
||||
warning_state:
|
||||
description:
|
||||
- Notification list to use when the alarm state is WARNING. Must be an array
|
||||
of valid rax_mon_notification ids.
|
||||
ok_state:
|
||||
description:
|
||||
- Notification list to use when the alarm state is OK. Must be an array of
|
||||
valid rax_mon_notification ids.
|
||||
author: Ash Wilson (@smashwilson)
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Example notification plan
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Establish who gets called when.
|
||||
rax_mon_notification_plan:
|
||||
credentials: ~/.rax_pub
|
||||
state: present
|
||||
label: defcon1
|
||||
critical_state:
|
||||
- "{{ everyone['notification']['id'] }}"
|
||||
warning_state:
|
||||
- "{{ opsfloor['notification']['id'] }}"
|
||||
register: defcon1
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def notification_plan(module, state, label, critical_state, warning_state, ok_state):
|
||||
|
||||
if len(label) < 1 or len(label) > 255:
|
||||
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||
|
||||
changed = False
|
||||
notification_plan = None
|
||||
|
||||
cm = pyrax.cloud_monitoring
|
||||
if not cm:
|
||||
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||
'indicates an invalid region or an incorrectly '
|
||||
'capitalized region name.')
|
||||
|
||||
existing = []
|
||||
for n in cm.list_notification_plans():
|
||||
if n.label == label:
|
||||
existing.append(n)
|
||||
|
||||
if existing:
|
||||
notification_plan = existing[0]
|
||||
|
||||
if state == 'present':
|
||||
should_create = False
|
||||
should_delete = False
|
||||
|
||||
if len(existing) > 1:
|
||||
module.fail_json(msg='%s notification plans are labelled %s.' %
|
||||
(len(existing), label))
|
||||
|
||||
if notification_plan:
|
||||
should_delete = (critical_state and critical_state != notification_plan.critical_state) or \
|
||||
(warning_state and warning_state != notification_plan.warning_state) or \
|
||||
(ok_state and ok_state != notification_plan.ok_state)
|
||||
|
||||
if should_delete:
|
||||
notification_plan.delete()
|
||||
should_create = True
|
||||
else:
|
||||
should_create = True
|
||||
|
||||
if should_create:
|
||||
notification_plan = cm.create_notification_plan(label=label,
|
||||
critical_state=critical_state,
|
||||
warning_state=warning_state,
|
||||
ok_state=ok_state)
|
||||
changed = True
|
||||
else:
|
||||
for np in existing:
|
||||
np.delete()
|
||||
changed = True
|
||||
|
||||
if notification_plan:
|
||||
notification_plan_dict = {
|
||||
"id": notification_plan.id,
|
||||
"critical_state": notification_plan.critical_state,
|
||||
"warning_state": notification_plan.warning_state,
|
||||
"ok_state": notification_plan.ok_state,
|
||||
"metadata": notification_plan.metadata
|
||||
}
|
||||
module.exit_json(changed=changed, notification_plan=notification_plan_dict)
|
||||
else:
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
label=dict(required=True),
|
||||
critical_state=dict(type='list'),
|
||||
warning_state=dict(type='list'),
|
||||
ok_state=dict(type='list')
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
label = module.params.get('label')
|
||||
critical_state = module.params.get('critical_state')
|
||||
warning_state = module.params.get('warning_state')
|
||||
ok_state = module.params.get('ok_state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
notification_plan(module, state, label, critical_state, warning_state, ok_state)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
139
plugins/modules/cloud/rackspace/rax_network.py
Normal file
139
plugins/modules/cloud/rackspace/rax_network.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_network
|
||||
short_description: create / delete an isolated network in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud isolated network.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
label:
|
||||
description:
|
||||
- Label (name) to give the network
|
||||
cidr:
|
||||
description:
|
||||
- cidr of the network being created
|
||||
author:
|
||||
- "Christopher H. Laco (@claco)"
|
||||
- "Jesse Keating (@omgjlk)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build an Isolated Network
|
||||
gather_facts: False
|
||||
|
||||
tasks:
|
||||
- name: Network create request
|
||||
local_action:
|
||||
module: rax_network
|
||||
credentials: ~/.raxpub
|
||||
label: my-net
|
||||
cidr: 192.168.3.0/24
|
||||
state: present
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def cloud_network(module, state, label, cidr):
|
||||
changed = False
|
||||
network = None
|
||||
networks = []
|
||||
|
||||
if not pyrax.cloud_networks:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
if not cidr:
|
||||
module.fail_json(msg='missing required arguments: cidr')
|
||||
|
||||
try:
|
||||
network = pyrax.cloud_networks.find_network_by_label(label)
|
||||
except pyrax.exceptions.NetworkNotFound:
|
||||
try:
|
||||
network = pyrax.cloud_networks.create(label, cidr=cidr)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
network = pyrax.cloud_networks.find_network_by_label(label)
|
||||
network.delete()
|
||||
changed = True
|
||||
except pyrax.exceptions.NetworkNotFound:
|
||||
pass
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if network:
|
||||
instance = dict(id=network.id,
|
||||
label=network.label,
|
||||
cidr=network.cidr)
|
||||
networks.append(instance)
|
||||
|
||||
module.exit_json(changed=changed, networks=networks)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent']),
|
||||
label=dict(required=True),
|
||||
cidr=dict()
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
label = module.params.get('label')
|
||||
cidr = module.params.get('cidr')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_network(module, state, label, cidr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
142
plugins/modules/cloud/rackspace/rax_queue.py
Normal file
142
plugins/modules/cloud/rackspace/rax_queue.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_queue
|
||||
short_description: create / delete a queue in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud queue.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name to give the queue
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author:
|
||||
- "Christopher H. Laco (@claco)"
|
||||
- "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Queue
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Queue create request
|
||||
local_action:
|
||||
module: rax_queue
|
||||
credentials: ~/.raxpub
|
||||
name: my-queue
|
||||
region: DFW
|
||||
state: present
|
||||
register: my_queue
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
|
||||
|
||||
|
||||
def cloud_queue(module, state, name):
|
||||
for arg in (state, name):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for rax_queue' % arg)
|
||||
|
||||
changed = False
|
||||
queues = []
|
||||
instance = {}
|
||||
|
||||
cq = pyrax.queues
|
||||
if not cq:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
for queue in cq.list():
|
||||
if name != queue.name:
|
||||
continue
|
||||
|
||||
queues.append(queue)
|
||||
|
||||
if len(queues) > 1:
|
||||
module.fail_json(msg='Multiple Queues were matched by name')
|
||||
|
||||
if state == 'present':
|
||||
if not queues:
|
||||
try:
|
||||
queue = cq.create(name)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
queue = queues[0]
|
||||
|
||||
instance = dict(name=queue.name)
|
||||
result = dict(changed=changed, queue=instance)
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
if queues:
|
||||
queue = queues[0]
|
||||
try:
|
||||
queue.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, queue=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
name = module.params.get('name')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_queue(module, state, name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
426
plugins/modules/cloud/rackspace/rax_scaling_group.py
Normal file
426
plugins/modules/cloud/rackspace/rax_scaling_group.py
Normal file
|
@ -0,0 +1,426 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_scaling_group
|
||||
short_description: Manipulate Rackspace Cloud Autoscale Groups
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Autoscale Groups
|
||||
options:
|
||||
config_drive:
|
||||
description:
|
||||
- Attach read-only configuration drive to server as label config-2
|
||||
type: bool
|
||||
default: 'no'
|
||||
cooldown:
|
||||
description:
|
||||
- The period of time, in seconds, that must pass before any scaling can
|
||||
occur after the previous scaling. Must be an integer between 0 and
|
||||
86400 (24 hrs).
|
||||
disk_config:
|
||||
description:
|
||||
- Disk partitioning strategy
|
||||
choices:
|
||||
- auto
|
||||
- manual
|
||||
default: auto
|
||||
files:
|
||||
description:
|
||||
- 'Files to insert into the instance. Hash of C(remotepath: localpath)'
|
||||
flavor:
|
||||
description:
|
||||
- flavor to use for the instance
|
||||
required: true
|
||||
image:
|
||||
description:
|
||||
- image to use for the instance. Can be an C(id), C(human_id) or C(name)
|
||||
required: true
|
||||
key_name:
|
||||
description:
|
||||
- key pair to use on the instance
|
||||
loadbalancers:
|
||||
description:
|
||||
- List of load balancer C(id) and C(port) hashes
|
||||
max_entities:
|
||||
description:
|
||||
- The maximum number of entities that are allowed in the scaling group.
|
||||
Must be an integer between 0 and 1000.
|
||||
required: true
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
min_entities:
|
||||
description:
|
||||
- The minimum number of entities that are allowed in the scaling group.
|
||||
Must be an integer between 0 and 1000.
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- Name to give the scaling group
|
||||
required: true
|
||||
networks:
|
||||
description:
|
||||
- The network to attach to the instances. If specified, you must include
|
||||
ALL networks including the public and private interfaces. Can be C(id)
|
||||
or C(label).
|
||||
default:
|
||||
- public
|
||||
- private
|
||||
server_name:
|
||||
description:
|
||||
- The base name for servers created by Autoscale
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
user_data:
|
||||
description:
|
||||
- Data to be uploaded to the servers config drive. This option implies
|
||||
I(config_drive). Can be a file path or a string
|
||||
wait:
|
||||
description:
|
||||
- wait for the scaling group to finish provisioning the minimum amount of
|
||||
servers
|
||||
type: bool
|
||||
default: 'no'
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
---
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tasks:
|
||||
- rax_scaling_group:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
cooldown: 300
|
||||
flavor: performance1-1
|
||||
image: bb02b1a3-bc77-4d17-ab5b-421d89850fca
|
||||
min_entities: 5
|
||||
max_entities: 10
|
||||
name: ASG Test
|
||||
server_name: asgtest
|
||||
loadbalancers:
|
||||
- id: 228385
|
||||
port: 80
|
||||
register: asg
|
||||
'''
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec, rax_find_image, rax_find_network,
|
||||
rax_required_together, rax_to_dict, setup_rax_module)
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
|
||||
def rax_asg(module, cooldown=300, disk_config=None, files=None, flavor=None,
|
||||
image=None, key_name=None, loadbalancers=None, meta=None,
|
||||
min_entities=0, max_entities=0, name=None, networks=None,
|
||||
server_name=None, state='present', user_data=None,
|
||||
config_drive=False, wait=True, wait_timeout=300):
|
||||
files = {} if files is None else files
|
||||
loadbalancers = [] if loadbalancers is None else loadbalancers
|
||||
meta = {} if meta is None else meta
|
||||
networks = [] if networks is None else networks
|
||||
|
||||
changed = False
|
||||
|
||||
au = pyrax.autoscale
|
||||
if not au:
|
||||
module.fail_json(msg='Failed to instantiate clients. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if user_data:
|
||||
config_drive = True
|
||||
|
||||
if user_data and os.path.isfile(user_data):
|
||||
try:
|
||||
f = open(user_data)
|
||||
user_data = f.read()
|
||||
f.close()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Failed to load %s' % user_data)
|
||||
|
||||
if state == 'present':
|
||||
# Normalize and ensure all metadata values are strings
|
||||
if meta:
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, list):
|
||||
meta[k] = ','.join(['%s' % i for i in v])
|
||||
elif isinstance(v, dict):
|
||||
meta[k] = json.dumps(v)
|
||||
elif not isinstance(v, string_types):
|
||||
meta[k] = '%s' % v
|
||||
|
||||
if image:
|
||||
image = rax_find_image(module, pyrax, image)
|
||||
|
||||
nics = []
|
||||
if networks:
|
||||
for network in networks:
|
||||
nics.extend(rax_find_network(module, pyrax, network))
|
||||
|
||||
for nic in nics:
|
||||
# pyrax is currently returning net-id, but we need uuid
|
||||
# this check makes this forward compatible for a time when
|
||||
# pyrax uses uuid instead
|
||||
if nic.get('net-id'):
|
||||
nic.update(uuid=nic['net-id'])
|
||||
del nic['net-id']
|
||||
|
||||
# Handle the file contents
|
||||
personality = []
|
||||
if files:
|
||||
for rpath in files.keys():
|
||||
lpath = os.path.expanduser(files[rpath])
|
||||
try:
|
||||
f = open(lpath, 'r')
|
||||
personality.append({
|
||||
'path': rpath,
|
||||
'contents': f.read()
|
||||
})
|
||||
f.close()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Failed to load %s' % lpath)
|
||||
|
||||
lbs = []
|
||||
if loadbalancers:
|
||||
for lb in loadbalancers:
|
||||
try:
|
||||
lb_id = int(lb.get('id'))
|
||||
except (ValueError, TypeError):
|
||||
module.fail_json(msg='Load balancer ID is not an integer: '
|
||||
'%s' % lb.get('id'))
|
||||
try:
|
||||
port = int(lb.get('port'))
|
||||
except (ValueError, TypeError):
|
||||
module.fail_json(msg='Load balancer port is not an '
|
||||
'integer: %s' % lb.get('port'))
|
||||
if not lb_id or not port:
|
||||
continue
|
||||
lbs.append((lb_id, port))
|
||||
|
||||
try:
|
||||
sg = au.find(name=name)
|
||||
except pyrax.exceptions.NoUniqueMatch as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except pyrax.exceptions.NotFound:
|
||||
try:
|
||||
sg = au.create(name, cooldown=cooldown,
|
||||
min_entities=min_entities,
|
||||
max_entities=max_entities,
|
||||
launch_config_type='launch_server',
|
||||
server_name=server_name, image=image,
|
||||
flavor=flavor, disk_config=disk_config,
|
||||
metadata=meta, personality=personality,
|
||||
networks=nics, load_balancers=lbs,
|
||||
key_name=key_name, config_drive=config_drive,
|
||||
user_data=user_data)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if not changed:
|
||||
# Scaling Group Updates
|
||||
group_args = {}
|
||||
if cooldown != sg.cooldown:
|
||||
group_args['cooldown'] = cooldown
|
||||
|
||||
if min_entities != sg.min_entities:
|
||||
group_args['min_entities'] = min_entities
|
||||
|
||||
if max_entities != sg.max_entities:
|
||||
group_args['max_entities'] = max_entities
|
||||
|
||||
if group_args:
|
||||
changed = True
|
||||
sg.update(**group_args)
|
||||
|
||||
# Launch Configuration Updates
|
||||
lc = sg.get_launch_config()
|
||||
lc_args = {}
|
||||
if server_name != lc.get('name'):
|
||||
lc_args['server_name'] = server_name
|
||||
|
||||
if image != lc.get('image'):
|
||||
lc_args['image'] = image
|
||||
|
||||
if flavor != lc.get('flavor'):
|
||||
lc_args['flavor'] = flavor
|
||||
|
||||
disk_config = disk_config or 'AUTO'
|
||||
if ((disk_config or lc.get('disk_config')) and
|
||||
disk_config != lc.get('disk_config', 'AUTO')):
|
||||
lc_args['disk_config'] = disk_config
|
||||
|
||||
if (meta or lc.get('meta')) and meta != lc.get('metadata'):
|
||||
lc_args['metadata'] = meta
|
||||
|
||||
test_personality = []
|
||||
for p in personality:
|
||||
test_personality.append({
|
||||
'path': p['path'],
|
||||
'contents': base64.b64encode(p['contents'])
|
||||
})
|
||||
if ((test_personality or lc.get('personality')) and
|
||||
test_personality != lc.get('personality')):
|
||||
lc_args['personality'] = personality
|
||||
|
||||
if nics != lc.get('networks'):
|
||||
lc_args['networks'] = nics
|
||||
|
||||
if lbs != lc.get('load_balancers'):
|
||||
# Work around for https://github.com/rackspace/pyrax/pull/393
|
||||
lc_args['load_balancers'] = sg.manager._resolve_lbs(lbs)
|
||||
|
||||
if key_name != lc.get('key_name'):
|
||||
lc_args['key_name'] = key_name
|
||||
|
||||
if config_drive != lc.get('config_drive', False):
|
||||
lc_args['config_drive'] = config_drive
|
||||
|
||||
if (user_data and
|
||||
base64.b64encode(user_data) != lc.get('user_data')):
|
||||
lc_args['user_data'] = user_data
|
||||
|
||||
if lc_args:
|
||||
# Work around for https://github.com/rackspace/pyrax/pull/389
|
||||
if 'flavor' not in lc_args:
|
||||
lc_args['flavor'] = lc.get('flavor')
|
||||
changed = True
|
||||
sg.update_launch_config(**lc_args)
|
||||
|
||||
sg.get()
|
||||
|
||||
if wait:
|
||||
end_time = time.time() + wait_timeout
|
||||
infinite = wait_timeout == 0
|
||||
while infinite or time.time() < end_time:
|
||||
state = sg.get_state()
|
||||
if state["pending_capacity"] == 0:
|
||||
break
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
module.exit_json(changed=changed, autoscale_group=rax_to_dict(sg))
|
||||
|
||||
else:
|
||||
try:
|
||||
sg = au.find(name=name)
|
||||
sg.delete()
|
||||
changed = True
|
||||
except pyrax.exceptions.NotFound as e:
|
||||
sg = {}
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, autoscale_group=rax_to_dict(sg))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
config_drive=dict(default=False, type='bool'),
|
||||
cooldown=dict(type='int', default=300),
|
||||
disk_config=dict(choices=['auto', 'manual']),
|
||||
files=dict(type='dict', default={}),
|
||||
flavor=dict(required=True),
|
||||
image=dict(required=True),
|
||||
key_name=dict(),
|
||||
loadbalancers=dict(type='list'),
|
||||
meta=dict(type='dict', default={}),
|
||||
min_entities=dict(type='int', required=True),
|
||||
max_entities=dict(type='int', required=True),
|
||||
name=dict(required=True),
|
||||
networks=dict(type='list', default=['public', 'private']),
|
||||
server_name=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
user_data=dict(no_log=True),
|
||||
wait=dict(default=False, type='bool'),
|
||||
wait_timeout=dict(default=300, type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
config_drive = module.params.get('config_drive')
|
||||
cooldown = module.params.get('cooldown')
|
||||
disk_config = module.params.get('disk_config')
|
||||
if disk_config:
|
||||
disk_config = disk_config.upper()
|
||||
files = module.params.get('files')
|
||||
flavor = module.params.get('flavor')
|
||||
image = module.params.get('image')
|
||||
key_name = module.params.get('key_name')
|
||||
loadbalancers = module.params.get('loadbalancers')
|
||||
meta = module.params.get('meta')
|
||||
min_entities = module.params.get('min_entities')
|
||||
max_entities = module.params.get('max_entities')
|
||||
name = module.params.get('name')
|
||||
networks = module.params.get('networks')
|
||||
server_name = module.params.get('server_name')
|
||||
state = module.params.get('state')
|
||||
user_data = module.params.get('user_data')
|
||||
|
||||
if not 0 <= min_entities <= 1000 or not 0 <= max_entities <= 1000:
|
||||
module.fail_json(msg='min_entities and max_entities must be an '
|
||||
'integer between 0 and 1000')
|
||||
|
||||
if not 0 <= cooldown <= 86400:
|
||||
module.fail_json(msg='cooldown must be an integer between 0 and 86400')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_asg(module, cooldown=cooldown, disk_config=disk_config,
|
||||
files=files, flavor=flavor, image=image, meta=meta,
|
||||
key_name=key_name, loadbalancers=loadbalancers,
|
||||
min_entities=min_entities, max_entities=max_entities,
|
||||
name=name, networks=networks, server_name=server_name,
|
||||
state=state, config_drive=config_drive, user_data=user_data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
281
plugins/modules/cloud/rackspace/rax_scaling_policy.py
Normal file
281
plugins/modules/cloud/rackspace/rax_scaling_policy.py
Normal file
|
@ -0,0 +1,281 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_scaling_policy
|
||||
short_description: Manipulate Rackspace Cloud Autoscale Scaling Policy
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Autoscale Scaling Policy
|
||||
options:
|
||||
at:
|
||||
description:
|
||||
- The UTC time when this policy will be executed. The time must be
|
||||
formatted according to C(yyyy-MM-dd'T'HH:mm:ss.SSS) such as
|
||||
C(2013-05-19T08:07:08Z)
|
||||
change:
|
||||
description:
|
||||
- The change, either as a number of servers or as a percentage, to make
|
||||
in the scaling group. If this is a percentage, you must set
|
||||
I(is_percent) to C(true) also.
|
||||
cron:
|
||||
description:
|
||||
- The time when the policy will be executed, as a cron entry. For
|
||||
example, if this is parameter is set to C(1 0 * * *)
|
||||
cooldown:
|
||||
description:
|
||||
- The period of time, in seconds, that must pass before any scaling can
|
||||
occur after the previous scaling. Must be an integer between 0 and
|
||||
86400 (24 hrs).
|
||||
desired_capacity:
|
||||
description:
|
||||
- The desired server capacity of the scaling the group; that is, how
|
||||
many servers should be in the scaling group.
|
||||
is_percent:
|
||||
description:
|
||||
- Whether the value in I(change) is a percent value
|
||||
default: false
|
||||
type: bool
|
||||
name:
|
||||
description:
|
||||
- Name to give the policy
|
||||
required: true
|
||||
policy_type:
|
||||
description:
|
||||
- The type of policy that will be executed for the current release.
|
||||
choices:
|
||||
- webhook
|
||||
- schedule
|
||||
required: true
|
||||
scaling_group:
|
||||
description:
|
||||
- Name of the scaling group that this policy will be added to
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment:
|
||||
- community.general.rackspace
|
||||
- community.general.rackspace.openstack
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
---
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tasks:
|
||||
- rax_scaling_policy:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
at: '2013-05-19T08:07:08Z'
|
||||
change: 25
|
||||
cooldown: 300
|
||||
is_percent: true
|
||||
name: ASG Test Policy - at
|
||||
policy_type: schedule
|
||||
scaling_group: ASG Test
|
||||
register: asps_at
|
||||
|
||||
- rax_scaling_policy:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
cron: '1 0 * * *'
|
||||
change: 25
|
||||
cooldown: 300
|
||||
is_percent: true
|
||||
name: ASG Test Policy - cron
|
||||
policy_type: schedule
|
||||
scaling_group: ASG Test
|
||||
register: asp_cron
|
||||
|
||||
- rax_scaling_policy:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
cooldown: 300
|
||||
desired_capacity: 5
|
||||
name: ASG Test Policy - webhook
|
||||
policy_type: webhook
|
||||
scaling_group: ASG Test
|
||||
register: asp_webhook
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.rax import (UUID, rax_argument_spec, rax_required_together, rax_to_dict,
|
||||
setup_rax_module)
|
||||
|
||||
|
||||
def rax_asp(module, at=None, change=0, cron=None, cooldown=300,
|
||||
desired_capacity=0, is_percent=False, name=None,
|
||||
policy_type=None, scaling_group=None, state='present'):
|
||||
changed = False
|
||||
|
||||
au = pyrax.autoscale
|
||||
if not au:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
try:
|
||||
UUID(scaling_group)
|
||||
except ValueError:
|
||||
try:
|
||||
sg = au.find(name=scaling_group)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
try:
|
||||
sg = au.get(scaling_group)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if state == 'present':
|
||||
policies = filter(lambda p: name == p.name, sg.list_policies())
|
||||
if len(policies) > 1:
|
||||
module.fail_json(msg='No unique policy match found by name')
|
||||
if at:
|
||||
args = dict(at=at)
|
||||
elif cron:
|
||||
args = dict(cron=cron)
|
||||
else:
|
||||
args = None
|
||||
|
||||
if not policies:
|
||||
try:
|
||||
policy = sg.add_policy(name, policy_type=policy_type,
|
||||
cooldown=cooldown, change=change,
|
||||
is_percent=is_percent,
|
||||
desired_capacity=desired_capacity,
|
||||
args=args)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
else:
|
||||
policy = policies[0]
|
||||
kwargs = {}
|
||||
if policy_type != policy.type:
|
||||
kwargs['policy_type'] = policy_type
|
||||
|
||||
if cooldown != policy.cooldown:
|
||||
kwargs['cooldown'] = cooldown
|
||||
|
||||
if hasattr(policy, 'change') and change != policy.change:
|
||||
kwargs['change'] = change
|
||||
|
||||
if hasattr(policy, 'changePercent') and is_percent is False:
|
||||
kwargs['change'] = change
|
||||
kwargs['is_percent'] = False
|
||||
elif hasattr(policy, 'change') and is_percent is True:
|
||||
kwargs['change'] = change
|
||||
kwargs['is_percent'] = True
|
||||
|
||||
if hasattr(policy, 'desiredCapacity') and change:
|
||||
kwargs['change'] = change
|
||||
elif ((hasattr(policy, 'change') or
|
||||
hasattr(policy, 'changePercent')) and desired_capacity):
|
||||
kwargs['desired_capacity'] = desired_capacity
|
||||
|
||||
if hasattr(policy, 'args') and args != policy.args:
|
||||
kwargs['args'] = args
|
||||
|
||||
if kwargs:
|
||||
policy.update(**kwargs)
|
||||
changed = True
|
||||
|
||||
policy.get()
|
||||
|
||||
module.exit_json(changed=changed, autoscale_policy=rax_to_dict(policy))
|
||||
|
||||
else:
|
||||
try:
|
||||
policies = filter(lambda p: name == p.name, sg.list_policies())
|
||||
if len(policies) > 1:
|
||||
module.fail_json(msg='No unique policy match found by name')
|
||||
elif not policies:
|
||||
policy = {}
|
||||
else:
|
||||
policy.delete()
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, autoscale_policy=rax_to_dict(policy))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
at=dict(),
|
||||
change=dict(type='int'),
|
||||
cron=dict(),
|
||||
cooldown=dict(type='int', default=300),
|
||||
desired_capacity=dict(type='int'),
|
||||
is_percent=dict(type='bool', default=False),
|
||||
name=dict(required=True),
|
||||
policy_type=dict(required=True, choices=['webhook', 'schedule']),
|
||||
scaling_group=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[
|
||||
['cron', 'at'],
|
||||
['change', 'desired_capacity'],
|
||||
]
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
at = module.params.get('at')
|
||||
change = module.params.get('change')
|
||||
cron = module.params.get('cron')
|
||||
cooldown = module.params.get('cooldown')
|
||||
desired_capacity = module.params.get('desired_capacity')
|
||||
is_percent = module.params.get('is_percent')
|
||||
name = module.params.get('name')
|
||||
policy_type = module.params.get('policy_type')
|
||||
scaling_group = module.params.get('scaling_group')
|
||||
state = module.params.get('state')
|
||||
|
||||
if (at or cron) and policy_type == 'webhook':
|
||||
module.fail_json(msg='policy_type=schedule is required for a time '
|
||||
'based policy')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_asp(module, at=at, change=change, cron=cron, cooldown=cooldown,
|
||||
desired_capacity=desired_capacity, is_percent=is_percent,
|
||||
name=name, policy_type=policy_type, scaling_group=scaling_group,
|
||||
state=state)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue