Initial commit

This commit is contained in:
Ansible Core Team 2020-03-09 09:11:07 +00:00
commit aebc1b03fd
4861 changed files with 812621 additions and 0 deletions

View file

View file

@ -0,0 +1,817 @@
#!/usr/bin/python
# Copyright (c) 2017 Alibaba Group Holding Limited. He Guimin <heguimin36@163.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see http://www.gnu.org/licenses/.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = r'''
---
module: ali_instance
short_description: Create, start, stop, restart or terminate an instance in ECS, add or remove an instance to/from a security group
description:
- Create, start, stop, restart, modify or terminate ecs instances.
- Add or remove ecs instances to/from security group.
options:
state:
description:
- The state of the instance after operating.
type: str
default: 'present'
choices: ['absent', 'present', 'restarted', 'running', 'stopped']
availability_zone:
description:
- Aliyun availability zone ID in which to launch the instance.
- If it is not specified, it will be allocated by system automatically.
aliases: ['alicloud_zone']
type: str
image_id:
description:
- Image ID used to launch instances.
- Required when I(state=present) and creating new ECS instances.
aliases: ['image']
type: str
instance_type:
description:
- Instance type used to launch instances.
- Required when I(state=present) and creating new ECS instances.
aliases: ['type']
type: str
security_groups:
description:
- A list of security group IDs.
type: list
vswitch_id:
description:
- The subnet ID in which to launch the instances (VPC).
aliases: ['subnet_id']
type: str
instance_name:
description:
- The name of ECS instance, which is a string of 2 to 128 Chinese or English characters.
- It must begin with an uppercase/lowercase letter or a Chinese character and
can contain numerals, ".", "_" or "-". It cannot begin with http:// or https://.
aliases: ['name']
type: str
description:
description:
- The description of ECS instance, which is a string of 2 to 256 characters.
- It cannot begin with http:// or https://.
type: str
internet_charge_type:
description:
- Internet charge type of ECS instance.
type: str
default: 'PayByBandwidth'
choices: ['PayByBandwidth', 'PayByTraffic']
max_bandwidth_in:
description:
- Maximum incoming bandwidth from the public network,
measured in Mbps (Megabits per second).
default: 200
type: int
max_bandwidth_out:
description:
- Maximum outgoing bandwidth to the public network, measured in Mbps (Megabits per second).
type: int
default: 0
host_name:
description:
- Instance host name.
type: str
password:
description:
- The password to login instance.
- After rebooting instances, modified password will take effect.
type: str
system_disk_category:
description:
- Category of the system disk.
type: str
default: 'cloud_efficiency'
choices: ['cloud_efficiency', 'cloud_ssd']
system_disk_size:
description:
- Size of the system disk, in GB. The valid values are 40~500.
type: int
default: 40
system_disk_name:
description:
- Name of the system disk.
type: str
system_disk_description:
description:
- Description of the system disk.
type: str
count:
description:
- The number of the new instance.
- Indicates how many instances that match I(count_tag) should be running.
- Instances are either created or terminated based on this value.
type: int
default: 1
count_tag:
description:
- Determines how many instances based on a specific tag criteria should be present.
- This can be expressed in multiple ways and is shown in the EXAMPLES section.
- The specified count_tag must already exist or be passed in as the I(instance_tags) option.
- If it is not specified, it will be replaced by I(instance_name).
type: str
allocate_public_ip:
description:
- Whether allocate a public ip for the new instance.
default: False
aliases: ['assign_public_ip']
type: bool
instance_charge_type:
description:
- The charge type of the instance.
type: str
choices: ['PrePaid', 'PostPaid']
default: 'PostPaid'
period:
description:
- The charge duration of the instance, in month.
- Required when I(instance_charge_type=PrePaid).
- The valid value are [1-9, 12, 24, 36].
type: int
default: 1
auto_renew:
description:
- Whether automate renew the charge of the instance.
type: bool
default: False
auto_renew_period:
description:
- The duration of the automatic renew the charge of the instance.
- Required when I(auto_renew=True).
type: int
choices: [1, 2, 3, 6, 12]
instance_ids:
description:
- A list of instance ids. It is required when need to operate existing instances.
- If it is specified, I(count) will lose efficacy.
type: list
force:
description:
- Whether the current operation needs to be execute forcibly.
default: False
type: bool
instance_tags:
description:
- A hash/dictionaries of instance tags, to add to the new instance or
for starting/stopping instance by tag (C({"key":"value"})).
aliases: ['tags']
type: dict
key_name:
description:
- The name of key pair which is used to access ECS instance in SSH.
type: str
required: false
aliases: ['keypair']
user_data:
description:
- User-defined data to customize the startup behaviors of an ECS instance and to pass data into an ECS instance.
It only will take effect when launching the new ECS instances.
required: false
type: str
author:
- "He Guimin (@xiaozhu36)"
requirements:
- "python >= 2.6"
- "footmark >= 1.1.16"
extends_documentation_fragment:
- community.general.alicloud
'''
EXAMPLES = r'''
# basic provisioning example vpc network
- name: basic provisioning example
hosts: localhost
vars:
alicloud_access_key: <your-alicloud-access-key-id>
alicloud_secret_key: <your-alicloud-access-secret-key>
alicloud_region: cn-beijing
image: ubuntu1404_64_40G_cloudinit_20160727.raw
instance_type: ecs.n4.small
vswitch_id: vsw-abcd1234
assign_public_ip: True
max_bandwidth_out: 10
host_name: myhost
password: mypassword
system_disk_category: cloud_efficiency
system_disk_size: 100
internet_charge_type: PayByBandwidth
security_groups: ["sg-f2rwnfh23r"]
instance_ids: ["i-abcd12346", "i-abcd12345"]
force: True
tasks:
- name: launch ECS instance in VPC network
ali_instance:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
image: '{{ image }}'
system_disk_category: '{{ system_disk_category }}'
system_disk_size: '{{ system_disk_size }}'
instance_type: '{{ instance_type }}'
vswitch_id: '{{ vswitch_id }}'
assign_public_ip: '{{ assign_public_ip }}'
internet_charge_type: '{{ internet_charge_type }}'
max_bandwidth_out: '{{ max_bandwidth_out }}'
instance_tags:
Name: created_one
host_name: '{{ host_name }}'
password: '{{ password }}'
- name: with count and count_tag to create a number of instances
ali_instance:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
image: '{{ image }}'
system_disk_category: '{{ system_disk_category }}'
system_disk_size: '{{ system_disk_size }}'
instance_type: '{{ instance_type }}'
assign_public_ip: '{{ assign_public_ip }}'
security_groups: '{{ security_groups }}'
internet_charge_type: '{{ internet_charge_type }}'
max_bandwidth_out: '{{ max_bandwidth_out }}'
instance_tags:
Name: created_one
Version: 0.1
count: 2
count_tag:
Name: created_one
host_name: '{{ host_name }}'
password: '{{ password }}'
- name: start instance
ali_instance:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
instance_ids: '{{ instance_ids }}'
state: 'running'
- name: reboot instance forcibly
ecs:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
instance_ids: '{{ instance_ids }}'
state: 'restarted'
force: '{{ force }}'
- name: Add instances to an security group
ecs:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
instance_ids: '{{ instance_ids }}'
security_groups: '{{ security_groups }}'
'''
RETURN = r'''
instances:
description: List of ECS instances.
returned: always
type: complex
contains:
availability_zone:
description: The availability zone of the instance is in.
returned: always
type: str
sample: cn-beijing-a
block_device_mappings:
description: Any block device mapping entries for the instance.
returned: always
type: complex
contains:
device_name:
description: The device name exposed to the instance (for example, /dev/xvda).
returned: always
type: str
sample: /dev/xvda
attach_time:
description: The time stamp when the attachment initiated.
returned: always
type: str
sample: "2018-06-25T04:08:26Z"
delete_on_termination:
description: Indicates whether the volume is deleted on instance termination.
returned: always
type: bool
sample: true
status:
description: The attachment state.
returned: always
type: str
sample: in_use
volume_id:
description: The ID of the cloud disk.
returned: always
type: str
sample: d-2zei53pjsi117y6gf9t6
cpu:
description: The CPU core count of the instance.
returned: always
type: int
sample: 4
creation_time:
description: The time the instance was created.
returned: always
type: str
sample: "2018-06-25T04:08Z"
description:
description: The instance description.
returned: always
type: str
sample: "my ansible instance"
eip:
description: The attribution of EIP associated with the instance.
returned: always
type: complex
contains:
allocation_id:
description: The ID of the EIP.
returned: always
type: str
sample: eip-12345
internet_charge_type:
description: The internet charge type of the EIP.
returned: always
type: str
sample: "paybybandwidth"
ip_address:
description: EIP address.
returned: always
type: str
sample: 42.10.2.2
expired_time:
description: The time the instance will expire.
returned: always
type: str
sample: "2099-12-31T15:59Z"
gpu:
description: The attribution of instance GPU.
returned: always
type: complex
contains:
amount:
description: The count of the GPU.
returned: always
type: int
sample: 0
spec:
description: The specification of the GPU.
returned: always
type: str
sample: ""
host_name:
description: The host name of the instance.
returned: always
type: str
sample: iZ2zewaoZ
id:
description: Alias of instance_id.
returned: always
type: str
sample: i-abc12345
instance_id:
description: ECS instance resource ID.
returned: always
type: str
sample: i-abc12345
image_id:
description: The ID of the image used to launch the instance.
returned: always
type: str
sample: m-0011223344
inner_ip_address:
description: The inner IPv4 address of the classic instance.
returned: always
type: str
sample: 10.0.0.2
instance_charge_type:
description: The instance charge type.
returned: always
type: str
sample: PostPaid
instance_name:
description: The name of the instance.
returned: always
type: str
sample: my-ecs
instance_type:
description: The instance type of the running instance.
returned: always
type: str
sample: ecs.sn1ne.xlarge
internet_charge_type:
description: The billing method of the network bandwidth.
returned: always
type: str
sample: PayByBandwidth
internet_max_bandwidth_in:
description: Maximum incoming bandwidth from the internet network.
returned: always
type: int
sample: 200
internet_max_bandwidth_out:
description: Maximum incoming bandwidth from the internet network.
returned: always
type: int
sample: 20
io_optimized:
description: Indicates whether the instance is optimized for EBS I/O.
returned: always
type: bool
sample: false
memory:
description: Memory size of the instance.
returned: always
type: int
sample: 8192
network_interfaces:
description: One or more network interfaces for the instance.
returned: always
type: complex
contains:
mac_address:
description: The MAC address.
returned: always
type: str
sample: "00:11:22:33:44:55"
network_interface_id:
description: The ID of the network interface.
returned: always
type: str
sample: eni-01234567
primary_ip_address:
description: The primary IPv4 address of the network interface within the vswitch.
returned: always
type: str
sample: 10.0.0.1
osname:
description: The operation system name of the instance owned.
returned: always
type: str
sample: CentOS
ostype:
description: The operation system type of the instance owned.
returned: always
type: str
sample: linux
private_ip_address:
description: The IPv4 address of the network interface within the subnet.
returned: always
type: str
sample: 10.0.0.1
public_ip_address:
description: The public IPv4 address assigned to the instance.
returned: always
type: str
sample: 43.0.0.1
resource_group_id:
description: The id of the resource group to which the instance belongs.
returned: always
type: str
sample: my-ecs-group
security_groups:
description: One or more security groups for the instance.
returned: always
type: list
elements: dict
contains:
group_id:
description: The ID of the security group.
returned: always
type: str
sample: sg-0123456
group_name:
description: The name of the security group.
returned: always
type: str
sample: my-security-group
status:
description: The current status of the instance.
returned: always
type: str
sample: running
tags:
description: Any tags assigned to the instance.
returned: always
type: dict
sample:
vswitch_id:
description: The ID of the vswitch in which the instance is running.
returned: always
type: str
sample: vsw-dew00abcdef
vpc_id:
description: The ID of the VPC the instance is in.
returned: always
type: dict
sample: vpc-0011223344
ids:
description: List of ECS instance IDs.
returned: always
type: list
sample: [i-12345er, i-3245fs]
'''
import time
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.alicloud_ecs import ecs_argument_spec, ecs_connect
HAS_FOOTMARK = False
FOOTMARK_IMP_ERR = None
try:
from footmark.exception import ECSResponseError
HAS_FOOTMARK = True
except ImportError:
FOOTMARK_IMP_ERR = traceback.format_exc()
HAS_FOOTMARK = False
def get_instances_info(connection, ids):
result = []
instances = connection.get_all_instances(instance_ids=ids)
if len(instances) > 0:
for inst in instances:
result.append(inst.read())
return result
def create_instance(module, ecs, exact_count):
if exact_count <= 0:
return None
zone_id = module.params['availability_zone']
image_id = module.params['image_id']
instance_type = module.params['instance_type']
security_groups = module.params['security_groups']
vswitch_id = module.params['vswitch_id']
instance_name = module.params['instance_name']
description = module.params['description']
internet_charge_type = module.params['internet_charge_type']
max_bandwidth_out = module.params['max_bandwidth_out']
max_bandwidth_in = module.params['max_bandwidth_out']
host_name = module.params['host_name']
password = module.params['password']
system_disk_category = module.params['system_disk_category']
system_disk_size = module.params['system_disk_size']
system_disk_name = module.params['system_disk_name']
system_disk_description = module.params['system_disk_description']
allocate_public_ip = module.params['allocate_public_ip']
instance_tags = module.params['instance_tags']
period = module.params['period']
auto_renew = module.params['auto_renew']
instance_charge_type = module.params['instance_charge_type']
auto_renew_period = module.params['auto_renew_period']
user_data = module.params['user_data']
key_name = module.params['key_name']
# check whether the required parameter passed or not
if not image_id:
module.fail_json(msg='image_id is required for new instance')
if not instance_type:
module.fail_json(msg='instance_type is required for new instance')
if not isinstance(security_groups, list):
module.fail_json(msg='The parameter security_groups should be a list, aborting')
if len(security_groups) <= 0:
module.fail_json(msg='Expected the parameter security_groups is non-empty when create new ECS instances, aborting')
client_token = "Ansible-Alicloud-{0}-{1}".format(hash(str(module.params)), str(time.time()))
try:
# call to create_instance method from footmark
instances = ecs.create_instance(image_id=image_id, instance_type=instance_type, security_group_id=security_groups[0],
zone_id=zone_id, instance_name=instance_name, description=description,
internet_charge_type=internet_charge_type, max_bandwidth_out=max_bandwidth_out,
max_bandwidth_in=max_bandwidth_in, host_name=host_name, password=password,
io_optimized='optimized', system_disk_category=system_disk_category,
system_disk_size=system_disk_size, system_disk_name=system_disk_name,
system_disk_description=system_disk_description,
vswitch_id=vswitch_id, count=exact_count, allocate_public_ip=allocate_public_ip,
instance_charge_type=instance_charge_type, period=period, auto_renew=auto_renew,
auto_renew_period=auto_renew_period, instance_tags=instance_tags,
key_pair_name=key_name, user_data=user_data, client_token=client_token)
except Exception as e:
module.fail_json(msg='Unable to create instance, error: {0}'.format(e))
return instances
def main():
argument_spec = ecs_argument_spec()
argument_spec.update(dict(
security_groups=dict(type='list'),
availability_zone=dict(type='str', aliases=['alicloud_zone']),
instance_type=dict(type='str', aliases=['type']),
image_id=dict(type='str', aliases=['image']),
count=dict(type='int', default=1),
count_tag=dict(type='str'),
vswitch_id=dict(type='str', aliases=['subnet_id']),
instance_name=dict(type='str', aliases=['name']),
host_name=dict(type='str'),
password=dict(type='str', no_log=True),
internet_charge_type=dict(type='str', default='PayByBandwidth', choices=['PayByBandwidth', 'PayByTraffic']),
max_bandwidth_in=dict(type='int', default=200),
max_bandwidth_out=dict(type='int', default=0),
system_disk_category=dict(type='str', default='cloud_efficiency', choices=['cloud_efficiency', 'cloud_ssd']),
system_disk_size=dict(type='int', default=40),
system_disk_name=dict(type='str'),
system_disk_description=dict(type='str'),
force=dict(type='bool', default=False),
instance_tags=dict(type='dict', aliases=['tags']),
state=dict(default='present', choices=['present', 'running', 'stopped', 'restarted', 'absent']),
description=dict(type='str'),
allocate_public_ip=dict(type='bool', aliases=['assign_public_ip'], default=False),
instance_charge_type=dict(type='str', default='PostPaid', choices=['PrePaid', 'PostPaid']),
period=dict(type='int', default=1),
auto_renew=dict(type='bool', default=False),
instance_ids=dict(type='list'),
auto_renew_period=dict(type='int', choices=[1, 2, 3, 6, 12]),
key_name=dict(type='str', aliases=['keypair']),
user_data=dict(type='str')
)
)
module = AnsibleModule(argument_spec=argument_spec)
if HAS_FOOTMARK is False:
module.fail_json(msg=missing_required_lib('footmark'), exception=FOOTMARK_IMP_ERR)
ecs = ecs_connect(module)
state = module.params['state']
instance_ids = module.params['instance_ids']
count_tag = module.params['count_tag']
count = module.params['count']
instance_name = module.params['instance_name']
force = module.params['force']
zone_id = module.params['availability_zone']
key_name = module.params['key_name']
changed = False
instances = []
if instance_ids:
if not isinstance(instance_ids, list):
module.fail_json(msg='The parameter instance_ids should be a list, aborting')
instances = ecs.get_all_instances(zone_id=zone_id, instance_ids=instance_ids)
if not instances:
module.fail_json(msg="There are no instances in our record based on instance_ids {0}. "
"Please check it and try again.".format(instance_ids))
elif count_tag:
instances = ecs.get_all_instances(zone_id=zone_id, instance_tags=eval(count_tag))
elif instance_name:
instances = ecs.get_all_instances(zone_id=zone_id, instance_name=instance_name)
ids = []
if state == 'present':
if not instance_ids:
if len(instances) > count:
for i in range(0, len(instances) - count):
inst = instances[len(instances) - 1]
if inst.status != 'stopped' and not force:
module.fail_json(msg="That to delete instance {0} is failed results from it is running, "
"and please stop it or set 'force' as True.".format(inst.id))
try:
changed = inst.terminate(force=force)
except Exception as e:
module.fail_json(msg="Delete instance {0} got an error: {1}".format(inst.id, e))
instances.pop(len(instances) - 1)
else:
try:
new_instances = create_instance(module, ecs, count - len(instances))
if new_instances:
changed = True
instances.extend(new_instances)
except Exception as e:
module.fail_json(msg="Create new instances got an error: {0}".format(e))
# Security Group join/leave begin
security_groups = module.params['security_groups']
if not isinstance(security_groups, list):
module.fail_json(msg='The parameter security_groups should be a list, aborting')
if len(security_groups) > 0:
for inst in instances:
existing = inst.security_group_ids['security_group_id']
remove = list(set(existing).difference(set(security_groups)))
add = list(set(security_groups).difference(set(existing)))
for sg in remove:
if inst.leave_security_group(sg):
changed = True
for sg in add:
if inst.join_security_group(sg):
changed = True
# Security Group join/leave ends here
# Attach/Detach key pair
inst_ids = []
for inst in instances:
if key_name is not None and key_name != inst.key_name:
if key_name == "":
changed = inst.detach_key_pair()
else:
inst_ids.append(inst.id)
if inst_ids:
changed = ecs.attach_key_pair(instance_ids=inst_ids, key_pair_name=key_name)
# Modify instance attribute
description = module.params['description']
host_name = module.params['host_name']
password = module.params['password']
for inst in instances:
if not instance_name:
instance_name = inst.name
if not description:
description = inst.description
if not host_name:
host_name = inst.host_name
try:
if inst.modify(name=instance_name, description=description, host_name=host_name, password=password):
changed = True
except Exception as e:
module.fail_json(msg="Modify instance attribute {0} got an error: {1}".format(inst.id, e))
if inst.id not in ids:
ids.append(inst.id)
module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids))
else:
if len(instances) < 1:
module.fail_json(msg='Please specify ECS instances that you want to operate by using '
'parameters instance_ids, instance_tags or instance_name, aborting')
force = module.params['force']
if state == 'running':
try:
for inst in instances:
if inst.start():
changed = True
ids.append(inst.id)
module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids))
except Exception as e:
module.fail_json(msg='Start instances got an error: {0}'.format(e))
elif state == 'stopped':
try:
for inst in instances:
if inst.stop(force=force):
changed = True
ids.append(inst.id)
module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids))
except Exception as e:
module.fail_json(msg='Stop instances got an error: {0}'.format(e))
elif state == 'restarted':
try:
for inst in instances:
if inst.reboot(force=module.params['force']):
changed = True
ids.append(inst.id)
module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids))
except Exception as e:
module.fail_json(msg='Reboot instances got an error: {0}'.format(e))
else:
try:
for inst in instances:
if inst.status != 'stopped' and not force:
module.fail_json(msg="Instance is running, and please stop it or set 'force' as True.")
if inst.terminate(force=module.params['force']):
changed = True
module.exit_json(changed=changed, ids=[], instances=[])
except Exception as e:
module.fail_json(msg='Delete instance got an error: {0}'.format(e))
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
ali_instance_info.py

View file

@ -0,0 +1,411 @@
#!/usr/bin/python
# Copyright (c) 2017 Alibaba Group Holding Limited. He Guimin <heguimin36@163.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see http://www.gnu.org/licenses/.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: ali_instance_info
short_description: Gather information on instances of Alibaba Cloud ECS.
description:
- This module fetches data from the Open API in Alicloud.
The module must be called from within the ECS instance itself.
- This module was called C(ali_instance_facts) before Ansible 2.9. The usage did not change.
options:
availability_zone:
description:
- Aliyun availability zone ID in which to launch the instance
aliases: ['alicloud_zone']
instance_names:
description:
- A list of ECS instance names.
aliases: [ "names"]
instance_ids:
description:
- A list of ECS instance ids.
aliases: ["ids"]
instance_tags:
description:
- A hash/dictionaries of instance tags. C({"key":"value"})
aliases: ["tags"]
author:
- "He Guimin (@xiaozhu36)"
requirements:
- "python >= 2.6"
- "footmark >= 1.1.16"
extends_documentation_fragment:
- community.general.alicloud
'''
EXAMPLES = '''
# Fetch instances details according to setting different filters
- name: fetch instances details example
hosts: localhost
vars:
alicloud_access_key: <your-alicloud-access-key>
alicloud_secret_key: <your-alicloud-secret-key>
alicloud_region: cn-beijing
availability_zone: cn-beijing-a
tasks:
- name: Find all instances in the specified region
ali_instance_info:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
register: all_instances
- name: Find all instances based on the specified ids
ali_instance_info:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
instance_ids:
- "i-35b333d9"
- "i-ddav43kd"
register: instances_by_ids
- name: Find all instances based on the specified names/name-prefixes
ali_instance_info:
alicloud_access_key: '{{ alicloud_access_key }}'
alicloud_secret_key: '{{ alicloud_secret_key }}'
alicloud_region: '{{ alicloud_region }}'
instance_names:
- "ecs_instance-1"
- "ecs_instance_2"
register: instances_by_ids
'''
RETURN = '''
instances:
description: List of ECS instances
returned: always
type: complex
contains:
availability_zone:
description: The availability zone of the instance is in.
returned: always
type: str
sample: cn-beijing-a
block_device_mappings:
description: Any block device mapping entries for the instance.
returned: always
type: complex
contains:
device_name:
description: The device name exposed to the instance (for example, /dev/xvda).
returned: always
type: str
sample: /dev/xvda
attach_time:
description: The time stamp when the attachment initiated.
returned: always
type: str
sample: "2018-06-25T04:08:26Z"
delete_on_termination:
description: Indicates whether the volume is deleted on instance termination.
returned: always
type: bool
sample: true
status:
description: The attachment state.
returned: always
type: str
sample: in_use
volume_id:
description: The ID of the cloud disk.
returned: always
type: str
sample: d-2zei53pjsi117y6gf9t6
cpu:
description: The CPU core count of the instance.
returned: always
type: int
sample: 4
creation_time:
description: The time the instance was created.
returned: always
type: str
sample: "2018-06-25T04:08Z"
description:
description: The instance description.
returned: always
type: str
sample: "my ansible instance"
eip:
description: The attribution of EIP associated with the instance.
returned: always
type: complex
contains:
allocation_id:
description: The ID of the EIP.
returned: always
type: str
sample: eip-12345
internet_charge_type:
description: The internet charge type of the EIP.
returned: always
type: str
sample: "paybybandwidth"
ip_address:
description: EIP address.
returned: always
type: str
sample: 42.10.2.2
expired_time:
description: The time the instance will expire.
returned: always
type: str
sample: "2099-12-31T15:59Z"
gpu:
description: The attribution of instance GPU.
returned: always
type: complex
contains:
amount:
description: The count of the GPU.
returned: always
type: int
sample: 0
spec:
description: The specification of the GPU.
returned: always
type: str
sample: ""
host_name:
description: The host name of the instance.
returned: always
type: str
sample: iZ2zewaoZ
id:
description: Alias of instance_id.
returned: always
type: str
sample: i-abc12345
instance_id:
description: ECS instance resource ID.
returned: always
type: str
sample: i-abc12345
image_id:
description: The ID of the image used to launch the instance.
returned: always
type: str
sample: m-0011223344
inner_ip_address:
description: The inner IPv4 address of the classic instance.
returned: always
type: str
sample: 10.0.0.2
instance_charge_type:
description: The instance charge type.
returned: always
type: str
sample: PostPaid
instance_name:
description: The name of the instance.
returned: always
type: str
sample: my-ecs
instance_type:
description: The instance type of the running instance.
returned: always
type: str
sample: ecs.sn1ne.xlarge
internet_charge_type:
description: The billing method of the network bandwidth.
returned: always
type: str
sample: PayByBandwidth
internet_max_bandwidth_in:
description: Maximum incoming bandwidth from the internet network.
returned: always
type: int
sample: 200
internet_max_bandwidth_out:
description: Maximum incoming bandwidth from the internet network.
returned: always
type: int
sample: 20
io_optimized:
description: Indicates whether the instance is optimized for EBS I/O.
returned: always
type: bool
sample: false
memory:
description: Memory size of the instance.
returned: always
type: int
sample: 8192
network_interfaces:
description: One or more network interfaces for the instance.
returned: always
type: complex
contains:
mac_address:
description: The MAC address.
returned: always
type: str
sample: "00:11:22:33:44:55"
network_interface_id:
description: The ID of the network interface.
returned: always
type: str
sample: eni-01234567
primary_ip_address:
description: The primary IPv4 address of the network interface within the vswitch.
returned: always
type: str
sample: 10.0.0.1
osname:
description: The operation system name of the instance owned.
returned: always
type: str
sample: CentOS
ostype:
description: The operation system type of the instance owned.
returned: always
type: str
sample: linux
private_ip_address:
description: The IPv4 address of the network interface within the subnet.
returned: always
type: str
sample: 10.0.0.1
public_ip_address:
description: The public IPv4 address assigned to the instance
returned: always
type: str
sample: 43.0.0.1
resource_group_id:
description: The id of the resource group to which the instance belongs.
returned: always
type: str
sample: my-ecs-group
security_groups:
description: One or more security groups for the instance.
returned: always
type: list
elements: dict
contains:
group_id:
description: The ID of the security group.
returned: always
type: str
sample: sg-0123456
group_name:
description: The name of the security group.
returned: always
type: str
sample: my-security-group
status:
description: The current status of the instance.
returned: always
type: str
sample: running
tags:
description: Any tags assigned to the instance.
returned: always
type: dict
sample:
vswitch_id:
description: The ID of the vswitch in which the instance is running.
returned: always
type: str
sample: vsw-dew00abcdef
vpc_id:
description: The ID of the VPC the instance is in.
returned: always
type: dict
sample: vpc-0011223344
ids:
description: List of ECS instance IDs
returned: always
type: list
sample: [i-12345er, i-3245fs]
'''
# import time
# import sys
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.alicloud_ecs import get_acs_connection_info, ecs_argument_spec, ecs_connect
HAS_FOOTMARK = False
FOOTMARK_IMP_ERR = None
try:
from footmark.exception import ECSResponseError
HAS_FOOTMARK = True
except ImportError:
FOOTMARK_IMP_ERR = traceback.format_exc()
HAS_FOOTMARK = False
def main():
argument_spec = ecs_argument_spec()
argument_spec.update(dict(
availability_zone=dict(aliases=['alicloud_zone']),
instance_ids=dict(type='list', aliases=['ids']),
instance_names=dict(type='list', aliases=['names']),
instance_tags=dict(type='list', aliases=['tags']),
)
)
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'ali_instance_facts':
module.deprecate("The 'ali_instance_facts' module has been renamed to 'ali_instance_info'", version='2.13')
if HAS_FOOTMARK is False:
module.fail_json(msg=missing_required_lib('footmark'), exception=FOOTMARK_IMP_ERR)
ecs = ecs_connect(module)
instances = []
instance_ids = []
ids = module.params['instance_ids']
names = module.params['instance_names']
zone_id = module.params['availability_zone']
if ids and (not isinstance(ids, list) or len(ids) < 1):
module.fail_json(msg='instance_ids should be a list of instances, aborting')
if names and (not isinstance(names, list) or len(names) < 1):
module.fail_json(msg='instance_ids should be a list of instances, aborting')
if names:
for name in names:
for inst in ecs.get_all_instances(zone_id=zone_id, instance_ids=ids, instance_name=name):
instances.append(inst.read())
instance_ids.append(inst.id)
else:
for inst in ecs.get_all_instances(zone_id=zone_id, instance_ids=ids):
instances.append(inst.read())
instance_ids.append(inst.id)
module.exit_json(changed=False, ids=instance_ids, instances=instances)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,206 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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: atomic_container
short_description: Manage the containers on the atomic host platform
description:
- Manage the containers on the atomic host platform
- Allows to manage the lifecycle of a container on the atomic host platform
author: "Giuseppe Scrivano (@giuseppe)"
notes:
- Host should support C(atomic) command
requirements:
- atomic
- "python >= 2.6"
options:
backend:
description:
- Define the backend to use for the container
required: True
choices: ["docker", "ostree"]
name:
description:
- Name of the container
required: True
image:
description:
- The image to use to install the container
required: True
rootfs:
description:
- Define the rootfs of the image
state:
description:
- State of the container
required: True
choices: ["latest", "present", "absent", "rollback"]
default: "latest"
mode:
description:
- Define if it is an user or a system container
required: True
choices: ["user", "system"]
values:
description:
- Values for the installation of the container. This option is permitted only with mode 'user' or 'system'.
The values specified here will be used at installation time as --set arguments for atomic install.
'''
EXAMPLES = '''
# Install the etcd system container
- atomic_container:
name: etcd
image: rhel/etcd
backend: ostree
state: latest
mode: system
values:
- ETCD_NAME=etcd.server
# Uninstall the etcd system container
- atomic_container:
name: etcd
image: rhel/etcd
backend: ostree
state: absent
mode: system
'''
RETURN = '''
msg:
description: The command standard output
returned: always
type: str
sample: [u'Using default tag: latest ...']
'''
# import module snippets
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
def do_install(module, mode, rootfs, container, image, values_list, backend):
system_list = ["--system"] if mode == 'system' else []
user_list = ["--user"] if mode == 'user' else []
rootfs_list = ["--rootfs=%s" % rootfs] if rootfs else []
args = ['atomic', 'install', "--storage=%s" % backend, '--name=%s' % container] + system_list + user_list + rootfs_list + values_list + [image]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
else:
changed = "Extracting" in out or "Copying blob" in out
module.exit_json(msg=out, changed=changed)
def do_update(module, container, image, values_list):
args = ['atomic', 'containers', 'update', "--rebase=%s" % image] + values_list + [container]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
else:
changed = "Extracting" in out or "Copying blob" in out
module.exit_json(msg=out, changed=changed)
def do_uninstall(module, name, backend):
args = ['atomic', 'uninstall', "--storage=%s" % backend, name]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
module.exit_json(msg=out, changed=True)
def do_rollback(module, name):
args = ['atomic', 'containers', 'rollback', name]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
else:
changed = "Rolling back" in out
module.exit_json(msg=out, changed=changed)
def core(module):
mode = module.params['mode']
name = module.params['name']
image = module.params['image']
rootfs = module.params['rootfs']
values = module.params['values']
backend = module.params['backend']
state = module.params['state']
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
out = {}
err = {}
rc = 0
values_list = ["--set=%s" % x for x in values] if values else []
args = ['atomic', 'containers', 'list', '--no-trunc', '-n', '--all', '-f', 'backend=%s' % backend, '-f', 'container=%s' % name]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
return
present = name in out
if state == 'present' and present:
module.exit_json(msg=out, changed=False)
elif (state in ['latest', 'present']) and not present:
do_install(module, mode, rootfs, name, image, values_list, backend)
elif state == 'latest':
do_update(module, name, image, values_list)
elif state == 'absent':
if not present:
module.exit_json(msg="The container is not present", changed=False)
else:
do_uninstall(module, name, backend)
elif state == 'rollback':
do_rollback(module, name)
def main():
module = AnsibleModule(
argument_spec=dict(
mode=dict(default=None, choices=['user', 'system']),
name=dict(default=None, required=True),
image=dict(default=None, required=True),
rootfs=dict(default=None),
state=dict(default='latest', choices=['present', 'absent', 'latest', 'rollback']),
backend=dict(default=None, required=True, choices=['docker', 'ostree']),
values=dict(type='list', default=[]),
),
)
if module.params['values'] is not None and module.params['mode'] == 'default':
module.fail_json(msg="values is supported only with user or system mode")
# Verify that the platform supports atomic command
rc, out, err = module.run_command('atomic -v', check_rc=False)
if rc != 0:
module.fail_json(msg="Error in running atomic command", err=err)
try:
core(module)
except Exception as e:
module.fail_json(msg='Unanticipated error running atomic: %s' % to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,103 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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: atomic_host
short_description: Manage the atomic host platform
description:
- Manage the atomic host platform.
- Rebooting of Atomic host platform should be done outside this module.
author:
- Saravanan KR (@krsacme)
notes:
- Host should be an atomic platform (verified by existence of '/run/ostree-booted' file).
requirements:
- atomic
- python >= 2.6
options:
revision:
description:
- The version number of the atomic host to be deployed. Providing C(latest) will upgrade to the latest available version.
default: latest
aliases: [ version ]
'''
EXAMPLES = '''
- name: Upgrade the atomic host platform to the latest version (atomic host upgrade)
atomic_host:
revision: latest
- name: Deploy a specific revision as the atomic host (atomic host deploy 23.130)
atomic_host:
revision: 23.130
'''
RETURN = '''
msg:
description: The command standard output
returned: always
type: str
sample: 'Already on latest'
'''
import os
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
def core(module):
revision = module.params['revision']
args = []
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
if revision == 'latest':
args = ['atomic', 'host', 'upgrade']
else:
args = ['atomic', 'host', 'deploy', revision]
out = {}
err = {}
rc = 0
rc, out, err = module.run_command(args, check_rc=False)
if rc == 77 and revision == 'latest':
module.exit_json(msg="Already on latest", changed=False)
elif rc != 0:
module.fail_json(rc=rc, msg=err)
else:
module.exit_json(msg=out, changed=True)
def main():
module = AnsibleModule(
argument_spec=dict(
revision=dict(type='str', default='latest', aliases=["version"]),
),
)
# Verify that the platform is atomic host
if not os.path.exists("/run/ostree-booted"):
module.fail_json(msg="Module atomic_host is applicable for Atomic Host Platforms only")
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,170 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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: atomic_image
short_description: Manage the container images on the atomic host platform
description:
- Manage the container images on the atomic host platform.
- Allows to execute the commands specified by the RUN label in the container image when present.
author:
- Saravanan KR (@krsacme)
notes:
- Host should support C(atomic) command.
requirements:
- atomic
- python >= 2.6
options:
backend:
description:
- Define the backend where the image is pulled.
choices: [ docker, ostree ]
name:
description:
- Name of the container image.
required: True
state:
description:
- The state of the container image.
- The state C(latest) will ensure container image is upgraded to the latest version and forcefully restart container, if running.
choices: [ absent, latest, present ]
default: latest
started:
description:
- Start or Stop the container.
type: bool
default: 'yes'
'''
EXAMPLES = '''
- name: Execute the run command on rsyslog container image (atomic run rhel7/rsyslog)
atomic_image:
name: rhel7/rsyslog
state: latest
- name: Pull busybox to the OSTree backend
atomic_image:
name: busybox
state: latest
backend: ostree
'''
RETURN = '''
msg:
description: The command standard output
returned: always
type: str
sample: [u'Using default tag: latest ...']
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
def do_upgrade(module, image):
args = ['atomic', 'update', '--force', image]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0: # something went wrong emit the msg
module.fail_json(rc=rc, msg=err)
elif 'Image is up to date' in out:
return False
return True
def core(module):
image = module.params['name']
state = module.params['state']
started = module.params['started']
backend = module.params['backend']
is_upgraded = False
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
out = {}
err = {}
rc = 0
if backend:
if state == 'present' or state == 'latest':
args = ['atomic', 'pull', "--storage=%s" % backend, image]
rc, out, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
else:
out_run = ""
if started:
args = ['atomic', 'run', "--storage=%s" % backend, image]
rc, out_run, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
changed = "Extracting" in out or "Copying blob" in out
module.exit_json(msg=(out + out_run), changed=changed)
elif state == 'absent':
args = ['atomic', 'images', 'delete', "--storage=%s" % backend, image]
rc, out, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
else:
changed = "Unable to find" not in out
module.exit_json(msg=out, changed=changed)
return
if state == 'present' or state == 'latest':
if state == 'latest':
is_upgraded = do_upgrade(module, image)
if started:
args = ['atomic', 'run', image]
else:
args = ['atomic', 'install', image]
elif state == 'absent':
args = ['atomic', 'uninstall', image]
rc, out, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
elif rc == 1 and 'already present' in err:
module.exit_json(restult=err, changed=is_upgraded)
elif started and 'Container is running' in out:
module.exit_json(result=out, changed=is_upgraded)
else:
module.exit_json(msg=out, changed=True)
def main():
module = AnsibleModule(
argument_spec=dict(
backend=dict(type='str', choices=['docker', 'ostree']),
name=dict(type='str', required=True),
state=dict(type='str', default='latest', choices=['absent', 'latest', 'present']),
started=dict(type='bool', default=True),
),
)
# Verify that the platform supports atomic command
rc, out, err = module.run_command('atomic -v', check_rc=False)
if rc != 0:
module.fail_json(msg="Error in running atomic command", err=err)
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,352 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_aa_policy
short_description: Create or Delete Anti Affinity Policies at CenturyLink Cloud.
description:
- An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud.
options:
name:
description:
- The name of the Anti Affinity Policy.
required: True
location:
description:
- Datacenter in which the policy lives/should live.
required: True
state:
description:
- Whether to create or delete the policy.
required: False
default: present
choices: ['present','absent']
wait:
description:
- This option does nothing and will be removed in Ansible 2.14.
type: bool
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
---
- name: Create AA Policy
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Create an Anti Affinity Policy
clc_aa_policy:
name: Hammer Time
location: UK3
state: present
register: policy
- name: debug
debug:
var: policy
---
- name: Delete AA Policy
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Delete an Anti Affinity Policy
clc_aa_policy:
name: Hammer Time
location: UK3
state: absent
register: policy
- name: debug
debug:
var: policy
'''
RETURN = '''
policy:
description: The anti affinity policy information
returned: success
type: dict
sample:
{
"id":"1a28dd0988984d87b9cd61fa8da15424",
"name":"test_aa_policy",
"location":"UC1",
"links":[
{
"rel":"self",
"href":"/v2/antiAffinityPolicies/wfad/1a28dd0988984d87b9cd61fa8da15424",
"verbs":[
"GET",
"DELETE",
"PUT"
]
},
{
"rel":"location",
"href":"/v2/datacenters/wfad/UC1",
"id":"uc1",
"name":"UC1 - US West (Santa Clara)"
}
]
}
'''
__version__ = '${version}'
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk:
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import CLCException
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcAntiAffinityPolicy:
clc = clc_sdk
module = None
def __init__(self, module):
"""
Construct module
"""
self.module = module
self.policy_dict = {}
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'),
exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'),
exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
@staticmethod
def _define_module_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
name=dict(required=True),
location=dict(required=True),
wait=dict(type='bool', removed_in_version='2.14'),
state=dict(default='present', choices=['present', 'absent']),
)
return argument_spec
# Module Behavior Goodness
def process_request(self):
"""
Process the request - Main Code Path
:return: Returns with either an exit_json or fail_json
"""
p = self.module.params
self._set_clc_credentials_from_env()
self.policy_dict = self._get_policies_for_datacenter(p)
if p['state'] == "absent":
changed, policy = self._ensure_policy_is_absent(p)
else:
changed, policy = self._ensure_policy_is_present(p)
if hasattr(policy, 'data'):
policy = policy.data
elif hasattr(policy, '__dict__'):
policy = policy.__dict__
self.module.exit_json(changed=changed, policy=policy)
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
def _get_policies_for_datacenter(self, p):
"""
Get the Policies for a datacenter by calling the CLC API.
:param p: datacenter to get policies from
:return: policies in the datacenter
"""
response = {}
policies = self.clc.v2.AntiAffinity.GetAll(location=p['location'])
for policy in policies:
response[policy.name] = policy
return response
def _create_policy(self, p):
"""
Create an Anti Affinity Policy using the CLC API.
:param p: datacenter to create policy in
:return: response dictionary from the CLC API.
"""
try:
return self.clc.v2.AntiAffinity.Create(
name=p['name'],
location=p['location'])
except CLCException as ex:
self.module.fail_json(msg='Failed to create anti affinity policy : {0}. {1}'.format(
p['name'], ex.response_text
))
def _delete_policy(self, p):
"""
Delete an Anti Affinity Policy using the CLC API.
:param p: datacenter to delete a policy from
:return: none
"""
try:
policy = self.policy_dict[p['name']]
policy.Delete()
except CLCException as ex:
self.module.fail_json(msg='Failed to delete anti affinity policy : {0}. {1}'.format(
p['name'], ex.response_text
))
def _policy_exists(self, policy_name):
"""
Check to see if an Anti Affinity Policy exists
:param policy_name: name of the policy
:return: boolean of if the policy exists
"""
if policy_name in self.policy_dict:
return self.policy_dict.get(policy_name)
return False
def _ensure_policy_is_absent(self, p):
"""
Makes sure that a policy is absent
:param p: dictionary of policy name
:return: tuple of if a deletion occurred and the name of the policy that was deleted
"""
changed = False
if self._policy_exists(policy_name=p['name']):
changed = True
if not self.module.check_mode:
self._delete_policy(p)
return changed, None
def _ensure_policy_is_present(self, p):
"""
Ensures that a policy is present
:param p: dictionary of a policy name
:return: tuple of if an addition occurred and the name of the policy that was added
"""
changed = False
policy = self._policy_exists(policy_name=p['name'])
if not policy:
changed = True
policy = None
if not self.module.check_mode:
policy = self._create_policy(p)
return changed, policy
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
module = AnsibleModule(
argument_spec=ClcAntiAffinityPolicy._define_module_argument_spec(),
supports_check_mode=True)
clc_aa_policy = ClcAntiAffinityPolicy(module)
clc_aa_policy.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,524 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_alert_policy
short_description: Create or Delete Alert Policies at CenturyLink Cloud.
description:
- An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud.
options:
alias:
description:
- The alias of your CLC Account
required: True
name:
description:
- The name of the alert policy. This is mutually exclusive with id
id:
description:
- The alert policy id. This is mutually exclusive with name
alert_recipients:
description:
- A list of recipient email ids to notify the alert.
This is required for state 'present'
metric:
description:
- The metric on which to measure the condition that will trigger the alert.
This is required for state 'present'
choices: ['cpu','memory','disk']
duration:
description:
- The length of time in minutes that the condition must exceed the threshold.
This is required for state 'present'
threshold:
description:
- The threshold that will trigger the alert when the metric equals or exceeds it.
This is required for state 'present'
This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0
state:
description:
- Whether to create or delete the policy.
default: present
choices: ['present','absent']
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
---
- name: Create Alert Policy Example
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Create an Alert Policy for disk above 80% for 5 minutes
clc_alert_policy:
alias: wfad
name: 'alert for disk > 80%'
alert_recipients:
- test1@centurylink.com
- test2@centurylink.com
metric: 'disk'
duration: '00:05:00'
threshold: 80
state: present
register: policy
- name: debug
debug: var=policy
---
- name: Delete Alert Policy Example
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Delete an Alert Policy
clc_alert_policy:
alias: wfad
name: 'alert for disk > 80%'
state: absent
register: policy
- name: debug
debug: var=policy
'''
RETURN = '''
policy:
description: The alert policy information
returned: success
type: dict
sample:
{
"actions": [
{
"action": "email",
"settings": {
"recipients": [
"user1@domain.com",
"user1@domain.com"
]
}
}
],
"id": "ba54ac54a60d4a4f1ed6d48c1ce240a7",
"links": [
{
"href": "/v2/alertPolicies/alias/ba54ac54a60d4a4fb1d6d48c1ce240a7",
"rel": "self",
"verbs": [
"GET",
"DELETE",
"PUT"
]
}
],
"name": "test_alert",
"triggers": [
{
"duration": "00:05:00",
"metric": "disk",
"threshold": 80.0
}
]
}
'''
__version__ = '${version}'
import json
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import APIFailedResponse
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcAlertPolicy:
clc = clc_sdk
module = None
def __init__(self, module):
"""
Construct module
"""
self.module = module
self.policy_dict = {}
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
@staticmethod
def _define_module_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
name=dict(default=None),
id=dict(default=None),
alias=dict(required=True, default=None),
alert_recipients=dict(type='list', default=None),
metric=dict(
choices=[
'cpu',
'memory',
'disk'],
default=None),
duration=dict(type='str', default=None),
threshold=dict(type='int', default=None),
state=dict(default='present', choices=['present', 'absent'])
)
mutually_exclusive = [
['name', 'id']
]
return {'argument_spec': argument_spec,
'mutually_exclusive': mutually_exclusive}
# Module Behavior Goodness
def process_request(self):
"""
Process the request - Main Code Path
:return: Returns with either an exit_json or fail_json
"""
p = self.module.params
self._set_clc_credentials_from_env()
self.policy_dict = self._get_alert_policies(p['alias'])
if p['state'] == 'present':
changed, policy = self._ensure_alert_policy_is_present()
else:
changed, policy = self._ensure_alert_policy_is_absent()
self.module.exit_json(changed=changed, policy=policy)
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
def _ensure_alert_policy_is_present(self):
"""
Ensures that the alert policy is present
:return: (changed, policy)
changed: A flag representing if anything is modified
policy: the created/updated alert policy
"""
changed = False
p = self.module.params
policy_name = p.get('name')
if not policy_name:
self.module.fail_json(msg='Policy name is a required')
policy = self._alert_policy_exists(policy_name)
if not policy:
changed = True
policy = None
if not self.module.check_mode:
policy = self._create_alert_policy()
else:
changed_u, policy = self._ensure_alert_policy_is_updated(policy)
if changed_u:
changed = True
return changed, policy
def _ensure_alert_policy_is_absent(self):
"""
Ensures that the alert policy is absent
:return: (changed, None)
changed: A flag representing if anything is modified
"""
changed = False
p = self.module.params
alert_policy_id = p.get('id')
alert_policy_name = p.get('name')
alias = p.get('alias')
if not alert_policy_id and not alert_policy_name:
self.module.fail_json(
msg='Either alert policy id or policy name is required')
if not alert_policy_id and alert_policy_name:
alert_policy_id = self._get_alert_policy_id(
self.module,
alert_policy_name)
if alert_policy_id and alert_policy_id in self.policy_dict:
changed = True
if not self.module.check_mode:
self._delete_alert_policy(alias, alert_policy_id)
return changed, None
def _ensure_alert_policy_is_updated(self, alert_policy):
"""
Ensures the alert policy is updated if anything is changed in the alert policy configuration
:param alert_policy: the target alert policy
:return: (changed, policy)
changed: A flag representing if anything is modified
policy: the updated the alert policy
"""
changed = False
p = self.module.params
alert_policy_id = alert_policy.get('id')
email_list = p.get('alert_recipients')
metric = p.get('metric')
duration = p.get('duration')
threshold = p.get('threshold')
policy = alert_policy
if (metric and metric != str(alert_policy.get('triggers')[0].get('metric'))) or \
(duration and duration != str(alert_policy.get('triggers')[0].get('duration'))) or \
(threshold and float(threshold) != float(alert_policy.get('triggers')[0].get('threshold'))):
changed = True
elif email_list:
t_email_list = list(
alert_policy.get('actions')[0].get('settings').get('recipients'))
if set(email_list) != set(t_email_list):
changed = True
if changed and not self.module.check_mode:
policy = self._update_alert_policy(alert_policy_id)
return changed, policy
def _get_alert_policies(self, alias):
"""
Get the alert policies for account alias by calling the CLC API.
:param alias: the account alias
:return: the alert policies for the account alias
"""
response = {}
policies = self.clc.v2.API.Call('GET',
'/v2/alertPolicies/%s'
% alias)
for policy in policies.get('items'):
response[policy.get('id')] = policy
return response
def _create_alert_policy(self):
"""
Create an alert Policy using the CLC API.
:return: response dictionary from the CLC API.
"""
p = self.module.params
alias = p['alias']
email_list = p['alert_recipients']
metric = p['metric']
duration = p['duration']
threshold = p['threshold']
policy_name = p['name']
arguments = json.dumps(
{
'name': policy_name,
'actions': [{
'action': 'email',
'settings': {
'recipients': email_list
}
}],
'triggers': [{
'metric': metric,
'duration': duration,
'threshold': threshold
}]
}
)
try:
result = self.clc.v2.API.Call(
'POST',
'/v2/alertPolicies/%s' % alias,
arguments)
except APIFailedResponse as e:
return self.module.fail_json(
msg='Unable to create alert policy "{0}". {1}'.format(
policy_name, str(e.response_text)))
return result
def _update_alert_policy(self, alert_policy_id):
"""
Update alert policy using the CLC API.
:param alert_policy_id: The clc alert policy id
:return: response dictionary from the CLC API.
"""
p = self.module.params
alias = p['alias']
email_list = p['alert_recipients']
metric = p['metric']
duration = p['duration']
threshold = p['threshold']
policy_name = p['name']
arguments = json.dumps(
{
'name': policy_name,
'actions': [{
'action': 'email',
'settings': {
'recipients': email_list
}
}],
'triggers': [{
'metric': metric,
'duration': duration,
'threshold': threshold
}]
}
)
try:
result = self.clc.v2.API.Call(
'PUT', '/v2/alertPolicies/%s/%s' %
(alias, alert_policy_id), arguments)
except APIFailedResponse as e:
return self.module.fail_json(
msg='Unable to update alert policy "{0}". {1}'.format(
policy_name, str(e.response_text)))
return result
def _delete_alert_policy(self, alias, policy_id):
"""
Delete an alert policy using the CLC API.
:param alias : the account alias
:param policy_id: the alert policy id
:return: response dictionary from the CLC API.
"""
try:
result = self.clc.v2.API.Call(
'DELETE', '/v2/alertPolicies/%s/%s' %
(alias, policy_id), None)
except APIFailedResponse as e:
return self.module.fail_json(
msg='Unable to delete alert policy id "{0}". {1}'.format(
policy_id, str(e.response_text)))
return result
def _alert_policy_exists(self, policy_name):
"""
Check to see if an alert policy exists
:param policy_name: name of the alert policy
:return: boolean of if the policy exists
"""
result = False
for policy_id in self.policy_dict:
if self.policy_dict.get(policy_id).get('name') == policy_name:
result = self.policy_dict.get(policy_id)
return result
def _get_alert_policy_id(self, module, alert_policy_name):
"""
retrieves the alert policy id of the account based on the name of the policy
:param module: the AnsibleModule object
:param alert_policy_name: the alert policy name
:return: alert_policy_id: The alert policy id
"""
alert_policy_id = None
for policy_id in self.policy_dict:
if self.policy_dict.get(policy_id).get('name') == alert_policy_name:
if not alert_policy_id:
alert_policy_id = policy_id
else:
return module.fail_json(
msg='multiple alert policies were found with policy name : %s' % alert_policy_name)
return alert_policy_id
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
argument_dict = ClcAlertPolicy._define_module_argument_spec()
module = AnsibleModule(supports_check_mode=True, **argument_dict)
clc_alert_policy = ClcAlertPolicy(module)
clc_alert_policy.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,300 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_blueprint_package
short_description: deploys a blue print package on a set of servers in CenturyLink Cloud.
description:
- An Ansible module to deploy blue print package on a set of servers in CenturyLink Cloud.
options:
server_ids:
description:
- A list of server Ids to deploy the blue print package.
required: True
package_id:
description:
- The package id of the blue print.
required: True
package_params:
description:
- The dictionary of arguments required to deploy the blue print.
default: {}
required: False
state:
description:
- Whether to install or uninstall the package. Currently it supports only "present" for install action.
required: False
default: present
choices: ['present']
wait:
description:
- Whether to wait for the tasks to finish before returning.
type: bool
default: True
required: False
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
- name: Deploy package
clc_blueprint_package:
server_ids:
- UC1TEST-SERVER1
- UC1TEST-SERVER2
package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a
package_params: {}
'''
RETURN = '''
server_ids:
description: The list of server ids that are changed
returned: success
type: list
sample:
[
"UC1TEST-SERVER1",
"UC1TEST-SERVER2"
]
'''
__version__ = '${version}'
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import CLCException
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcBlueprintPackage:
clc = clc_sdk
module = None
def __init__(self, module):
"""
Construct module
"""
self.module = module
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(
requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
def process_request(self):
"""
Process the request - Main Code Path
:return: Returns with either an exit_json or fail_json
"""
p = self.module.params
changed = False
changed_server_ids = []
self._set_clc_credentials_from_env()
server_ids = p['server_ids']
package_id = p['package_id']
package_params = p['package_params']
state = p['state']
if state == 'present':
changed, changed_server_ids, request_list = self.ensure_package_installed(
server_ids, package_id, package_params)
self._wait_for_requests_to_complete(request_list)
self.module.exit_json(changed=changed, server_ids=changed_server_ids)
@staticmethod
def define_argument_spec():
"""
This function defines the dictionary object required for
package module
:return: the package dictionary object
"""
argument_spec = dict(
server_ids=dict(type='list', required=True),
package_id=dict(required=True),
package_params=dict(type='dict', default={}),
wait=dict(default=True),
state=dict(default='present', choices=['present'])
)
return argument_spec
def ensure_package_installed(self, server_ids, package_id, package_params):
"""
Ensure the package is installed in the given list of servers
:param server_ids: the server list where the package needs to be installed
:param package_id: the blueprint package id
:param package_params: the package arguments
:return: (changed, server_ids, request_list)
changed: A flag indicating if a change was made
server_ids: The list of servers modified
request_list: The list of request objects from clc-sdk
"""
changed = False
request_list = []
servers = self._get_servers_from_clc(
server_ids,
'Failed to get servers from CLC')
for server in servers:
if not self.module.check_mode:
request = self.clc_install_package(
server,
package_id,
package_params)
request_list.append(request)
changed = True
return changed, server_ids, request_list
def clc_install_package(self, server, package_id, package_params):
"""
Install the package to a given clc server
:param server: The server object where the package needs to be installed
:param package_id: The blue print package id
:param package_params: the required argument dict for the package installation
:return: The result object from the CLC API call
"""
result = None
try:
result = server.ExecutePackage(
package_id=package_id,
parameters=package_params)
except CLCException as ex:
self.module.fail_json(msg='Failed to install package : {0} to server {1}. {2}'.format(
package_id, server.id, ex.message
))
return result
def _wait_for_requests_to_complete(self, request_lst):
"""
Waits until the CLC requests are complete if the wait argument is True
:param request_lst: The list of CLC request objects
:return: none
"""
if not self.module.params['wait']:
return
for request in request_lst:
request.WaitUntilComplete()
for request_details in request.requests:
if request_details.Status() != 'succeeded':
self.module.fail_json(
msg='Unable to process package install request')
def _get_servers_from_clc(self, server_list, message):
"""
Internal function to fetch list of CLC server objects from a list of server ids
:param server_list: the list of server ids
:param message: the error message to raise if there is any error
:return the list of CLC server objects
"""
try:
return self.clc.v2.Servers(server_list).servers
except CLCException as ex:
self.module.fail_json(msg=message + ': %s' % ex)
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
Main function
:return: None
"""
module = AnsibleModule(
argument_spec=ClcBlueprintPackage.define_argument_spec(),
supports_check_mode=True
)
clc_blueprint_package = ClcBlueprintPackage(module)
clc_blueprint_package.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,581 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_firewall_policy
short_description: Create/delete/update firewall policies
description:
- Create or delete or update firewall policies on Centurylink Cloud
options:
location:
description:
- Target datacenter for the firewall policy
required: True
state:
description:
- Whether to create or delete the firewall policy
default: present
choices: ['present', 'absent']
source:
description:
- The list of source addresses for traffic on the originating firewall.
This is required when state is 'present'
destination:
description:
- The list of destination addresses for traffic on the terminating firewall.
This is required when state is 'present'
ports:
description:
- The list of ports associated with the policy.
TCP and UDP can take in single ports or port ranges.
choices: ['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456']
firewall_policy_id:
description:
- Id of the firewall policy. This is required to update or delete an existing firewall policy
source_account_alias:
description:
- CLC alias for the source account
required: True
destination_account_alias:
description:
- CLC alias for the destination account
wait:
description:
- Whether to wait for the provisioning tasks to finish before returning.
type: bool
default: 'yes'
enabled:
description:
- Whether the firewall policy is enabled or disabled
choices: [True, False]
default: 'yes'
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
---
- name: Create Firewall Policy
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Create / Verify an Firewall Policy at CenturyLink Cloud
clc_firewall:
source_account_alias: WFAD
location: VA1
state: present
source: 10.128.216.0/24
destination: 10.128.216.0/24
ports: Any
destination_account_alias: WFAD
---
- name: Delete Firewall Policy
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Delete an Firewall Policy at CenturyLink Cloud
clc_firewall:
source_account_alias: WFAD
location: VA1
state: absent
firewall_policy_id: c62105233d7a4231bd2e91b9c791e43e1
'''
RETURN = '''
firewall_policy_id:
description: The fire wall policy id
returned: success
type: str
sample: fc36f1bfd47242e488a9c44346438c05
firewall_policy:
description: The fire wall policy information
returned: success
type: dict
sample:
{
"destination":[
"10.1.1.0/24",
"10.2.2.0/24"
],
"destinationAccount":"wfad",
"enabled":true,
"id":"fc36f1bfd47242e488a9c44346438c05",
"links":[
{
"href":"http://api.ctl.io/v2-experimental/firewallPolicies/wfad/uc1/fc36f1bfd47242e488a9c44346438c05",
"rel":"self",
"verbs":[
"GET",
"PUT",
"DELETE"
]
}
],
"ports":[
"any"
],
"source":[
"10.1.1.0/24",
"10.2.2.0/24"
],
"status":"active"
}
'''
__version__ = '${version}'
import os
import traceback
from ansible.module_utils.six.moves.urllib.parse import urlparse
from time import sleep
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import APIFailedResponse
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcFirewallPolicy:
clc = None
def __init__(self, module):
"""
Construct module
"""
self.clc = clc_sdk
self.module = module
self.firewall_dict = {}
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(
requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
@staticmethod
def _define_module_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
location=dict(required=True),
source_account_alias=dict(required=True, default=None),
destination_account_alias=dict(default=None),
firewall_policy_id=dict(default=None),
ports=dict(default=None, type='list'),
source=dict(default=None, type='list'),
destination=dict(default=None, type='list'),
wait=dict(default=True),
state=dict(default='present', choices=['present', 'absent']),
enabled=dict(default=True, choices=[True, False])
)
return argument_spec
def process_request(self):
"""
Execute the main code path, and handle the request
:return: none
"""
changed = False
firewall_policy = None
location = self.module.params.get('location')
source_account_alias = self.module.params.get('source_account_alias')
destination_account_alias = self.module.params.get(
'destination_account_alias')
firewall_policy_id = self.module.params.get('firewall_policy_id')
ports = self.module.params.get('ports')
source = self.module.params.get('source')
destination = self.module.params.get('destination')
wait = self.module.params.get('wait')
state = self.module.params.get('state')
enabled = self.module.params.get('enabled')
self.firewall_dict = {
'location': location,
'source_account_alias': source_account_alias,
'destination_account_alias': destination_account_alias,
'firewall_policy_id': firewall_policy_id,
'ports': ports,
'source': source,
'destination': destination,
'wait': wait,
'state': state,
'enabled': enabled}
self._set_clc_credentials_from_env()
if state == 'absent':
changed, firewall_policy_id, firewall_policy = self._ensure_firewall_policy_is_absent(
source_account_alias, location, self.firewall_dict)
elif state == 'present':
changed, firewall_policy_id, firewall_policy = self._ensure_firewall_policy_is_present(
source_account_alias, location, self.firewall_dict)
return self.module.exit_json(
changed=changed,
firewall_policy_id=firewall_policy_id,
firewall_policy=firewall_policy)
@staticmethod
def _get_policy_id_from_response(response):
"""
Method to parse out the policy id from creation response
:param response: response from firewall creation API call
:return: policy_id: firewall policy id from creation call
"""
url = response.get('links')[0]['href']
path = urlparse(url).path
path_list = os.path.split(path)
policy_id = path_list[-1]
return policy_id
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
def _ensure_firewall_policy_is_present(
self,
source_account_alias,
location,
firewall_dict):
"""
Ensures that a given firewall policy is present
:param source_account_alias: the source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_dict: dictionary of request parameters for firewall policy
:return: (changed, firewall_policy_id, firewall_policy)
changed: flag for if a change occurred
firewall_policy_id: the firewall policy id that was created/updated
firewall_policy: The firewall_policy object
"""
firewall_policy = None
firewall_policy_id = firewall_dict.get('firewall_policy_id')
if firewall_policy_id is None:
if not self.module.check_mode:
response = self._create_firewall_policy(
source_account_alias,
location,
firewall_dict)
firewall_policy_id = self._get_policy_id_from_response(
response)
changed = True
else:
firewall_policy = self._get_firewall_policy(
source_account_alias, location, firewall_policy_id)
if not firewall_policy:
return self.module.fail_json(
msg='Unable to find the firewall policy id : {0}'.format(
firewall_policy_id))
changed = self._compare_get_request_with_dict(
firewall_policy,
firewall_dict)
if not self.module.check_mode and changed:
self._update_firewall_policy(
source_account_alias,
location,
firewall_policy_id,
firewall_dict)
if changed and firewall_policy_id:
firewall_policy = self._wait_for_requests_to_complete(
source_account_alias,
location,
firewall_policy_id)
return changed, firewall_policy_id, firewall_policy
def _ensure_firewall_policy_is_absent(
self,
source_account_alias,
location,
firewall_dict):
"""
Ensures that a given firewall policy is removed if present
:param source_account_alias: the source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_dict: firewall policy to delete
:return: (changed, firewall_policy_id, response)
changed: flag for if a change occurred
firewall_policy_id: the firewall policy id that was deleted
response: response from CLC API call
"""
changed = False
response = []
firewall_policy_id = firewall_dict.get('firewall_policy_id')
result = self._get_firewall_policy(
source_account_alias, location, firewall_policy_id)
if result:
if not self.module.check_mode:
response = self._delete_firewall_policy(
source_account_alias,
location,
firewall_policy_id)
changed = True
return changed, firewall_policy_id, response
def _create_firewall_policy(
self,
source_account_alias,
location,
firewall_dict):
"""
Creates the firewall policy for the given account alias
:param source_account_alias: the source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_dict: dictionary of request parameters for firewall policy
:return: response from CLC API call
"""
payload = {
'destinationAccount': firewall_dict.get('destination_account_alias'),
'source': firewall_dict.get('source'),
'destination': firewall_dict.get('destination'),
'ports': firewall_dict.get('ports')}
try:
response = self.clc.v2.API.Call(
'POST', '/v2-experimental/firewallPolicies/%s/%s' %
(source_account_alias, location), payload)
except APIFailedResponse as e:
return self.module.fail_json(
msg="Unable to create firewall policy. %s" %
str(e.response_text))
return response
def _delete_firewall_policy(
self,
source_account_alias,
location,
firewall_policy_id):
"""
Deletes a given firewall policy for an account alias in a datacenter
:param source_account_alias: the source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_policy_id: firewall policy id to delete
:return: response: response from CLC API call
"""
try:
response = self.clc.v2.API.Call(
'DELETE', '/v2-experimental/firewallPolicies/%s/%s/%s' %
(source_account_alias, location, firewall_policy_id))
except APIFailedResponse as e:
return self.module.fail_json(
msg="Unable to delete the firewall policy id : {0}. {1}".format(
firewall_policy_id, str(e.response_text)))
return response
def _update_firewall_policy(
self,
source_account_alias,
location,
firewall_policy_id,
firewall_dict):
"""
Updates a firewall policy for a given datacenter and account alias
:param source_account_alias: the source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_policy_id: firewall policy id to update
:param firewall_dict: dictionary of request parameters for firewall policy
:return: response: response from CLC API call
"""
try:
response = self.clc.v2.API.Call(
'PUT',
'/v2-experimental/firewallPolicies/%s/%s/%s' %
(source_account_alias,
location,
firewall_policy_id),
firewall_dict)
except APIFailedResponse as e:
return self.module.fail_json(
msg="Unable to update the firewall policy id : {0}. {1}".format(
firewall_policy_id, str(e.response_text)))
return response
@staticmethod
def _compare_get_request_with_dict(response, firewall_dict):
"""
Helper method to compare the json response for getting the firewall policy with the request parameters
:param response: response from the get method
:param firewall_dict: dictionary of request parameters for firewall policy
:return: changed: Boolean that returns true if there are differences between
the response parameters and the playbook parameters
"""
changed = False
response_dest_account_alias = response.get('destinationAccount')
response_enabled = response.get('enabled')
response_source = response.get('source')
response_dest = response.get('destination')
response_ports = response.get('ports')
request_dest_account_alias = firewall_dict.get(
'destination_account_alias')
request_enabled = firewall_dict.get('enabled')
if request_enabled is None:
request_enabled = True
request_source = firewall_dict.get('source')
request_dest = firewall_dict.get('destination')
request_ports = firewall_dict.get('ports')
if (
response_dest_account_alias and str(response_dest_account_alias) != str(request_dest_account_alias)) or (
response_enabled != request_enabled) or (
response_source and response_source != request_source) or (
response_dest and response_dest != request_dest) or (
response_ports and response_ports != request_ports):
changed = True
return changed
def _get_firewall_policy(
self,
source_account_alias,
location,
firewall_policy_id):
"""
Get back details for a particular firewall policy
:param source_account_alias: the source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_policy_id: id of the firewall policy to get
:return: response - The response from CLC API call
"""
response = None
try:
response = self.clc.v2.API.Call(
'GET', '/v2-experimental/firewallPolicies/%s/%s/%s' %
(source_account_alias, location, firewall_policy_id))
except APIFailedResponse as e:
if e.response_status_code != 404:
self.module.fail_json(
msg="Unable to fetch the firewall policy with id : {0}. {1}".format(
firewall_policy_id, str(e.response_text)))
return response
def _wait_for_requests_to_complete(
self,
source_account_alias,
location,
firewall_policy_id,
wait_limit=50):
"""
Waits until the CLC requests are complete if the wait argument is True
:param source_account_alias: The source account alias for the firewall policy
:param location: datacenter of the firewall policy
:param firewall_policy_id: The firewall policy id
:param wait_limit: The number of times to check the status for completion
:return: the firewall_policy object
"""
wait = self.module.params.get('wait')
count = 0
firewall_policy = None
while wait:
count += 1
firewall_policy = self._get_firewall_policy(
source_account_alias, location, firewall_policy_id)
status = firewall_policy.get('status')
if status == 'active' or count > wait_limit:
wait = False
else:
# wait for 2 seconds
sleep(2)
return firewall_policy
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
module = AnsibleModule(
argument_spec=ClcFirewallPolicy._define_module_argument_spec(),
supports_check_mode=True)
clc_firewall = ClcFirewallPolicy(module)
clc_firewall.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,514 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_group
short_description: Create/delete Server Groups at Centurylink Cloud
description:
- Create or delete Server Groups at Centurylink Centurylink Cloud
options:
name:
description:
- The name of the Server Group
required: True
description:
description:
- A description of the Server Group
required: False
parent:
description:
- The parent group of the server group. If parent is not provided, it creates the group at top level.
required: False
location:
description:
- Datacenter to create the group in. If location is not provided, the group gets created in the default datacenter
associated with the account
required: False
state:
description:
- Whether to create or delete the group
default: present
choices: ['present', 'absent']
wait:
description:
- Whether to wait for the tasks to finish before returning.
type: bool
default: True
required: False
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Create a Server Group
---
- name: Create Server Group
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Create / Verify a Server Group at CenturyLink Cloud
clc_group:
name: My Cool Server Group
parent: Default Group
state: present
register: clc
- name: debug
debug:
var: clc
# Delete a Server Group
---
- name: Delete Server Group
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Delete / Verify Absent a Server Group at CenturyLink Cloud
clc_group:
name: My Cool Server Group
parent: Default Group
state: absent
register: clc
- name: debug
debug:
var: clc
'''
RETURN = '''
group:
description: The group information
returned: success
type: dict
sample:
{
"changeInfo":{
"createdBy":"service.wfad",
"createdDate":"2015-07-29T18:52:47Z",
"modifiedBy":"service.wfad",
"modifiedDate":"2015-07-29T18:52:47Z"
},
"customFields":[
],
"description":"test group",
"groups":[
],
"id":"bb5f12a3c6044ae4ad0a03e73ae12cd1",
"links":[
{
"href":"/v2/groups/wfad",
"rel":"createGroup",
"verbs":[
"POST"
]
},
{
"href":"/v2/servers/wfad",
"rel":"createServer",
"verbs":[
"POST"
]
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1",
"rel":"self",
"verbs":[
"GET",
"PATCH",
"DELETE"
]
},
{
"href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0",
"id":"086ac1dfe0b6411989e8d1b77c4065f0",
"rel":"parentGroup"
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/defaults",
"rel":"defaults",
"verbs":[
"GET",
"POST"
]
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/billing",
"rel":"billing"
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/archive",
"rel":"archiveGroupAction"
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/statistics",
"rel":"statistics"
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/upcomingScheduledActivities",
"rel":"upcomingScheduledActivities"
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/horizontalAutoscalePolicy",
"rel":"horizontalAutoscalePolicyMapping",
"verbs":[
"GET",
"PUT",
"DELETE"
]
},
{
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/scheduledActivities",
"rel":"scheduledActivities",
"verbs":[
"GET",
"POST"
]
}
],
"locationId":"UC1",
"name":"test group",
"status":"active",
"type":"default"
}
'''
__version__ = '${version}'
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import CLCException
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcGroup(object):
clc = None
root_group = None
def __init__(self, module):
"""
Construct module
"""
self.clc = clc_sdk
self.module = module
self.group_dict = {}
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
def process_request(self):
"""
Execute the main code path, and handle the request
:return: none
"""
location = self.module.params.get('location')
group_name = self.module.params.get('name')
parent_name = self.module.params.get('parent')
group_description = self.module.params.get('description')
state = self.module.params.get('state')
self._set_clc_credentials_from_env()
self.group_dict = self._get_group_tree_for_datacenter(
datacenter=location)
if state == "absent":
changed, group, requests = self._ensure_group_is_absent(
group_name=group_name, parent_name=parent_name)
if requests:
self._wait_for_requests_to_complete(requests)
else:
changed, group = self._ensure_group_is_present(
group_name=group_name, parent_name=parent_name, group_description=group_description)
try:
group = group.data
except AttributeError:
group = group_name
self.module.exit_json(changed=changed, group=group)
@staticmethod
def _define_module_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
name=dict(required=True),
description=dict(default=None),
parent=dict(default=None),
location=dict(default=None),
state=dict(default='present', choices=['present', 'absent']),
wait=dict(type='bool', default=True))
return argument_spec
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
def _ensure_group_is_absent(self, group_name, parent_name):
"""
Ensure that group_name is absent by deleting it if necessary
:param group_name: string - the name of the clc server group to delete
:param parent_name: string - the name of the parent group for group_name
:return: changed, group
"""
changed = False
group = []
results = []
if self._group_exists(group_name=group_name, parent_name=parent_name):
if not self.module.check_mode:
group.append(group_name)
result = self._delete_group(group_name)
results.append(result)
changed = True
return changed, group, results
def _delete_group(self, group_name):
"""
Delete the provided server group
:param group_name: string - the server group to delete
:return: none
"""
response = None
group, parent = self.group_dict.get(group_name)
try:
response = group.Delete()
except CLCException as ex:
self.module.fail_json(msg='Failed to delete group :{0}. {1}'.format(
group_name, ex.response_text
))
return response
def _ensure_group_is_present(
self,
group_name,
parent_name,
group_description):
"""
Checks to see if a server group exists, creates it if it doesn't.
:param group_name: the name of the group to validate/create
:param parent_name: the name of the parent group for group_name
:param group_description: a short description of the server group (used when creating)
:return: (changed, group) -
changed: Boolean- whether a change was made,
group: A clc group object for the group
"""
if not self.root_group:
raise AssertionError("Implementation Error: Root Group not set")
parent = parent_name if parent_name is not None else self.root_group.name
description = group_description
changed = False
group = group_name
parent_exists = self._group_exists(group_name=parent, parent_name=None)
child_exists = self._group_exists(
group_name=group_name,
parent_name=parent)
if parent_exists and child_exists:
group, parent = self.group_dict[group_name]
changed = False
elif parent_exists and not child_exists:
if not self.module.check_mode:
group = self._create_group(
group=group,
parent=parent,
description=description)
changed = True
else:
self.module.fail_json(
msg="parent group: " +
parent +
" does not exist")
return changed, group
def _create_group(self, group, parent, description):
"""
Create the provided server group
:param group: clc_sdk.Group - the group to create
:param parent: clc_sdk.Parent - the parent group for {group}
:param description: string - a text description of the group
:return: clc_sdk.Group - the created group
"""
response = None
(parent, grandparent) = self.group_dict[parent]
try:
response = parent.Create(name=group, description=description)
except CLCException as ex:
self.module.fail_json(msg='Failed to create group :{0}. {1}'.format(
group, ex.response_text))
return response
def _group_exists(self, group_name, parent_name):
"""
Check to see if a group exists
:param group_name: string - the group to check
:param parent_name: string - the parent of group_name
:return: boolean - whether the group exists
"""
result = False
if group_name in self.group_dict:
(group, parent) = self.group_dict[group_name]
if parent_name is None or parent_name == parent.name:
result = True
return result
def _get_group_tree_for_datacenter(self, datacenter=None):
"""
Walk the tree of groups for a datacenter
:param datacenter: string - the datacenter to walk (ex: 'UC1')
:return: a dictionary of groups and parents
"""
self.root_group = self.clc.v2.Datacenter(
location=datacenter).RootGroup()
return self._walk_groups_recursive(
parent_group=None,
child_group=self.root_group)
def _walk_groups_recursive(self, parent_group, child_group):
"""
Walk a parent-child tree of groups, starting with the provided child group
:param parent_group: clc_sdk.Group - the parent group to start the walk
:param child_group: clc_sdk.Group - the child group to start the walk
:return: a dictionary of groups and parents
"""
result = {str(child_group): (child_group, parent_group)}
groups = child_group.Subgroups().groups
if len(groups) > 0:
for group in groups:
if group.type != 'default':
continue
result.update(self._walk_groups_recursive(child_group, group))
return result
def _wait_for_requests_to_complete(self, requests_lst):
"""
Waits until the CLC requests are complete if the wait argument is True
:param requests_lst: The list of CLC request objects
:return: none
"""
if not self.module.params['wait']:
return
for request in requests_lst:
request.WaitUntilComplete()
for request_details in request.requests:
if request_details.Status() != 'succeeded':
self.module.fail_json(
msg='Unable to process group request')
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
module = AnsibleModule(
argument_spec=ClcGroup._define_module_argument_spec(),
supports_check_mode=True)
clc_group = ClcGroup(module)
clc_group.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,930 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
#
# 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: clc_loadbalancer
short_description: Create, Delete shared loadbalancers in CenturyLink Cloud.
description:
- An Ansible module to Create, Delete shared loadbalancers in CenturyLink Cloud.
options:
name:
description:
- The name of the loadbalancer
required: True
description:
description:
- A description for the loadbalancer
alias:
description:
- The alias of your CLC Account
required: True
location:
description:
- The location of the datacenter where the load balancer resides in
required: True
method:
description:
-The balancing method for the load balancer pool
choices: ['leastConnection', 'roundRobin']
persistence:
description:
- The persistence method for the load balancer
choices: ['standard', 'sticky']
port:
description:
- Port to configure on the public-facing side of the load balancer pool
choices: [80, 443]
nodes:
description:
- A list of nodes that needs to be added to the load balancer pool
default: []
status:
description:
- The status of the loadbalancer
default: enabled
choices: ['enabled', 'disabled']
state:
description:
- Whether to create or delete the load balancer pool
default: present
choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent']
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
- name: Create Loadbalancer
hosts: localhost
connection: local
tasks:
- name: Actually Create things
clc_loadbalancer:
name: test
description: test
alias: TEST
location: WA1
port: 443
nodes:
- ipAddress: 10.11.22.123
privatePort: 80
state: present
- name: Add node to an existing loadbalancer pool
hosts: localhost
connection: local
tasks:
- name: Actually Create things
clc_loadbalancer:
name: test
description: test
alias: TEST
location: WA1
port: 443
nodes:
- ipAddress: 10.11.22.234
privatePort: 80
state: nodes_present
- name: Remove node from an existing loadbalancer pool
hosts: localhost
connection: local
tasks:
- name: Actually Create things
clc_loadbalancer:
name: test
description: test
alias: TEST
location: WA1
port: 443
nodes:
- ipAddress: 10.11.22.234
privatePort: 80
state: nodes_absent
- name: Delete LoadbalancerPool
hosts: localhost
connection: local
tasks:
- name: Actually Delete things
clc_loadbalancer:
name: test
description: test
alias: TEST
location: WA1
port: 443
nodes:
- ipAddress: 10.11.22.123
privatePort: 80
state: port_absent
- name: Delete Loadbalancer
hosts: localhost
connection: local
tasks:
- name: Actually Delete things
clc_loadbalancer:
name: test
description: test
alias: TEST
location: WA1
port: 443
nodes:
- ipAddress: 10.11.22.123
privatePort: 80
state: absent
'''
RETURN = '''
loadbalancer:
description: The load balancer result object from CLC
returned: success
type: dict
sample:
{
"description":"test-lb",
"id":"ab5b18cb81e94ab9925b61d1ca043fb5",
"ipAddress":"66.150.174.197",
"links":[
{
"href":"/v2/sharedLoadBalancers/wfad/wa1/ab5b18cb81e94ab9925b61d1ca043fb5",
"rel":"self",
"verbs":[
"GET",
"PUT",
"DELETE"
]
},
{
"href":"/v2/sharedLoadBalancers/wfad/wa1/ab5b18cb81e94ab9925b61d1ca043fb5/pools",
"rel":"pools",
"verbs":[
"GET",
"POST"
]
}
],
"name":"test-lb",
"pools":[
],
"status":"enabled"
}
'''
__version__ = '${version}'
import json
import os
import traceback
from time import sleep
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import APIFailedResponse
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcLoadBalancer:
clc = None
def __init__(self, module):
"""
Construct module
"""
self.clc = clc_sdk
self.module = module
self.lb_dict = {}
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(
requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
def process_request(self):
"""
Execute the main code path, and handle the request
:return: none
"""
changed = False
result_lb = None
loadbalancer_name = self.module.params.get('name')
loadbalancer_alias = self.module.params.get('alias')
loadbalancer_location = self.module.params.get('location')
loadbalancer_description = self.module.params.get('description')
loadbalancer_port = self.module.params.get('port')
loadbalancer_method = self.module.params.get('method')
loadbalancer_persistence = self.module.params.get('persistence')
loadbalancer_nodes = self.module.params.get('nodes')
loadbalancer_status = self.module.params.get('status')
state = self.module.params.get('state')
if loadbalancer_description is None:
loadbalancer_description = loadbalancer_name
self._set_clc_credentials_from_env()
self.lb_dict = self._get_loadbalancer_list(
alias=loadbalancer_alias,
location=loadbalancer_location)
if state == 'present':
changed, result_lb, lb_id = self.ensure_loadbalancer_present(
name=loadbalancer_name,
alias=loadbalancer_alias,
location=loadbalancer_location,
description=loadbalancer_description,
status=loadbalancer_status)
if loadbalancer_port:
changed, result_pool, pool_id = self.ensure_loadbalancerpool_present(
lb_id=lb_id,
alias=loadbalancer_alias,
location=loadbalancer_location,
method=loadbalancer_method,
persistence=loadbalancer_persistence,
port=loadbalancer_port)
if loadbalancer_nodes:
changed, result_nodes = self.ensure_lbpool_nodes_set(
alias=loadbalancer_alias,
location=loadbalancer_location,
name=loadbalancer_name,
port=loadbalancer_port,
nodes=loadbalancer_nodes)
elif state == 'absent':
changed, result_lb = self.ensure_loadbalancer_absent(
name=loadbalancer_name,
alias=loadbalancer_alias,
location=loadbalancer_location)
elif state == 'port_absent':
changed, result_lb = self.ensure_loadbalancerpool_absent(
alias=loadbalancer_alias,
location=loadbalancer_location,
name=loadbalancer_name,
port=loadbalancer_port)
elif state == 'nodes_present':
changed, result_lb = self.ensure_lbpool_nodes_present(
alias=loadbalancer_alias,
location=loadbalancer_location,
name=loadbalancer_name,
port=loadbalancer_port,
nodes=loadbalancer_nodes)
elif state == 'nodes_absent':
changed, result_lb = self.ensure_lbpool_nodes_absent(
alias=loadbalancer_alias,
location=loadbalancer_location,
name=loadbalancer_name,
port=loadbalancer_port,
nodes=loadbalancer_nodes)
self.module.exit_json(changed=changed, loadbalancer=result_lb)
def ensure_loadbalancer_present(
self, name, alias, location, description, status):
"""
Checks to see if a load balancer exists and creates one if it does not.
:param name: Name of loadbalancer
:param alias: Alias of account
:param location: Datacenter
:param description: Description of loadbalancer
:param status: Enabled / Disabled
:return: (changed, result, lb_id)
changed: Boolean whether a change was made
result: The result object from the CLC load balancer request
lb_id: The load balancer id
"""
changed = False
result = name
lb_id = self._loadbalancer_exists(name=name)
if not lb_id:
if not self.module.check_mode:
result = self.create_loadbalancer(name=name,
alias=alias,
location=location,
description=description,
status=status)
lb_id = result.get('id')
changed = True
return changed, result, lb_id
def ensure_loadbalancerpool_present(
self, lb_id, alias, location, method, persistence, port):
"""
Checks to see if a load balancer pool exists and creates one if it does not.
:param lb_id: The loadbalancer id
:param alias: The account alias
:param location: the datacenter the load balancer resides in
:param method: the load balancing method
:param persistence: the load balancing persistence type
:param port: the port that the load balancer will listen on
:return: (changed, group, pool_id) -
changed: Boolean whether a change was made
result: The result from the CLC API call
pool_id: The string id of the load balancer pool
"""
changed = False
result = port
if not lb_id:
return changed, None, None
pool_id = self._loadbalancerpool_exists(
alias=alias,
location=location,
port=port,
lb_id=lb_id)
if not pool_id:
if not self.module.check_mode:
result = self.create_loadbalancerpool(
alias=alias,
location=location,
lb_id=lb_id,
method=method,
persistence=persistence,
port=port)
pool_id = result.get('id')
changed = True
return changed, result, pool_id
def ensure_loadbalancer_absent(self, name, alias, location):
"""
Checks to see if a load balancer exists and deletes it if it does
:param name: Name of the load balancer
:param alias: Alias of account
:param location: Datacenter
:return: (changed, result)
changed: Boolean whether a change was made
result: The result from the CLC API Call
"""
changed = False
result = name
lb_exists = self._loadbalancer_exists(name=name)
if lb_exists:
if not self.module.check_mode:
result = self.delete_loadbalancer(alias=alias,
location=location,
name=name)
changed = True
return changed, result
def ensure_loadbalancerpool_absent(self, alias, location, name, port):
"""
Checks to see if a load balancer pool exists and deletes it if it does
:param alias: The account alias
:param location: the datacenter the load balancer resides in
:param name: the name of the load balancer
:param port: the port that the load balancer listens on
:return: (changed, result) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
result = None
lb_exists = self._loadbalancer_exists(name=name)
if lb_exists:
lb_id = self._get_loadbalancer_id(name=name)
pool_id = self._loadbalancerpool_exists(
alias=alias,
location=location,
port=port,
lb_id=lb_id)
if pool_id:
changed = True
if not self.module.check_mode:
result = self.delete_loadbalancerpool(
alias=alias,
location=location,
lb_id=lb_id,
pool_id=pool_id)
else:
result = "Pool doesn't exist"
else:
result = "LB Doesn't Exist"
return changed, result
def ensure_lbpool_nodes_set(self, alias, location, name, port, nodes):
"""
Checks to see if the provided list of nodes exist for the pool
and set the nodes if any in the list those doesn't exist
:param alias: The account alias
:param location: the datacenter the load balancer resides in
:param name: the name of the load balancer
:param port: the port that the load balancer will listen on
:param nodes: The list of nodes to be updated to the pool
:return: (changed, result) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
result = {}
changed = False
lb_exists = self._loadbalancer_exists(name=name)
if lb_exists:
lb_id = self._get_loadbalancer_id(name=name)
pool_id = self._loadbalancerpool_exists(
alias=alias,
location=location,
port=port,
lb_id=lb_id)
if pool_id:
nodes_exist = self._loadbalancerpool_nodes_exists(alias=alias,
location=location,
lb_id=lb_id,
pool_id=pool_id,
nodes_to_check=nodes)
if not nodes_exist:
changed = True
result = self.set_loadbalancernodes(alias=alias,
location=location,
lb_id=lb_id,
pool_id=pool_id,
nodes=nodes)
else:
result = "Pool doesn't exist"
else:
result = "Load balancer doesn't Exist"
return changed, result
def ensure_lbpool_nodes_present(self, alias, location, name, port, nodes):
"""
Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool
:param alias: The account alias
:param location: the datacenter the load balancer resides in
:param name: the name of the load balancer
:param port: the port that the load balancer will listen on
:param nodes: the list of nodes to be added
:return: (changed, result) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
lb_exists = self._loadbalancer_exists(name=name)
if lb_exists:
lb_id = self._get_loadbalancer_id(name=name)
pool_id = self._loadbalancerpool_exists(
alias=alias,
location=location,
port=port,
lb_id=lb_id)
if pool_id:
changed, result = self.add_lbpool_nodes(alias=alias,
location=location,
lb_id=lb_id,
pool_id=pool_id,
nodes_to_add=nodes)
else:
result = "Pool doesn't exist"
else:
result = "Load balancer doesn't Exist"
return changed, result
def ensure_lbpool_nodes_absent(self, alias, location, name, port, nodes):
"""
Checks to see if the provided list of nodes exist for the pool and removes them if found any
:param alias: The account alias
:param location: the datacenter the load balancer resides in
:param name: the name of the load balancer
:param port: the port that the load balancer will listen on
:param nodes: the list of nodes to be removed
:return: (changed, result) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
lb_exists = self._loadbalancer_exists(name=name)
if lb_exists:
lb_id = self._get_loadbalancer_id(name=name)
pool_id = self._loadbalancerpool_exists(
alias=alias,
location=location,
port=port,
lb_id=lb_id)
if pool_id:
changed, result = self.remove_lbpool_nodes(alias=alias,
location=location,
lb_id=lb_id,
pool_id=pool_id,
nodes_to_remove=nodes)
else:
result = "Pool doesn't exist"
else:
result = "Load balancer doesn't Exist"
return changed, result
def create_loadbalancer(self, name, alias, location, description, status):
"""
Create a loadbalancer w/ params
:param name: Name of loadbalancer
:param alias: Alias of account
:param location: Datacenter
:param description: Description for loadbalancer to be created
:param status: Enabled / Disabled
:return: result: The result from the CLC API call
"""
result = None
try:
result = self.clc.v2.API.Call('POST',
'/v2/sharedLoadBalancers/%s/%s' % (alias,
location),
json.dumps({"name": name,
"description": description,
"status": status}))
sleep(1)
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to create load balancer "{0}". {1}'.format(
name, str(e.response_text)))
return result
def create_loadbalancerpool(
self, alias, location, lb_id, method, persistence, port):
"""
Creates a pool on the provided load balancer
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the load balancer
:param method: the load balancing method
:param persistence: the load balancing persistence type
:param port: the port that the load balancer will listen on
:return: result: The result from the create API call
"""
result = None
try:
result = self.clc.v2.API.Call(
'POST', '/v2/sharedLoadBalancers/%s/%s/%s/pools' %
(alias, location, lb_id), json.dumps(
{
"port": port, "method": method, "persistence": persistence
}))
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to create pool for load balancer id "{0}". {1}'.format(
lb_id, str(e.response_text)))
return result
def delete_loadbalancer(self, alias, location, name):
"""
Delete CLC loadbalancer
:param alias: Alias for account
:param location: Datacenter
:param name: Name of the loadbalancer to delete
:return: result: The result from the CLC API call
"""
result = None
lb_id = self._get_loadbalancer_id(name=name)
try:
result = self.clc.v2.API.Call(
'DELETE', '/v2/sharedLoadBalancers/%s/%s/%s' %
(alias, location, lb_id))
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to delete load balancer "{0}". {1}'.format(
name, str(e.response_text)))
return result
def delete_loadbalancerpool(self, alias, location, lb_id, pool_id):
"""
Delete the pool on the provided load balancer
:param alias: The account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the load balancer
:param pool_id: the id string of the load balancer pool
:return: result: The result from the delete API call
"""
result = None
try:
result = self.clc.v2.API.Call(
'DELETE', '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s' %
(alias, location, lb_id, pool_id))
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to delete pool for load balancer id "{0}". {1}'.format(
lb_id, str(e.response_text)))
return result
def _get_loadbalancer_id(self, name):
"""
Retrieves unique ID of loadbalancer
:param name: Name of loadbalancer
:return: Unique ID of the loadbalancer
"""
id = None
for lb in self.lb_dict:
if lb.get('name') == name:
id = lb.get('id')
return id
def _get_loadbalancer_list(self, alias, location):
"""
Retrieve a list of loadbalancers
:param alias: Alias for account
:param location: Datacenter
:return: JSON data for all loadbalancers at datacenter
"""
result = None
try:
result = self.clc.v2.API.Call(
'GET', '/v2/sharedLoadBalancers/%s/%s' % (alias, location))
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to fetch load balancers for account: {0}. {1}'.format(
alias, str(e.response_text)))
return result
def _loadbalancer_exists(self, name):
"""
Verify a loadbalancer exists
:param name: Name of loadbalancer
:return: False or the ID of the existing loadbalancer
"""
result = False
for lb in self.lb_dict:
if lb.get('name') == name:
result = lb.get('id')
return result
def _loadbalancerpool_exists(self, alias, location, port, lb_id):
"""
Checks to see if a pool exists on the specified port on the provided load balancer
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param port: the port to check and see if it exists
:param lb_id: the id string of the provided load balancer
:return: result: The id string of the pool or False
"""
result = False
try:
pool_list = self.clc.v2.API.Call(
'GET', '/v2/sharedLoadBalancers/%s/%s/%s/pools' %
(alias, location, lb_id))
except APIFailedResponse as e:
return self.module.fail_json(
msg='Unable to fetch the load balancer pools for for load balancer id: {0}. {1}'.format(
lb_id, str(e.response_text)))
for pool in pool_list:
if int(pool.get('port')) == int(port):
result = pool.get('id')
return result
def _loadbalancerpool_nodes_exists(
self, alias, location, lb_id, pool_id, nodes_to_check):
"""
Checks to see if a set of nodes exists on the specified port on the provided load balancer
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the provided load balancer
:param pool_id: the id string of the load balancer pool
:param nodes_to_check: the list of nodes to check for
:return: result: True / False indicating if the given nodes exist
"""
result = False
nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id)
for node in nodes_to_check:
if not node.get('status'):
node['status'] = 'enabled'
if node in nodes:
result = True
else:
result = False
return result
def set_loadbalancernodes(self, alias, location, lb_id, pool_id, nodes):
"""
Updates nodes to the provided pool
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the load balancer
:param pool_id: the id string of the pool
:param nodes: a list of dictionaries containing the nodes to set
:return: result: The result from the CLC API call
"""
result = None
if not lb_id:
return result
if not self.module.check_mode:
try:
result = self.clc.v2.API.Call('PUT',
'/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes'
% (alias, location, lb_id, pool_id), json.dumps(nodes))
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to set nodes for the load balancer pool id "{0}". {1}'.format(
pool_id, str(e.response_text)))
return result
def add_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_add):
"""
Add nodes to the provided pool
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the load balancer
:param pool_id: the id string of the pool
:param nodes_to_add: a list of dictionaries containing the nodes to add
:return: (changed, result) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
result = {}
nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id)
for node in nodes_to_add:
if not node.get('status'):
node['status'] = 'enabled'
if node not in nodes:
changed = True
nodes.append(node)
if changed is True and not self.module.check_mode:
result = self.set_loadbalancernodes(
alias,
location,
lb_id,
pool_id,
nodes)
return changed, result
def remove_lbpool_nodes(
self, alias, location, lb_id, pool_id, nodes_to_remove):
"""
Removes nodes from the provided pool
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the load balancer
:param pool_id: the id string of the pool
:param nodes_to_remove: a list of dictionaries containing the nodes to remove
:return: (changed, result) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
result = {}
nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id)
for node in nodes_to_remove:
if not node.get('status'):
node['status'] = 'enabled'
if node in nodes:
changed = True
nodes.remove(node)
if changed is True and not self.module.check_mode:
result = self.set_loadbalancernodes(
alias,
location,
lb_id,
pool_id,
nodes)
return changed, result
def _get_lbpool_nodes(self, alias, location, lb_id, pool_id):
"""
Return the list of nodes available to the provided load balancer pool
:param alias: the account alias
:param location: the datacenter the load balancer resides in
:param lb_id: the id string of the load balancer
:param pool_id: the id string of the pool
:return: result: The list of nodes
"""
result = None
try:
result = self.clc.v2.API.Call('GET',
'/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes'
% (alias, location, lb_id, pool_id))
except APIFailedResponse as e:
self.module.fail_json(
msg='Unable to fetch list of available nodes for load balancer pool id: {0}. {1}'.format(
pool_id, str(e.response_text)))
return result
@staticmethod
def define_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
name=dict(required=True),
description=dict(default=None),
location=dict(required=True),
alias=dict(required=True),
port=dict(choices=[80, 443]),
method=dict(choices=['leastConnection', 'roundRobin']),
persistence=dict(choices=['standard', 'sticky']),
nodes=dict(type='list', default=[]),
status=dict(default='enabled', choices=['enabled', 'disabled']),
state=dict(
default='present',
choices=[
'present',
'absent',
'port_absent',
'nodes_present',
'nodes_absent'])
)
return argument_spec
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
module = AnsibleModule(argument_spec=ClcLoadBalancer.define_argument_spec(),
supports_check_mode=True)
clc_loadbalancer = ClcLoadBalancer(module)
clc_loadbalancer.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,962 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_modify_server
short_description: modify servers in CenturyLink Cloud.
description:
- An Ansible module to modify servers in CenturyLink Cloud.
options:
server_ids:
description:
- A list of server Ids to modify.
required: True
cpu:
description:
- How many CPUs to update on the server
memory:
description:
- Memory (in GB) to set to the server.
anti_affinity_policy_id:
description:
- The anti affinity policy id to be set for a hyper scale server.
This is mutually exclusive with 'anti_affinity_policy_name'
anti_affinity_policy_name:
description:
- The anti affinity policy name to be set for a hyper scale server.
This is mutually exclusive with 'anti_affinity_policy_id'
alert_policy_id:
description:
- The alert policy id to be associated to the server.
This is mutually exclusive with 'alert_policy_name'
alert_policy_name:
description:
- The alert policy name to be associated to the server.
This is mutually exclusive with 'alert_policy_id'
state:
description:
- The state to insure that the provided resources are in.
default: 'present'
choices: ['present', 'absent']
wait:
description:
- Whether to wait for the provisioning tasks to finish before returning.
type: bool
default: 'yes'
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
- name: set the cpu count to 4 on a server
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
cpu: 4
state: present
- name: set the memory to 8GB on a server
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
memory: 8
state: present
- name: set the anti affinity policy on a server
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
anti_affinity_policy_name: 'aa_policy'
state: present
- name: remove the anti affinity policy on a server
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
anti_affinity_policy_name: 'aa_policy'
state: absent
- name: add the alert policy on a server
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
alert_policy_name: 'alert_policy'
state: present
- name: remove the alert policy on a server
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
alert_policy_name: 'alert_policy'
state: absent
- name: set the memory to 16GB and cpu to 8 core on a lust if servers
clc_modify_server:
server_ids:
- UC1TESTSVR01
- UC1TESTSVR02
cpu: 8
memory: 16
state: present
'''
RETURN = '''
server_ids:
description: The list of server ids that are changed
returned: success
type: list
sample:
[
"UC1TEST-SVR01",
"UC1TEST-SVR02"
]
servers:
description: The list of server objects that are changed
returned: success
type: list
sample:
[
{
"changeInfo":{
"createdBy":"service.wfad",
"createdDate":1438196820,
"modifiedBy":"service.wfad",
"modifiedDate":1438196820
},
"description":"test-server",
"details":{
"alertPolicies":[
],
"cpu":1,
"customFields":[
],
"diskCount":3,
"disks":[
{
"id":"0:0",
"partitionPaths":[
],
"sizeGB":1
},
{
"id":"0:1",
"partitionPaths":[
],
"sizeGB":2
},
{
"id":"0:2",
"partitionPaths":[
],
"sizeGB":14
}
],
"hostName":"",
"inMaintenanceMode":false,
"ipAddresses":[
{
"internal":"10.1.1.1"
}
],
"memoryGB":1,
"memoryMB":1024,
"partitions":[
],
"powerState":"started",
"snapshots":[
],
"storageGB":17
},
"groupId":"086ac1dfe0b6411989e8d1b77c4065f0",
"id":"test-server",
"ipaddress":"10.120.45.23",
"isTemplate":false,
"links":[
{
"href":"/v2/servers/wfad/test-server",
"id":"test-server",
"rel":"self",
"verbs":[
"GET",
"PATCH",
"DELETE"
]
},
{
"href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0",
"id":"086ac1dfe0b6411989e8d1b77c4065f0",
"rel":"group"
},
{
"href":"/v2/accounts/wfad",
"id":"wfad",
"rel":"account"
},
{
"href":"/v2/billing/wfad/serverPricing/test-server",
"rel":"billing"
},
{
"href":"/v2/servers/wfad/test-server/publicIPAddresses",
"rel":"publicIPAddresses",
"verbs":[
"POST"
]
},
{
"href":"/v2/servers/wfad/test-server/credentials",
"rel":"credentials"
},
{
"href":"/v2/servers/wfad/test-server/statistics",
"rel":"statistics"
},
{
"href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/upcomingScheduledActivities",
"rel":"upcomingScheduledActivities"
},
{
"href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/scheduledActivities",
"rel":"scheduledActivities",
"verbs":[
"GET",
"POST"
]
},
{
"href":"/v2/servers/wfad/test-server/capabilities",
"rel":"capabilities"
},
{
"href":"/v2/servers/wfad/test-server/alertPolicies",
"rel":"alertPolicyMappings",
"verbs":[
"POST"
]
},
{
"href":"/v2/servers/wfad/test-server/antiAffinityPolicy",
"rel":"antiAffinityPolicyMapping",
"verbs":[
"PUT",
"DELETE"
]
},
{
"href":"/v2/servers/wfad/test-server/cpuAutoscalePolicy",
"rel":"cpuAutoscalePolicyMapping",
"verbs":[
"PUT",
"DELETE"
]
}
],
"locationId":"UC1",
"name":"test-server",
"os":"ubuntu14_64Bit",
"osType":"Ubuntu 14 64-bit",
"status":"active",
"storageType":"standard",
"type":"standard"
}
]
'''
__version__ = '${version}'
import json
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import CLCException
from clc import APIFailedResponse
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcModifyServer:
clc = clc_sdk
def __init__(self, module):
"""
Construct module
"""
self.clc = clc_sdk
self.module = module
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(
requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
def process_request(self):
"""
Process the request - Main Code Path
:return: Returns with either an exit_json or fail_json
"""
self._set_clc_credentials_from_env()
p = self.module.params
cpu = p.get('cpu')
memory = p.get('memory')
state = p.get('state')
if state == 'absent' and (cpu or memory):
return self.module.fail_json(
msg='\'absent\' state is not supported for \'cpu\' and \'memory\' arguments')
server_ids = p['server_ids']
if not isinstance(server_ids, list):
return self.module.fail_json(
msg='server_ids needs to be a list of instances to modify: %s' %
server_ids)
(changed, server_dict_array, changed_server_ids) = self._modify_servers(
server_ids=server_ids)
self.module.exit_json(
changed=changed,
server_ids=changed_server_ids,
servers=server_dict_array)
@staticmethod
def _define_module_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
server_ids=dict(type='list', required=True),
state=dict(default='present', choices=['present', 'absent']),
cpu=dict(),
memory=dict(),
anti_affinity_policy_id=dict(),
anti_affinity_policy_name=dict(),
alert_policy_id=dict(),
alert_policy_name=dict(),
wait=dict(type='bool', default=True)
)
mutually_exclusive = [
['anti_affinity_policy_id', 'anti_affinity_policy_name'],
['alert_policy_id', 'alert_policy_name']
]
return {"argument_spec": argument_spec,
"mutually_exclusive": mutually_exclusive}
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
def _get_servers_from_clc(self, server_list, message):
"""
Internal function to fetch list of CLC server objects from a list of server ids
:param server_list: The list of server ids
:param message: the error message to throw in case of any error
:return the list of CLC server objects
"""
try:
return self.clc.v2.Servers(server_list).servers
except CLCException as ex:
return self.module.fail_json(msg=message + ': %s' % ex.message)
def _modify_servers(self, server_ids):
"""
modify the servers configuration on the provided list
:param server_ids: list of servers to modify
:return: a list of dictionaries with server information about the servers that were modified
"""
p = self.module.params
state = p.get('state')
server_params = {
'cpu': p.get('cpu'),
'memory': p.get('memory'),
'anti_affinity_policy_id': p.get('anti_affinity_policy_id'),
'anti_affinity_policy_name': p.get('anti_affinity_policy_name'),
'alert_policy_id': p.get('alert_policy_id'),
'alert_policy_name': p.get('alert_policy_name'),
}
changed = False
server_changed = False
aa_changed = False
ap_changed = False
server_dict_array = []
result_server_ids = []
request_list = []
changed_servers = []
if not isinstance(server_ids, list) or len(server_ids) < 1:
return self.module.fail_json(
msg='server_ids should be a list of servers, aborting')
servers = self._get_servers_from_clc(
server_ids,
'Failed to obtain server list from the CLC API')
for server in servers:
if state == 'present':
server_changed, server_result = self._ensure_server_config(
server, server_params)
if server_result:
request_list.append(server_result)
aa_changed = self._ensure_aa_policy_present(
server,
server_params)
ap_changed = self._ensure_alert_policy_present(
server,
server_params)
elif state == 'absent':
aa_changed = self._ensure_aa_policy_absent(
server,
server_params)
ap_changed = self._ensure_alert_policy_absent(
server,
server_params)
if server_changed or aa_changed or ap_changed:
changed_servers.append(server)
changed = True
self._wait_for_requests(self.module, request_list)
self._refresh_servers(self.module, changed_servers)
for server in changed_servers:
server_dict_array.append(server.data)
result_server_ids.append(server.id)
return changed, server_dict_array, result_server_ids
def _ensure_server_config(
self, server, server_params):
"""
ensures the server is updated with the provided cpu and memory
:param server: the CLC server object
:param server_params: the dictionary of server parameters
:return: (changed, group) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
cpu = server_params.get('cpu')
memory = server_params.get('memory')
changed = False
result = None
if not cpu:
cpu = server.cpu
if not memory:
memory = server.memory
if memory != server.memory or cpu != server.cpu:
if not self.module.check_mode:
result = self._modify_clc_server(
self.clc,
self.module,
server.id,
cpu,
memory)
changed = True
return changed, result
@staticmethod
def _modify_clc_server(clc, module, server_id, cpu, memory):
"""
Modify the memory or CPU of a clc server.
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param server_id: id of the server to modify
:param cpu: the new cpu value
:param memory: the new memory value
:return: the result of CLC API call
"""
result = None
acct_alias = clc.v2.Account.GetAlias()
try:
# Update the server configuration
job_obj = clc.v2.API.Call('PATCH',
'servers/%s/%s' % (acct_alias,
server_id),
json.dumps([{"op": "set",
"member": "memory",
"value": memory},
{"op": "set",
"member": "cpu",
"value": cpu}]))
result = clc.v2.Requests(job_obj)
except APIFailedResponse as ex:
module.fail_json(
msg='Unable to update the server configuration for server : "{0}". {1}'.format(
server_id, str(ex.response_text)))
return result
@staticmethod
def _wait_for_requests(module, request_list):
"""
Block until server provisioning requests are completed.
:param module: the AnsibleModule object
:param request_list: a list of clc-sdk.Request instances
:return: none
"""
wait = module.params.get('wait')
if wait:
# Requests.WaitUntilComplete() returns the count of failed requests
failed_requests_count = sum(
[request.WaitUntilComplete() for request in request_list])
if failed_requests_count > 0:
module.fail_json(
msg='Unable to process modify server request')
@staticmethod
def _refresh_servers(module, servers):
"""
Loop through a list of servers and refresh them.
:param module: the AnsibleModule object
:param servers: list of clc-sdk.Server instances to refresh
:return: none
"""
for server in servers:
try:
server.Refresh()
except CLCException as ex:
module.fail_json(msg='Unable to refresh the server {0}. {1}'.format(
server.id, ex.message
))
def _ensure_aa_policy_present(
self, server, server_params):
"""
ensures the server is updated with the provided anti affinity policy
:param server: the CLC server object
:param server_params: the dictionary of server parameters
:return: (changed, group) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
acct_alias = self.clc.v2.Account.GetAlias()
aa_policy_id = server_params.get('anti_affinity_policy_id')
aa_policy_name = server_params.get('anti_affinity_policy_name')
if not aa_policy_id and aa_policy_name:
aa_policy_id = self._get_aa_policy_id_by_name(
self.clc,
self.module,
acct_alias,
aa_policy_name)
current_aa_policy_id = self._get_aa_policy_id_of_server(
self.clc,
self.module,
acct_alias,
server.id)
if aa_policy_id and aa_policy_id != current_aa_policy_id:
self._modify_aa_policy(
self.clc,
self.module,
acct_alias,
server.id,
aa_policy_id)
changed = True
return changed
def _ensure_aa_policy_absent(
self, server, server_params):
"""
ensures the provided anti affinity policy is removed from the server
:param server: the CLC server object
:param server_params: the dictionary of server parameters
:return: (changed, group) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
acct_alias = self.clc.v2.Account.GetAlias()
aa_policy_id = server_params.get('anti_affinity_policy_id')
aa_policy_name = server_params.get('anti_affinity_policy_name')
if not aa_policy_id and aa_policy_name:
aa_policy_id = self._get_aa_policy_id_by_name(
self.clc,
self.module,
acct_alias,
aa_policy_name)
current_aa_policy_id = self._get_aa_policy_id_of_server(
self.clc,
self.module,
acct_alias,
server.id)
if aa_policy_id and aa_policy_id == current_aa_policy_id:
self._delete_aa_policy(
self.clc,
self.module,
acct_alias,
server.id)
changed = True
return changed
@staticmethod
def _modify_aa_policy(clc, module, acct_alias, server_id, aa_policy_id):
"""
modifies the anti affinity policy of the CLC server
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param acct_alias: the CLC account alias
:param server_id: the CLC server id
:param aa_policy_id: the anti affinity policy id
:return: result: The result from the CLC API call
"""
result = None
if not module.check_mode:
try:
result = clc.v2.API.Call('PUT',
'servers/%s/%s/antiAffinityPolicy' % (
acct_alias,
server_id),
json.dumps({"id": aa_policy_id}))
except APIFailedResponse as ex:
module.fail_json(
msg='Unable to modify anti affinity policy to server : "{0}". {1}'.format(
server_id, str(ex.response_text)))
return result
@staticmethod
def _delete_aa_policy(clc, module, acct_alias, server_id):
"""
Delete the anti affinity policy of the CLC server
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param acct_alias: the CLC account alias
:param server_id: the CLC server id
:return: result: The result from the CLC API call
"""
result = None
if not module.check_mode:
try:
result = clc.v2.API.Call('DELETE',
'servers/%s/%s/antiAffinityPolicy' % (
acct_alias,
server_id),
json.dumps({}))
except APIFailedResponse as ex:
module.fail_json(
msg='Unable to delete anti affinity policy to server : "{0}". {1}'.format(
server_id, str(ex.response_text)))
return result
@staticmethod
def _get_aa_policy_id_by_name(clc, module, alias, aa_policy_name):
"""
retrieves the anti affinity policy id of the server based on the name of the policy
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param alias: the CLC account alias
:param aa_policy_name: the anti affinity policy name
:return: aa_policy_id: The anti affinity policy id
"""
aa_policy_id = None
try:
aa_policies = clc.v2.API.Call(method='GET',
url='antiAffinityPolicies/%s' % alias)
except APIFailedResponse as ex:
return module.fail_json(
msg='Unable to fetch anti affinity policies from account alias : "{0}". {1}'.format(
alias, str(ex.response_text)))
for aa_policy in aa_policies.get('items'):
if aa_policy.get('name') == aa_policy_name:
if not aa_policy_id:
aa_policy_id = aa_policy.get('id')
else:
return module.fail_json(
msg='multiple anti affinity policies were found with policy name : %s' % aa_policy_name)
if not aa_policy_id:
module.fail_json(
msg='No anti affinity policy was found with policy name : %s' % aa_policy_name)
return aa_policy_id
@staticmethod
def _get_aa_policy_id_of_server(clc, module, alias, server_id):
"""
retrieves the anti affinity policy id of the server based on the CLC server id
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param alias: the CLC account alias
:param server_id: the CLC server id
:return: aa_policy_id: The anti affinity policy id
"""
aa_policy_id = None
try:
result = clc.v2.API.Call(
method='GET', url='servers/%s/%s/antiAffinityPolicy' %
(alias, server_id))
aa_policy_id = result.get('id')
except APIFailedResponse as ex:
if ex.response_status_code != 404:
module.fail_json(msg='Unable to fetch anti affinity policy for server "{0}". {1}'.format(
server_id, str(ex.response_text)))
return aa_policy_id
def _ensure_alert_policy_present(
self, server, server_params):
"""
ensures the server is updated with the provided alert policy
:param server: the CLC server object
:param server_params: the dictionary of server parameters
:return: (changed, group) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
acct_alias = self.clc.v2.Account.GetAlias()
alert_policy_id = server_params.get('alert_policy_id')
alert_policy_name = server_params.get('alert_policy_name')
if not alert_policy_id and alert_policy_name:
alert_policy_id = self._get_alert_policy_id_by_name(
self.clc,
self.module,
acct_alias,
alert_policy_name)
if alert_policy_id and not self._alert_policy_exists(
server, alert_policy_id):
self._add_alert_policy_to_server(
self.clc,
self.module,
acct_alias,
server.id,
alert_policy_id)
changed = True
return changed
def _ensure_alert_policy_absent(
self, server, server_params):
"""
ensures the alert policy is removed from the server
:param server: the CLC server object
:param server_params: the dictionary of server parameters
:return: (changed, group) -
changed: Boolean whether a change was made
result: The result from the CLC API call
"""
changed = False
acct_alias = self.clc.v2.Account.GetAlias()
alert_policy_id = server_params.get('alert_policy_id')
alert_policy_name = server_params.get('alert_policy_name')
if not alert_policy_id and alert_policy_name:
alert_policy_id = self._get_alert_policy_id_by_name(
self.clc,
self.module,
acct_alias,
alert_policy_name)
if alert_policy_id and self._alert_policy_exists(
server, alert_policy_id):
self._remove_alert_policy_to_server(
self.clc,
self.module,
acct_alias,
server.id,
alert_policy_id)
changed = True
return changed
@staticmethod
def _add_alert_policy_to_server(
clc, module, acct_alias, server_id, alert_policy_id):
"""
add the alert policy to CLC server
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param acct_alias: the CLC account alias
:param server_id: the CLC server id
:param alert_policy_id: the alert policy id
:return: result: The result from the CLC API call
"""
result = None
if not module.check_mode:
try:
result = clc.v2.API.Call('POST',
'servers/%s/%s/alertPolicies' % (
acct_alias,
server_id),
json.dumps({"id": alert_policy_id}))
except APIFailedResponse as ex:
module.fail_json(msg='Unable to set alert policy to the server : "{0}". {1}'.format(
server_id, str(ex.response_text)))
return result
@staticmethod
def _remove_alert_policy_to_server(
clc, module, acct_alias, server_id, alert_policy_id):
"""
remove the alert policy to the CLC server
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param acct_alias: the CLC account alias
:param server_id: the CLC server id
:param alert_policy_id: the alert policy id
:return: result: The result from the CLC API call
"""
result = None
if not module.check_mode:
try:
result = clc.v2.API.Call('DELETE',
'servers/%s/%s/alertPolicies/%s'
% (acct_alias, server_id, alert_policy_id))
except APIFailedResponse as ex:
module.fail_json(msg='Unable to remove alert policy from the server : "{0}". {1}'.format(
server_id, str(ex.response_text)))
return result
@staticmethod
def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name):
"""
retrieves the alert policy id of the server based on the name of the policy
:param clc: the clc-sdk instance to use
:param module: the AnsibleModule object
:param alias: the CLC account alias
:param alert_policy_name: the alert policy name
:return: alert_policy_id: The alert policy id
"""
alert_policy_id = None
try:
alert_policies = clc.v2.API.Call(method='GET',
url='alertPolicies/%s' % alias)
except APIFailedResponse as ex:
return module.fail_json(msg='Unable to fetch alert policies for account : "{0}". {1}'.format(
alias, str(ex.response_text)))
for alert_policy in alert_policies.get('items'):
if alert_policy.get('name') == alert_policy_name:
if not alert_policy_id:
alert_policy_id = alert_policy.get('id')
else:
return module.fail_json(
msg='multiple alert policies were found with policy name : %s' % alert_policy_name)
return alert_policy_id
@staticmethod
def _alert_policy_exists(server, alert_policy_id):
"""
Checks if the alert policy exists for the server
:param server: the clc server object
:param alert_policy_id: the alert policy
:return: True: if the given alert policy id associated to the server, False otherwise
"""
result = False
alert_policies = server.alertPolicies
if alert_policies:
for alert_policy in alert_policies:
if alert_policy.get('id') == alert_policy_id:
result = True
return result
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
argument_dict = ClcModifyServer._define_module_argument_spec()
module = AnsibleModule(supports_check_mode=True, **argument_dict)
clc_modify_server = ClcModifyServer(module)
clc_modify_server.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,358 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_publicip
short_description: Add and Delete public ips on servers in CenturyLink Cloud.
description:
- An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud.
options:
protocol:
description:
- The protocol that the public IP will listen for.
default: TCP
choices: ['TCP', 'UDP', 'ICMP']
ports:
description:
- A list of ports to expose. This is required when state is 'present'
server_ids:
description:
- A list of servers to create public ips on.
required: True
state:
description:
- Determine whether to create or delete public IPs. If present module will not create a second public ip if one
already exists.
default: present
choices: ['present', 'absent']
wait:
description:
- Whether to wait for the tasks to finish before returning.
type: bool
default: 'yes'
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
- name: Add Public IP to Server
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Create Public IP For Servers
clc_publicip:
protocol: TCP
ports:
- 80
server_ids:
- UC1TEST-SVR01
- UC1TEST-SVR02
state: present
register: clc
- name: debug
debug:
var: clc
- name: Delete Public IP from Server
hosts: localhost
gather_facts: False
connection: local
tasks:
- name: Create Public IP For Servers
clc_publicip:
server_ids:
- UC1TEST-SVR01
- UC1TEST-SVR02
state: absent
register: clc
- name: debug
debug:
var: clc
'''
RETURN = '''
server_ids:
description: The list of server ids that are changed
returned: success
type: list
sample:
[
"UC1TEST-SVR01",
"UC1TEST-SVR02"
]
'''
__version__ = '${version}'
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import CLCException
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcPublicIp(object):
clc = clc_sdk
module = None
def __init__(self, module):
"""
Construct module
"""
self.module = module
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
def process_request(self):
"""
Process the request - Main Code Path
:return: Returns with either an exit_json or fail_json
"""
self._set_clc_credentials_from_env()
params = self.module.params
server_ids = params['server_ids']
ports = params['ports']
protocol = params['protocol']
state = params['state']
if state == 'present':
changed, changed_server_ids, requests = self.ensure_public_ip_present(
server_ids=server_ids, protocol=protocol, ports=ports)
elif state == 'absent':
changed, changed_server_ids, requests = self.ensure_public_ip_absent(
server_ids=server_ids)
else:
return self.module.fail_json(msg="Unknown State: " + state)
self._wait_for_requests_to_complete(requests)
return self.module.exit_json(changed=changed,
server_ids=changed_server_ids)
@staticmethod
def _define_module_argument_spec():
"""
Define the argument spec for the ansible module
:return: argument spec dictionary
"""
argument_spec = dict(
server_ids=dict(type='list', required=True),
protocol=dict(default='TCP', choices=['TCP', 'UDP', 'ICMP']),
ports=dict(type='list'),
wait=dict(type='bool', default=True),
state=dict(default='present', choices=['present', 'absent']),
)
return argument_spec
def ensure_public_ip_present(self, server_ids, protocol, ports):
"""
Ensures the given server ids having the public ip available
:param server_ids: the list of server ids
:param protocol: the ip protocol
:param ports: the list of ports to expose
:return: (changed, changed_server_ids, results)
changed: A flag indicating if there is any change
changed_server_ids : the list of server ids that are changed
results: The result list from clc public ip call
"""
changed = False
results = []
changed_server_ids = []
servers = self._get_servers_from_clc(
server_ids,
'Failed to obtain server list from the CLC API')
servers_to_change = [
server for server in servers if len(
server.PublicIPs().public_ips) == 0]
ports_to_expose = [{'protocol': protocol, 'port': port}
for port in ports]
for server in servers_to_change:
if not self.module.check_mode:
result = self._add_publicip_to_server(server, ports_to_expose)
results.append(result)
changed_server_ids.append(server.id)
changed = True
return changed, changed_server_ids, results
def _add_publicip_to_server(self, server, ports_to_expose):
result = None
try:
result = server.PublicIPs().Add(ports_to_expose)
except CLCException as ex:
self.module.fail_json(msg='Failed to add public ip to the server : {0}. {1}'.format(
server.id, ex.response_text
))
return result
def ensure_public_ip_absent(self, server_ids):
"""
Ensures the given server ids having the public ip removed if there is any
:param server_ids: the list of server ids
:return: (changed, changed_server_ids, results)
changed: A flag indicating if there is any change
changed_server_ids : the list of server ids that are changed
results: The result list from clc public ip call
"""
changed = False
results = []
changed_server_ids = []
servers = self._get_servers_from_clc(
server_ids,
'Failed to obtain server list from the CLC API')
servers_to_change = [
server for server in servers if len(
server.PublicIPs().public_ips) > 0]
for server in servers_to_change:
if not self.module.check_mode:
result = self._remove_publicip_from_server(server)
results.append(result)
changed_server_ids.append(server.id)
changed = True
return changed, changed_server_ids, results
def _remove_publicip_from_server(self, server):
result = None
try:
for ip_address in server.PublicIPs().public_ips:
result = ip_address.Delete()
except CLCException as ex:
self.module.fail_json(msg='Failed to remove public ip from the server : {0}. {1}'.format(
server.id, ex.response_text
))
return result
def _wait_for_requests_to_complete(self, requests_lst):
"""
Waits until the CLC requests are complete if the wait argument is True
:param requests_lst: The list of CLC request objects
:return: none
"""
if not self.module.params['wait']:
return
for request in requests_lst:
request.WaitUntilComplete()
for request_details in request.requests:
if request_details.Status() != 'succeeded':
self.module.fail_json(
msg='Unable to process public ip request')
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
def _get_servers_from_clc(self, server_ids, message):
"""
Gets list of servers form CLC api
"""
try:
return self.clc.v2.Servers(server_ids).servers
except CLCException as exception:
self.module.fail_json(msg=message + ': %s' % exception)
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
The main function. Instantiates the module and calls process_request.
:return: none
"""
module = AnsibleModule(
argument_spec=ClcPublicIp._define_module_argument_spec(),
supports_check_mode=True
)
clc_public_ip = ClcPublicIp(module)
clc_public_ip.process_request()
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,411 @@
#!/usr/bin/python
#
# Copyright (c) 2015 CenturyLink
# 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: clc_server_snapshot
short_description: Create, Delete and Restore server snapshots in CenturyLink Cloud.
description:
- An Ansible module to Create, Delete and Restore server snapshots in CenturyLink Cloud.
options:
server_ids:
description:
- The list of CLC server Ids.
required: True
expiration_days:
description:
- The number of days to keep the server snapshot before it expires.
default: 7
required: False
state:
description:
- The state to insure that the provided resources are in.
default: 'present'
required: False
choices: ['present', 'absent', 'restore']
wait:
description:
- Whether to wait for the provisioning tasks to finish before returning.
default: True
required: False
type: bool
requirements:
- python = 2.7
- requests >= 2.5.0
- clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
- To use this module, it is required to set the below environment variables which enables access to the
Centurylink Cloud
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''
EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples
- name: Create server snapshot
clc_server_snapshot:
server_ids:
- UC1TEST-SVR01
- UC1TEST-SVR02
expiration_days: 10
wait: True
state: present
- name: Restore server snapshot
clc_server_snapshot:
server_ids:
- UC1TEST-SVR01
- UC1TEST-SVR02
wait: True
state: restore
- name: Delete server snapshot
clc_server_snapshot:
server_ids:
- UC1TEST-SVR01
- UC1TEST-SVR02
wait: True
state: absent
'''
RETURN = '''
server_ids:
description: The list of server ids that are changed
returned: success
type: list
sample:
[
"UC1TEST-SVR01",
"UC1TEST-SVR02"
]
'''
__version__ = '${version}'
import os
import traceback
from distutils.version import LooseVersion
REQUESTS_IMP_ERR = None
try:
import requests
except ImportError:
REQUESTS_IMP_ERR = traceback.format_exc()
REQUESTS_FOUND = False
else:
REQUESTS_FOUND = True
#
# Requires the clc-python-sdk.
# sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
import clc as clc_sdk
from clc import CLCException
except ImportError:
CLC_IMP_ERR = traceback.format_exc()
CLC_FOUND = False
clc_sdk = None
else:
CLC_FOUND = True
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
class ClcSnapshot:
clc = clc_sdk
module = None
def __init__(self, module):
"""
Construct module
"""
self.module = module
if not CLC_FOUND:
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
if not REQUESTS_FOUND:
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
if requests.__version__ and LooseVersion(
requests.__version__) < LooseVersion('2.5.0'):
self.module.fail_json(
msg='requests library version should be >= 2.5.0')
self._set_user_agent(self.clc)
def process_request(self):
"""
Process the request - Main Code Path
:return: Returns with either an exit_json or fail_json
"""
p = self.module.params
server_ids = p['server_ids']
expiration_days = p['expiration_days']
state = p['state']
request_list = []
changed = False
changed_servers = []
self._set_clc_credentials_from_env()
if state == 'present':
changed, request_list, changed_servers = self.ensure_server_snapshot_present(
server_ids=server_ids,
expiration_days=expiration_days)
elif state == 'absent':
changed, request_list, changed_servers = self.ensure_server_snapshot_absent(
server_ids=server_ids)
elif state == 'restore':
changed, request_list, changed_servers = self.ensure_server_snapshot_restore(
server_ids=server_ids)
self._wait_for_requests_to_complete(request_list)
return self.module.exit_json(
changed=changed,
server_ids=changed_servers)
def ensure_server_snapshot_present(self, server_ids, expiration_days):
"""
Ensures the given set of server_ids have the snapshots created
:param server_ids: The list of server_ids to create the snapshot
:param expiration_days: The number of days to keep the snapshot
:return: (changed, request_list, changed_servers)
changed: A flag indicating whether any change was made
request_list: the list of clc request objects from CLC API call
changed_servers: The list of servers ids that are modified
"""
request_list = []
changed = False
servers = self._get_servers_from_clc(
server_ids,
'Failed to obtain server list from the CLC API')
servers_to_change = [
server for server in servers if len(
server.GetSnapshots()) == 0]
for server in servers_to_change:
changed = True
if not self.module.check_mode:
request = self._create_server_snapshot(server, expiration_days)
request_list.append(request)
changed_servers = [
server.id for server in servers_to_change if server.id]
return changed, request_list, changed_servers
def _create_server_snapshot(self, server, expiration_days):
"""
Create the snapshot for the CLC server
:param server: the CLC server object
:param expiration_days: The number of days to keep the snapshot
:return: the create request object from CLC API Call
"""
result = None
try:
result = server.CreateSnapshot(
delete_existing=True,
expiration_days=expiration_days)
except CLCException as ex:
self.module.fail_json(msg='Failed to create snapshot for server : {0}. {1}'.format(
server.id, ex.response_text
))
return result
def ensure_server_snapshot_absent(self, server_ids):
"""
Ensures the given set of server_ids have the snapshots removed
:param server_ids: The list of server_ids to delete the snapshot
:return: (changed, request_list, changed_servers)
changed: A flag indicating whether any change was made
request_list: the list of clc request objects from CLC API call
changed_servers: The list of servers ids that are modified
"""
request_list = []
changed = False
servers = self._get_servers_from_clc(
server_ids,
'Failed to obtain server list from the CLC API')
servers_to_change = [
server for server in servers if len(
server.GetSnapshots()) > 0]
for server in servers_to_change:
changed = True
if not self.module.check_mode:
request = self._delete_server_snapshot(server)
request_list.append(request)
changed_servers = [
server.id for server in servers_to_change if server.id]
return changed, request_list, changed_servers
def _delete_server_snapshot(self, server):
"""
Delete snapshot for the CLC server
:param server: the CLC server object
:return: the delete snapshot request object from CLC API
"""
result = None
try:
result = server.DeleteSnapshot()
except CLCException as ex:
self.module.fail_json(msg='Failed to delete snapshot for server : {0}. {1}'.format(
server.id, ex.response_text
))
return result
def ensure_server_snapshot_restore(self, server_ids):
"""
Ensures the given set of server_ids have the snapshots restored
:param server_ids: The list of server_ids to delete the snapshot
:return: (changed, request_list, changed_servers)
changed: A flag indicating whether any change was made
request_list: the list of clc request objects from CLC API call
changed_servers: The list of servers ids that are modified
"""
request_list = []
changed = False
servers = self._get_servers_from_clc(
server_ids,
'Failed to obtain server list from the CLC API')
servers_to_change = [
server for server in servers if len(
server.GetSnapshots()) > 0]
for server in servers_to_change:
changed = True
if not self.module.check_mode:
request = self._restore_server_snapshot(server)
request_list.append(request)
changed_servers = [
server.id for server in servers_to_change if server.id]
return changed, request_list, changed_servers
def _restore_server_snapshot(self, server):
"""
Restore snapshot for the CLC server
:param server: the CLC server object
:return: the restore snapshot request object from CLC API
"""
result = None
try:
result = server.RestoreSnapshot()
except CLCException as ex:
self.module.fail_json(msg='Failed to restore snapshot for server : {0}. {1}'.format(
server.id, ex.response_text
))
return result
def _wait_for_requests_to_complete(self, requests_lst):
"""
Waits until the CLC requests are complete if the wait argument is True
:param requests_lst: The list of CLC request objects
:return: none
"""
if not self.module.params['wait']:
return
for request in requests_lst:
request.WaitUntilComplete()
for request_details in request.requests:
if request_details.Status() != 'succeeded':
self.module.fail_json(
msg='Unable to process server snapshot request')
@staticmethod
def define_argument_spec():
"""
This function defines the dictionary object required for
package module
:return: the package dictionary object
"""
argument_spec = dict(
server_ids=dict(type='list', required=True),
expiration_days=dict(default=7, type='int'),
wait=dict(default=True),
state=dict(
default='present',
choices=[
'present',
'absent',
'restore']),
)
return argument_spec
def _get_servers_from_clc(self, server_list, message):
"""
Internal function to fetch list of CLC server objects from a list of server ids
:param server_list: The list of server ids
:param message: The error message to throw in case of any error
:return the list of CLC server objects
"""
try:
return self.clc.v2.Servers(server_list).servers
except CLCException as ex:
return self.module.fail_json(msg=message + ': %s' % ex)
def _set_clc_credentials_from_env(self):
"""
Set the CLC Credentials on the sdk by reading environment variables
:return: none
"""
env = os.environ
v2_api_token = env.get('CLC_V2_API_TOKEN', False)
v2_api_username = env.get('CLC_V2_API_USERNAME', False)
v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
clc_alias = env.get('CLC_ACCT_ALIAS', False)
api_url = env.get('CLC_V2_API_URL', False)
if api_url:
self.clc.defaults.ENDPOINT_URL_V2 = api_url
if v2_api_token and clc_alias:
self.clc._LOGIN_TOKEN_V2 = v2_api_token
self.clc._V2_ENABLED = True
self.clc.ALIAS = clc_alias
elif v2_api_username and v2_api_passwd:
self.clc.v2.SetCredentials(
api_username=v2_api_username,
api_passwd=v2_api_passwd)
else:
return self.module.fail_json(
msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
"environment variables")
@staticmethod
def _set_user_agent(clc):
if hasattr(clc, 'SetRequestsSession'):
agent_string = "ClcAnsibleModule/" + __version__
ses = requests.Session()
ses.headers.update({"Api-Client": agent_string})
ses.headers['User-Agent'] += " " + agent_string
clc.SetRequestsSession(ses)
def main():
"""
Main function
:return: None
"""
module = AnsibleModule(
argument_spec=ClcSnapshot.define_argument_spec(),
supports_check_mode=True
)
clc_snapshot = ClcSnapshot(module)
clc_snapshot.process_request()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,300 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
# 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: cloudscale_floating_ip
short_description: Manages floating IPs on the cloudscale.ch IaaS service
description:
- Create, assign and delete floating IPs on the cloudscale.ch IaaS service.
notes:
- To create a new floating IP at least the C(ip_version) and C(server) options are required.
- Once a floating_ip is created all parameters except C(server) are read-only.
- It's not possible to request a floating IP without associating it with a server at the same time.
- This module requires the ipaddress python library. This library is included in Python since version 3.3. It is available as a
module on PyPI for earlier versions.
author:
- Gaudenz Steinlin (@gaudenz)
- Denis Krienbühl (@href)
options:
state:
description:
- State of the floating IP.
default: present
choices: [ present, absent ]
type: str
ip:
description:
- Floating IP address to change.
- Required to assign the IP to a different server or if I(state) is absent.
aliases: [ network ]
type: str
ip_version:
description:
- IP protocol version of the floating IP.
choices: [ 4, 6 ]
type: int
server:
description:
- UUID of the server assigned to this floating IP.
- Required unless I(state) is absent.
type: str
type:
description:
- The type of the floating IP.
choices: [ regional, global ]
type: str
default: regional
region:
description:
- Region in which the floating IP resides (e.g. C(lgp) or C(rma)).
If omitted, the region of the project default zone is used.
This parameter must be omitted if I(type) is set to C(global).
type: str
prefix_length:
description:
- Only valid if I(ip_version) is 6.
- Prefix length for the IPv6 network. Currently only a prefix of /56 can be requested. If no I(prefix_length) is present, a
single address is created.
choices: [ 56 ]
type: int
reverse_ptr:
description:
- Reverse PTR entry for this address.
- You cannot set a reverse PTR entry for IPv6 floating networks. Reverse PTR entries are only allowed for single addresses.
type: str
extends_documentation_fragment:
- community.general.cloudscale
'''
EXAMPLES = '''
# Request a new floating IP
- name: Request a floating IP
cloudscale_floating_ip:
ip_version: 4
server: 47cec963-fcd2-482f-bdb6-24461b2d47b1
reverse_ptr: my-server.example.com
api_token: xxxxxx
register: floating_ip
# Assign an existing floating IP to a different server
- name: Move floating IP to backup server
cloudscale_floating_ip:
ip: 192.0.2.123
server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
api_token: xxxxxx
# Request a new floating IPv6 network
- name: Request a floating IP
cloudscale_floating_ip:
ip_version: 6
prefix_length: 56
server: 47cec963-fcd2-482f-bdb6-24461b2d47b1
api_token: xxxxxx
region: lpg1
register: floating_ip
# Assign an existing floating network to a different server
- name: Move floating IP to backup server
cloudscale_floating_ip:
ip: '{{ floating_ip.network | ip }}'
server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
api_token: xxxxxx
# Release a floating IP
- name: Release floating IP
cloudscale_floating_ip:
ip: 192.0.2.123
state: absent
api_token: xxxxxx
'''
RETURN = '''
href:
description: The API URL to get details about this floating IP.
returned: success when state == present
type: str
sample: https://api.cloudscale.ch/v1/floating-ips/2001:db8::cafe
network:
description: The CIDR notation of the network that is routed to your server.
returned: success when state == present
type: str
sample: 2001:db8::cafe/128
next_hop:
description: Your floating IP is routed to this IP address.
returned: success when state == present
type: str
sample: 2001:db8:dead:beef::42
reverse_ptr:
description: The reverse pointer for this floating IP address.
returned: success when state == present
type: str
sample: 185-98-122-176.cust.cloudscale.ch
server:
description: The floating IP is routed to this server.
returned: success when state == present
type: str
sample: 47cec963-fcd2-482f-bdb6-24461b2d47b1
ip:
description: The floating IP address or network. This is always present and used to identify floating IPs after creation.
returned: success
type: str
sample: 185.98.122.176
region:
description: The region of the floating IP.
returned: success when state == present
type: dict
sample: {'slug': 'lpg'}
version_added: '2.10'
state:
description: The current status of the floating IP.
returned: success
type: str
sample: present
'''
import traceback
IPADDRESS_IMP_ERR = None
try:
from ipaddress import ip_network
HAS_IPADDRESS = True
except ImportError:
IPADDRESS_IMP_ERR = traceback.format_exc()
HAS_IPADDRESS = False
from ansible.module_utils.basic import AnsibleModule, env_fallback, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.cloudscale import AnsibleCloudscaleBase, cloudscale_argument_spec
class AnsibleCloudscaleFloatingIP(AnsibleCloudscaleBase):
def __init__(self, module):
super(AnsibleCloudscaleFloatingIP, self).__init__(module)
# Initialize info dict
# Set state to absent, will be updated by self.update_info()
self.info = {'state': 'absent'}
if self._module.params['ip']:
self.update_info()
@staticmethod
def _resp2info(resp):
# If the API response has some content, the floating IP must exist
resp['state'] = 'present'
# Add the IP address to the response, otherwise handling get's to complicated as this
# has to be converted from the network all the time.
resp['ip'] = str(ip_network(resp['network']).network_address)
# Replace the server with just the UUID, the href to the server is useless and just makes
# things more complicated
if resp['server'] is not None:
resp['server'] = resp['server']['uuid']
return resp
def update_info(self):
resp = self._get('floating-ips/' + self._module.params['ip'])
if resp:
self.info = self._resp2info(resp)
else:
self.info = {'ip': self._module.params['ip'],
'state': 'absent'}
def request_floating_ip(self):
params = self._module.params
# check for required parameters to request a floating IP
missing_parameters = []
for p in ('ip_version', 'server'):
if p not in params or not params[p]:
missing_parameters.append(p)
if len(missing_parameters) > 0:
self._module.fail_json(msg='Missing required parameter(s) to request a floating IP: %s.' %
' '.join(missing_parameters))
data = {'ip_version': params['ip_version'],
'server': params['server']}
for p in ('prefix_length', 'reverse_ptr', 'type', 'region'):
if params[p]:
data[p] = params[p]
self.info = self._resp2info(self._post('floating-ips', data))
def release_floating_ip(self):
self._delete('floating-ips/%s' % self._module.params['ip'])
self.info = {'ip': self.info['ip'], 'state': 'absent'}
def update_floating_ip(self):
params = self._module.params
if 'server' not in params or not params['server']:
self._module.fail_json(msg='Missing required parameter to update a floating IP: server.')
self.info = self._resp2info(self._post('floating-ips/%s' % params['ip'], {'server': params['server']}))
def main():
argument_spec = cloudscale_argument_spec()
argument_spec.update(dict(
state=dict(default='present', choices=('present', 'absent'), type='str'),
ip=dict(aliases=('network', ), type='str'),
ip_version=dict(choices=(4, 6), type='int'),
server=dict(type='str'),
type=dict(type='str', choices=('regional', 'global'), default='regional'),
region=dict(type='str'),
prefix_length=dict(choices=(56,), type='int'),
reverse_ptr=dict(type='str'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(('ip', 'ip_version'),),
supports_check_mode=True,
)
if not HAS_IPADDRESS:
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMP_ERR)
target_state = module.params['state']
target_server = module.params['server']
floating_ip = AnsibleCloudscaleFloatingIP(module)
current_state = floating_ip.info['state']
current_server = floating_ip.info['server'] if 'server' in floating_ip.info else None
if module.check_mode:
module.exit_json(changed=not target_state == current_state or
(current_state == 'present' and current_server != target_server),
**floating_ip.info)
changed = False
if current_state == 'absent' and target_state == 'present':
floating_ip.request_floating_ip()
changed = True
elif current_state == 'present' and target_state == 'absent':
floating_ip.release_floating_ip()
changed = True
elif current_state == 'present' and current_server != target_server:
floating_ip.update_floating_ip()
changed = True
module.exit_json(changed=changed, **floating_ip.info)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,555 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2017, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
# Copyright: (c) 2019, René Moser <mail@renemoser.net>
# 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: cloudscale_server
short_description: Manages servers on the cloudscale.ch IaaS service
description:
- Create, update, start, stop and delete servers on the cloudscale.ch IaaS service.
notes:
- Since version 2.8, I(uuid) and I(name) or not mutually exclusive anymore.
- If I(uuid) option is provided, it takes precedence over I(name) for server selection. This allows to update the server's name.
- If no I(uuid) option is provided, I(name) is used for server selection. If more than one server with this name exists, execution is aborted.
- Only the I(name) and I(flavor) are evaluated for the update.
- The option I(force=true) must be given to allow the reboot of existing running servers for applying the changes.
author:
- Gaudenz Steinlin (@gaudenz)
- René Moser (@resmo)
- Denis Krienbühl (@href)
options:
state:
description:
- State of the server.
choices: [ running, stopped, absent ]
default: running
type: str
name:
description:
- Name of the Server.
- Either I(name) or I(uuid) are required.
type: str
uuid:
description:
- UUID of the server.
- Either I(name) or I(uuid) are required.
type: str
flavor:
description:
- Flavor of the server.
type: str
image:
description:
- Image used to create the server.
type: str
zone:
description:
- Zone in which the server resides (e.g. C(lgp1) or C(rma1)).
type: str
volume_size_gb:
description:
- Size of the root volume in GB.
default: 10
type: int
bulk_volume_size_gb:
description:
- Size of the bulk storage volume in GB.
- No bulk storage volume if not set.
type: int
ssh_keys:
description:
- List of SSH public keys.
- Use the full content of your .pub file here.
type: list
password:
description:
- Password for the server.
type: str
use_public_network:
description:
- Attach a public network interface to the server.
default: yes
type: bool
use_private_network:
description:
- Attach a private network interface to the server.
default: no
type: bool
use_ipv6:
description:
- Enable IPv6 on the public network interface.
default: yes
type: bool
anti_affinity_with:
description:
- UUID of another server to create an anti-affinity group with.
- Mutually exclusive with I(server_groups).
- Deprecated, removed in version 2.11.
type: str
server_groups:
description:
- List of UUID or names of server groups.
- Mutually exclusive with I(anti_affinity_with).
type: list
user_data:
description:
- Cloud-init configuration (cloud-config) data to use for the server.
type: str
force:
description:
- Allow to stop the running server for updating if necessary.
default: no
type: bool
tags:
description:
- Tags assosiated with the servers. Set this to C({}) to clear any tags.
type: dict
extends_documentation_fragment:
- community.general.cloudscale
'''
EXAMPLES = '''
# Create and start a server with an existing server group (shiny-group)
- name: Start cloudscale.ch server
cloudscale_server:
name: my-shiny-cloudscale-server
image: debian-8
flavor: flex-4
ssh_keys: ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
server_groups: shiny-group
zone: lpg1
use_private_network: True
bulk_volume_size_gb: 100
api_token: xxxxxx
# Start another server in anti-affinity (server group shiny-group)
- name: Start second cloudscale.ch server
cloudscale_server:
name: my-other-shiny-server
image: ubuntu-16.04
flavor: flex-8
ssh_keys: ssh-rsa XXXXXXXXXXX ansible@cloudscale
server_groups: shiny-group
zone: lpg1
api_token: xxxxxx
# Force to update the flavor of a running server
- name: Start cloudscale.ch server
cloudscale_server:
name: my-shiny-cloudscale-server
image: debian-8
flavor: flex-8
force: yes
ssh_keys: ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
use_private_network: True
bulk_volume_size_gb: 100
api_token: xxxxxx
register: server1
# Stop the first server
- name: Stop my first server
cloudscale_server:
uuid: '{{ server1.uuid }}'
state: stopped
api_token: xxxxxx
# Delete my second server
- name: Delete my second server
cloudscale_server:
name: my-other-shiny-server
state: absent
api_token: xxxxxx
# Start a server and wait for the SSH host keys to be generated
- name: Start server and wait for SSH host keys
cloudscale_server:
name: my-cloudscale-server-with-ssh-key
image: debian-8
flavor: flex-4
ssh_keys: ssh-rsa XXXXXXXXXXX ansible@cloudscale
api_token: xxxxxx
register: server
until: server.ssh_fingerprints is defined and server.ssh_fingerprints
retries: 60
delay: 2
'''
RETURN = '''
href:
description: API URL to get details about this server
returned: success when not state == absent
type: str
sample: https://api.cloudscale.ch/v1/servers/cfde831a-4e87-4a75-960f-89b0148aa2cc
uuid:
description: The unique identifier for this server
returned: success
type: str
sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
name:
description: The display name of the server
returned: success
type: str
sample: its-a-me-mario.cloudscale.ch
state:
description: The current status of the server
returned: success
type: str
sample: running
flavor:
description: The flavor that has been used for this server
returned: success when not state == absent
type: dict
sample: { "slug": "flex-4", "name": "Flex-4", "vcpu_count": 2, "memory_gb": 4 }
image:
description: The image used for booting this server
returned: success when not state == absent
type: dict
sample: { "default_username": "ubuntu", "name": "Ubuntu 18.04 LTS", "operating_system": "Ubuntu", "slug": "ubuntu-18.04" }
zone:
description: The zone used for booting this server
returned: success when not state == absent
type: dict
sample: { 'slug': 'lpg1' }
version_added: '2.10'
volumes:
description: List of volumes attached to the server
returned: success when not state == absent
type: list
sample: [ {"type": "ssd", "device": "/dev/vda", "size_gb": "50"} ]
interfaces:
description: List of network ports attached to the server
returned: success when not state == absent
type: list
sample: [ { "type": "public", "addresses": [ ... ] } ]
ssh_fingerprints:
description: A list of SSH host key fingerprints. Will be null until the host keys could be retrieved from the server.
returned: success when not state == absent
type: list
sample: ["ecdsa-sha2-nistp256 SHA256:XXXX", ... ]
ssh_host_keys:
description: A list of SSH host keys. Will be null until the host keys could be retrieved from the server.
returned: success when not state == absent
type: list
sample: ["ecdsa-sha2-nistp256 XXXXX", ... ]
anti_affinity_with:
description:
- List of servers in the same anti-affinity group
- Deprecated, removed in version 2.11.
returned: success when not state == absent
type: list
sample: []
server_groups:
description: List of server groups
returned: success when not state == absent
type: list
sample: [ {"href": "https://api.cloudscale.ch/v1/server-groups/...", "uuid": "...", "name": "db-group"} ]
version_added: '2.8'
tags:
description: Tags assosiated with the volume.
returned: success
type: dict
sample: { 'project': 'my project' }
version_added: '2.9'
'''
from datetime import datetime, timedelta
from time import sleep
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudscale import AnsibleCloudscaleBase, cloudscale_argument_spec
ALLOWED_STATES = ('running',
'stopped',
'absent',
)
class AnsibleCloudscaleServer(AnsibleCloudscaleBase):
def __init__(self, module):
super(AnsibleCloudscaleServer, self).__init__(module)
# Initialize server dictionary
self._info = {}
def _init_server_container(self):
return {
'uuid': self._module.params.get('uuid') or self._info.get('uuid'),
'name': self._module.params.get('name') or self._info.get('name'),
'state': 'absent',
}
def _get_server_info(self, refresh=False):
if self._info and not refresh:
return self._info
self._info = self._init_server_container()
uuid = self._info.get('uuid')
if uuid is not None:
server_info = self._get('servers/%s' % uuid)
if server_info:
self._info = self._transform_state(server_info)
else:
name = self._info.get('name')
if name is not None:
servers = self._get('servers') or []
matching_server = []
for server in servers:
if server['name'] == name:
matching_server.append(server)
if len(matching_server) == 1:
self._info = self._transform_state(matching_server[0])
elif len(matching_server) > 1:
self._module.fail_json(msg="More than one server with name '%s' exists. "
"Use the 'uuid' parameter to identify the server." % name)
return self._info
@staticmethod
def _transform_state(server):
if 'status' in server:
server['state'] = server['status']
del server['status']
else:
server['state'] = 'absent'
return server
def _wait_for_state(self, states):
start = datetime.now()
timeout = self._module.params['api_timeout'] * 2
while datetime.now() - start < timedelta(seconds=timeout):
server_info = self._get_server_info(refresh=True)
if server_info.get('state') in states:
return server_info
sleep(1)
# Timeout succeeded
if server_info.get('name') is not None:
msg = "Timeout while waiting for a state change on server %s to states %s. " \
"Current state is %s." % (server_info.get('name'), states, server_info.get('state'))
else:
name_uuid = self._module.params.get('name') or self._module.params.get('uuid')
msg = 'Timeout while waiting to find the server %s' % name_uuid
self._module.fail_json(msg=msg)
def _start_stop_server(self, server_info, target_state="running", ignore_diff=False):
actions = {
'stopped': 'stop',
'running': 'start',
}
server_state = server_info.get('state')
if server_state != target_state:
self._result['changed'] = True
if not ignore_diff:
self._result['diff']['before'].update({
'state': server_info.get('state'),
})
self._result['diff']['after'].update({
'state': target_state,
})
if not self._module.check_mode:
self._post('servers/%s/%s' % (server_info['uuid'], actions[target_state]))
server_info = self._wait_for_state((target_state, ))
return server_info
def _update_param(self, param_key, server_info, requires_stop=False):
param_value = self._module.params.get(param_key)
if param_value is None:
return server_info
if 'slug' in server_info[param_key]:
server_v = server_info[param_key]['slug']
else:
server_v = server_info[param_key]
if server_v != param_value:
# Set the diff output
self._result['diff']['before'].update({param_key: server_v})
self._result['diff']['after'].update({param_key: param_value})
if server_info.get('state') == "running":
if requires_stop and not self._module.params.get('force'):
self._module.warn("Some changes won't be applied to running servers. "
"Use force=yes to allow the server '%s' to be stopped/started." % server_info['name'])
return server_info
# Either the server is stopped or change is forced
self._result['changed'] = True
if not self._module.check_mode:
if requires_stop:
self._start_stop_server(server_info, target_state="stopped", ignore_diff=True)
patch_data = {
param_key: param_value,
}
# Response is 204: No Content
self._patch('servers/%s' % server_info['uuid'], patch_data)
# State changes to "changing" after update, waiting for stopped/running
server_info = self._wait_for_state(('stopped', 'running'))
return server_info
def _get_server_group_ids(self):
server_group_params = self._module.params['server_groups']
if not server_group_params:
return None
matching_group_names = []
results = []
server_groups = self._get('server-groups')
for server_group in server_groups:
if server_group['uuid'] in server_group_params:
results.append(server_group['uuid'])
server_group_params.remove(server_group['uuid'])
elif server_group['name'] in server_group_params:
results.append(server_group['uuid'])
server_group_params.remove(server_group['name'])
# Remember the names found
matching_group_names.append(server_group['name'])
# Names are not unique, verify if name already found in previous iterations
elif server_group['name'] in matching_group_names:
self._module.fail_json(msg="More than one server group with name exists: '%s'. "
"Use the 'uuid' parameter to identify the server group." % server_group['name'])
if server_group_params:
self._module.fail_json(msg="Server group name or UUID not found: %s" % ', '.join(server_group_params))
return results
def _create_server(self, server_info):
self._result['changed'] = True
data = deepcopy(self._module.params)
for i in ('uuid', 'state', 'force', 'api_timeout', 'api_token'):
del data[i]
data['server_groups'] = self._get_server_group_ids()
self._result['diff']['before'] = self._init_server_container()
self._result['diff']['after'] = deepcopy(data)
if not self._module.check_mode:
self._post('servers', data)
server_info = self._wait_for_state(('running', ))
return server_info
def _update_server(self, server_info):
previous_state = server_info.get('state')
# The API doesn't support to update server groups.
# Show a warning to the user if the desired state does not match.
desired_server_group_ids = self._get_server_group_ids()
if desired_server_group_ids is not None:
current_server_group_ids = [grp['uuid'] for grp in server_info['server_groups']]
if desired_server_group_ids != current_server_group_ids:
self._module.warn("Server groups can not be mutated, server needs redeployment to change groups.")
server_info = self._update_param('flavor', server_info, requires_stop=True)
server_info = self._update_param('name', server_info)
server_info = self._update_param('tags', server_info)
if previous_state == "running":
server_info = self._start_stop_server(server_info, target_state="running", ignore_diff=True)
return server_info
def present_server(self):
server_info = self._get_server_info()
if server_info.get('state') != "absent":
# If target state is stopped, stop before an potential update and force would not be required
if self._module.params.get('state') == "stopped":
server_info = self._start_stop_server(server_info, target_state="stopped")
server_info = self._update_server(server_info)
if self._module.params.get('state') == "running":
server_info = self._start_stop_server(server_info, target_state="running")
else:
server_info = self._create_server(server_info)
server_info = self._start_stop_server(server_info, target_state=self._module.params.get('state'))
return server_info
def absent_server(self):
server_info = self._get_server_info()
if server_info.get('state') != "absent":
self._result['changed'] = True
self._result['diff']['before'] = deepcopy(server_info)
self._result['diff']['after'] = self._init_server_container()
if not self._module.check_mode:
self._delete('servers/%s' % server_info['uuid'])
server_info = self._wait_for_state(('absent', ))
return server_info
def main():
argument_spec = cloudscale_argument_spec()
argument_spec.update(dict(
state=dict(default='running', choices=ALLOWED_STATES),
name=dict(),
uuid=dict(),
flavor=dict(),
image=dict(),
zone=dict(),
volume_size_gb=dict(type='int', default=10),
bulk_volume_size_gb=dict(type='int'),
ssh_keys=dict(type='list'),
password=dict(no_log=True),
use_public_network=dict(type='bool', default=True),
use_private_network=dict(type='bool', default=False),
use_ipv6=dict(type='bool', default=True),
anti_affinity_with=dict(removed_in_version='2.11'),
server_groups=dict(type='list'),
user_data=dict(),
force=dict(type='bool', default=False),
tags=dict(type='dict'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(('name', 'uuid'),),
mutually_exclusive=(('anti_affinity_with', 'server_groups'),),
supports_check_mode=True,
)
cloudscale_server = AnsibleCloudscaleServer(module)
if module.params['state'] == "absent":
server = cloudscale_server.absent_server()
else:
server = cloudscale_server.present_server()
result = cloudscale_server.get_result(server)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,237 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2019, René Moser <mail@renemoser.net>
# 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: cloudscale_server_group
short_description: Manages server groups on the cloudscale.ch IaaS service
description:
- Create, update and remove server groups.
author:
- René Moser (@resmo)
- Denis Krienbühl (@href)
options:
name:
description:
- Name of the server group.
- Either I(name) or I(uuid) is required. These options are mutually exclusive.
type: str
uuid:
description:
- UUID of the server group.
- Either I(name) or I(uuid) is required. These options are mutually exclusive.
type: str
type:
description:
- Type of the server group.
default: anti-affinity
type: str
zone:
description:
- Zone slug of the server group (e.g. C(lgp1) or C(rma1)).
type: str
state:
description:
- State of the server group.
choices: [ present, absent ]
default: present
type: str
tags:
description:
- Tags assosiated with the server groups. Set this to C({}) to clear any tags.
type: dict
extends_documentation_fragment:
- community.general.cloudscale
'''
EXAMPLES = '''
---
- name: Ensure server group exists
cloudscale_server_group:
name: my-name
type: anti-affinity
api_token: xxxxxx
- name: Ensure server group in a specific zone
cloudscale_server_group:
name: my-rma-group
type: anti-affinity
zone: lpg1
api_token: xxxxxx
- name: Ensure a server group is absent
cloudscale_server_group:
name: my-name
state: absent
api_token: xxxxxx
'''
RETURN = '''
---
href:
description: API URL to get details about this server group
returned: if available
type: str
sample: https://api.cloudscale.ch/v1/server-group/cfde831a-4e87-4a75-960f-89b0148aa2cc
uuid:
description: The unique identifier for this server
returned: always
type: str
sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
name:
description: The display name of the server group
returned: always
type: str
sample: load balancers
type:
description: The type the server group
returned: if available
type: str
sample: anti-affinity
zone:
description: The zone of the server group
returned: success
type: dict
sample: { 'slug': 'rma1' }
version_added: '2.10'
servers:
description: A list of servers that are part of the server group.
returned: if available
type: list
sample: []
state:
description: State of the server group.
returned: always
type: str
sample: present
tags:
description: Tags assosiated with the server group.
returned: success
type: dict
sample: { 'project': 'my project' }
version_added: '2.9'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudscale import AnsibleCloudscaleBase, cloudscale_argument_spec
class AnsibleCloudscaleServerGroup(AnsibleCloudscaleBase):
def __init__(self, module, namespace):
super(AnsibleCloudscaleServerGroup, self).__init__(module)
self._info = {}
def _init_container(self):
return {
'uuid': self._module.params.get('uuid') or self._info.get('uuid'),
'name': self._module.params.get('name') or self._info.get('name'),
'state': 'absent',
}
def _create_server_group(self, server_group):
self._module.fail_on_missing_params(['name'])
self._result['changed'] = True
data = {
'name': self._module.params.get('name'),
'type': self._module.params.get('type'),
'zone': self._module.params.get('zone'),
'tags': self._module.params.get('tags'),
}
if not self._module.check_mode:
server_group = self._post('server-groups', data)
return server_group
def _update_server_group(self, server_group):
updated = self._param_updated('name', server_group)
updated = self._param_updated('tags', server_group) or updated
# Refresh if resource was updated in live mode
if updated and not self._module.check_mode:
server_group = self.get_server_group()
return server_group
def get_server_group(self):
self._info = self._init_container()
uuid = self._info.get('uuid')
if uuid is not None:
server_group = self._get('server-groups/%s' % uuid)
if server_group:
self._info.update(server_group)
self._info.update(dict(state='present'))
else:
name = self._info.get('name')
matching_server_groups = []
for server_group in self._get('server-groups'):
if server_group['name'] == name:
matching_server_groups.append(server_group)
if len(matching_server_groups) > 1:
self._module.fail_json(msg="More than one server group with name exists: '%s'. "
"Use the 'uuid' parameter to identify the server group." % name)
elif len(matching_server_groups) == 1:
self._info.update(matching_server_groups[0])
self._info.update(dict(state='present'))
return self._info
def present_group(self):
server_group = self.get_server_group()
if server_group.get('state') == 'absent':
server_group = self._create_server_group(server_group)
else:
server_group = self._update_server_group(server_group)
return server_group
def absent_group(self):
server_group = self.get_server_group()
if server_group.get('state') != 'absent':
self._result['changed'] = True
if not self._module.check_mode:
self._delete('server-groups/%s' % server_group['uuid'])
return server_group
def main():
argument_spec = cloudscale_argument_spec()
argument_spec.update(dict(
name=dict(),
uuid=dict(),
type=dict(default='anti-affinity'),
zone=dict(),
tags=dict(type='dict'),
state=dict(default='present', choices=['absent', 'present']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(('name', 'uuid'),),
supports_check_mode=True,
)
cloudscale_server_group = AnsibleCloudscaleServerGroup(module, 'cloudscale_server_group')
if module.params['state'] == 'absent':
server_group = cloudscale_server_group.absent_group()
else:
server_group = cloudscale_server_group.present_group()
result = cloudscale_server_group.get_result(server_group)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,305 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2018, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
# Copyright (c) 2019, René Moser <mail@renemoser.net>
# 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: cloudscale_volume
short_description: Manages volumes on the cloudscale.ch IaaS service.
description:
- Create, attach/detach, update and delete volumes on the cloudscale.ch IaaS service.
notes:
- To create a new volume at least the I(name) and I(size_gb) options
are required.
- A volume can be created and attached to a server in the same task.
author:
- Gaudenz Steinlin (@gaudenz)
- René Moser (@resmo)
- Denis Krienbühl (@href)
options:
state:
description:
- State of the volume.
default: present
choices: [ present, absent ]
type: str
name:
description:
- Name of the volume. Either name or UUID must be present to change an
existing volume.
type: str
uuid:
description:
- UUID of the volume. Either name or UUID must be present to change an
existing volume.
type: str
size_gb:
description:
- Size of the volume in GB.
type: int
type:
description:
- Type of the volume. Cannot be changed after creating the volume.
Defaults to C(ssd) on volume creation.
choices: [ ssd, bulk ]
type: str
zone:
description:
- Zone in which the volume resides (e.g. C(lgp1) or C(rma1)). Cannot be
changed after creating the volume. Defaults to the project default zone.
type: str
server_uuids:
description:
- UUIDs of the servers this volume is attached to. Set this to C([]) to
detach the volume. Currently a volume can only be attached to a
single server.
aliases: [ server_uuid ]
type: list
tags:
description:
- Tags associated with the volume. Set this to C({}) to clear any tags.
type: dict
extends_documentation_fragment:
- community.general.cloudscale
'''
EXAMPLES = '''
# Create a new SSD volume
- name: Create an SSD volume
cloudscale_volume:
name: my_ssd_volume
zone: 'lpg1'
size_gb: 50
api_token: xxxxxx
register: my_ssd_volume
# Attach an existing volume to a server
- name: Attach volume to server
cloudscale_volume:
uuid: my_ssd_volume.uuid
server_uuids:
- ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
api_token: xxxxxx
# Create and attach a volume to a server
- name: Create and attach volume to server
cloudscale_volume:
name: my_ssd_volume
zone: 'lpg1'
size_gb: 50
server_uuids:
- ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
api_token: xxxxxx
# Detach volume from server
- name: Detach volume from server
cloudscale_volume:
uuid: my_ssd_volume.uuid
server_uuids: []
api_token: xxxxxx
# Delete a volume
- name: Delete volume
cloudscale_volume:
name: my_ssd_volume
state: absent
api_token: xxxxxx
'''
RETURN = '''
href:
description: The API URL to get details about this volume.
returned: state == present
type: str
sample: https://api.cloudscale.ch/v1/volumes/2db69ba3-1864-4608-853a-0771b6885a3a
uuid:
description: The unique identifier for this volume.
returned: state == present
type: str
sample: 2db69ba3-1864-4608-853a-0771b6885a3a
name:
description: The display name of the volume.
returned: state == present
type: str
sample: my_ssd_volume
size_gb:
description: The size of the volume in GB.
returned: state == present
type: str
sample: 50
type:
description: The type of the volume.
returned: state == present
type: str
sample: bulk
zone:
description: The zone of the volume.
returned: state == present
type: dict
sample: {'slug': 'lpg1'}
version_added: '2.10'
server_uuids:
description: The UUIDs of the servers this volume is attached to.
returned: state == present
type: list
sample: ['47cec963-fcd2-482f-bdb6-24461b2d47b1']
state:
description: The current status of the volume.
returned: success
type: str
sample: present
tags:
description: Tags associated with the volume.
returned: state == present
type: dict
sample: { 'project': 'my project' }
version_added: '2.9'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudscale import (AnsibleCloudscaleBase,
cloudscale_argument_spec,
)
class AnsibleCloudscaleVolume(AnsibleCloudscaleBase):
def __init__(self, module):
super(AnsibleCloudscaleVolume, self).__init__(module)
self._info = {}
def _init_container(self):
return {
'uuid': self._module.params.get('uuid') or self._info.get('uuid'),
'name': self._module.params.get('name') or self._info.get('name'),
'state': 'absent',
}
def _create(self, volume):
# Fail when missing params for creation
self._module.fail_on_missing_params(['name', 'size_gb'])
# Fail if a user uses a UUID and state=present but the volume was not found.
if self._module.params.get('uuid'):
self._module.fail_json(msg="The volume with UUID '%s' was not found "
"and we would create a new one with different UUID, "
"this is probably not want you have asked for." % self._module.params.get('uuid'))
self._result['changed'] = True
data = {
'name': self._module.params.get('name'),
'type': self._module.params.get('type'),
'zone': self._module.params.get('zone'),
'size_gb': self._module.params.get('size_gb') or 'ssd',
'server_uuids': self._module.params.get('server_uuids') or [],
'tags': self._module.params.get('tags'),
}
if not self._module.check_mode:
volume = self._post('volumes', data)
return volume
def _update(self, volume):
update_params = (
'name',
'size_gb',
'server_uuids',
'tags',
)
updated = False
for param in update_params:
updated = self._param_updated(param, volume) or updated
# Refresh if resource was updated in live mode
if updated and not self._module.check_mode:
volume = self.get_volume()
return volume
def get_volume(self):
self._info = self._init_container()
uuid = self._info.get('uuid')
if uuid is not None:
volume = self._get('volumes/%s' % uuid)
if volume:
self._info.update(volume)
self._info['state'] = 'present'
else:
name = self._info.get('name')
matching_volumes = []
for volume in self._get('volumes'):
if volume['name'] == name:
matching_volumes.append(volume)
if len(matching_volumes) > 1:
self._module.fail_json(msg="More than one volume with name exists: '%s'. "
"Use the 'uuid' parameter to identify the volume." % name)
elif len(matching_volumes) == 1:
self._info.update(matching_volumes[0])
self._info['state'] = 'present'
return self._info
def present(self):
volume = self.get_volume()
if volume.get('state') == 'absent':
volume = self._create(volume)
else:
volume = self._update(volume)
return volume
def absent(self):
volume = self.get_volume()
if volume.get('state') != 'absent':
self._result['changed'] = True
if not self._module.check_mode:
volume['state'] = "absent"
self._delete('volumes/%s' % volume['uuid'])
return volume
def main():
argument_spec = cloudscale_argument_spec()
argument_spec.update(dict(
state=dict(default='present', choices=('present', 'absent')),
name=dict(),
uuid=dict(),
zone=dict(),
size_gb=dict(type='int'),
type=dict(choices=('ssd', 'bulk')),
server_uuids=dict(type='list', aliases=['server_uuid']),
tags=dict(type='dict'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(('name', 'uuid'),),
supports_check_mode=True,
)
cloudscale_volume = AnsibleCloudscaleVolume(module)
if module.params['state'] == 'absent':
server_group = cloudscale_volume.absent()
else:
server_group = cloudscale_volume.present()
result = cloudscale_volume.get_result(server_group)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,460 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_account
short_description: Manages accounts on Apache CloudStack based clouds.
description:
- Create, disable, lock, enable and remove accounts.
author: René Moser (@resmo)
options:
name:
description:
- Name of account.
type: str
required: true
username:
description:
- Username of the user to be created if account did not exist.
- Required on I(state=present).
type: str
password:
description:
- Password of the user to be created if account did not exist.
- Required on I(state=present) if I(ldap_domain) is not set.
type: str
first_name:
description:
- First name of the user to be created if account did not exist.
- Required on I(state=present) if I(ldap_domain) is not set.
type: str
last_name:
description:
- Last name of the user to be created if account did not exist.
- Required on I(state=present) if I(ldap_domain) is not set.
type: str
email:
description:
- Email of the user to be created if account did not exist.
- Required on I(state=present) if I(ldap_domain) is not set.
type: str
timezone:
description:
- Timezone of the user to be created if account did not exist.
type: str
network_domain:
description:
- Network domain of the account.
type: str
account_type:
description:
- Type of the account.
type: str
choices: [ user, root_admin, domain_admin ]
default: user
domain:
description:
- Domain the account is related to.
type: str
default: ROOT
role:
description:
- Creates the account under the specified role name or id.
type: str
ldap_domain:
description:
- Name of the LDAP group or OU to bind.
- If set, account will be linked to LDAP.
type: str
ldap_type:
description:
- Type of the ldap name. GROUP or OU, defaults to GROUP.
type: str
choices: [ GROUP, OU ]
default: GROUP
state:
description:
- State of the account.
- C(unlocked) is an alias for C(enabled).
type: str
choices: [ present, absent, enabled, disabled, locked, unlocked ]
default: present
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create an account in domain 'CUSTOMERS'
cs_account:
name: customer_xy
username: customer_xy
password: S3Cur3
last_name: Doe
first_name: John
email: john.doe@example.com
domain: CUSTOMERS
role: Domain Admin
delegate_to: localhost
- name: Lock an existing account in domain 'CUSTOMERS'
cs_account:
name: customer_xy
domain: CUSTOMERS
state: locked
delegate_to: localhost
- name: Disable an existing account in domain 'CUSTOMERS'
cs_account:
name: customer_xy
domain: CUSTOMERS
state: disabled
delegate_to: localhost
- name: Enable an existing account in domain 'CUSTOMERS'
cs_account:
name: customer_xy
domain: CUSTOMERS
state: enabled
delegate_to: localhost
- name: Remove an account in domain 'CUSTOMERS'
cs_account:
name: customer_xy
domain: CUSTOMERS
state: absent
delegate_to: localhost
- name: Create a single user LDAP account in domain 'CUSTOMERS'
cs_account:
name: customer_xy
username: customer_xy
domain: CUSTOMERS
ldap_domain: cn=customer_xy,cn=team_xy,ou=People,dc=domain,dc=local
delegate_to: localhost
- name: Create a LDAP account in domain 'CUSTOMERS' and bind it to a LDAP group
cs_account:
name: team_xy
username: customer_xy
domain: CUSTOMERS
ldap_domain: cn=team_xy,ou=People,dc=domain,dc=local
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the account.
returned: success
type: str
sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8
name:
description: Name of the account.
returned: success
type: str
sample: linus@example.com
account_type:
description: Type of the account.
returned: success
type: str
sample: user
state:
description: State of the account.
returned: success
type: str
sample: enabled
network_domain:
description: Network domain of the account.
returned: success
type: str
sample: example.local
domain:
description: Domain the account is related.
returned: success
type: str
sample: ROOT
role:
description: The role name of the account
returned: success
type: str
sample: Domain Admin
'''
# import cloudstack common
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackAccount(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackAccount, self).__init__(module)
self.returns = {
'networkdomain': 'network_domain',
'rolename': 'role',
}
self.account = None
self.account_types = {
'user': 0,
'root_admin': 1,
'domain_admin': 2,
}
def get_role_id(self):
role_param = self.module.params.get('role')
role_id = None
if role_param:
role_list = self.query_api('listRoles')
for role in role_list['role']:
if role_param in [role['name'], role['id']]:
role_id = role['id']
if not role_id:
self.module.fail_json(msg="Role not found: %s" % role_param)
return role_id
def get_account_type(self):
account_type = self.module.params.get('account_type')
return self.account_types[account_type]
def get_account(self):
if not self.account:
args = {
'listall': True,
'domainid': self.get_domain(key='id'),
'fetch_list': True,
}
accounts = self.query_api('listAccounts', **args)
if accounts:
account_name = self.module.params.get('name')
for a in accounts:
if account_name == a['name']:
self.account = a
break
return self.account
def enable_account(self):
account = self.get_account()
if not account:
account = self.present_account()
if account['state'].lower() != 'enabled':
self.result['changed'] = True
args = {
'id': account['id'],
'account': self.module.params.get('name'),
'domainid': self.get_domain(key='id')
}
if not self.module.check_mode:
res = self.query_api('enableAccount', **args)
account = res['account']
return account
def lock_account(self):
return self.lock_or_disable_account(lock=True)
def disable_account(self):
return self.lock_or_disable_account()
def lock_or_disable_account(self, lock=False):
account = self.get_account()
if not account:
account = self.present_account()
# we need to enable the account to lock it.
if lock and account['state'].lower() == 'disabled':
account = self.enable_account()
if (lock and account['state'].lower() != 'locked' or
not lock and account['state'].lower() != 'disabled'):
self.result['changed'] = True
args = {
'id': account['id'],
'account': self.module.params.get('name'),
'domainid': self.get_domain(key='id'),
'lock': lock,
}
if not self.module.check_mode:
account = self.query_api('disableAccount', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
account = self.poll_job(account, 'account')
return account
def present_account(self):
account = self.get_account()
if not account:
self.result['changed'] = True
if self.module.params.get('ldap_domain'):
required_params = [
'domain',
'username',
]
self.module.fail_on_missing_params(required_params=required_params)
account = self.create_ldap_account(account)
else:
required_params = [
'email',
'username',
'password',
'first_name',
'last_name',
]
self.module.fail_on_missing_params(required_params=required_params)
account = self.create_account(account)
return account
def create_ldap_account(self, account):
args = {
'account': self.module.params.get('name'),
'domainid': self.get_domain(key='id'),
'accounttype': self.get_account_type(),
'networkdomain': self.module.params.get('network_domain'),
'username': self.module.params.get('username'),
'timezone': self.module.params.get('timezone'),
'roleid': self.get_role_id()
}
if not self.module.check_mode:
res = self.query_api('ldapCreateAccount', **args)
account = res['account']
args = {
'account': self.module.params.get('name'),
'domainid': self.get_domain(key='id'),
'accounttype': self.get_account_type(),
'ldapdomain': self.module.params.get('ldap_domain'),
'type': self.module.params.get('ldap_type')
}
self.query_api('linkAccountToLdap', **args)
return account
def create_account(self, account):
args = {
'account': self.module.params.get('name'),
'domainid': self.get_domain(key='id'),
'accounttype': self.get_account_type(),
'networkdomain': self.module.params.get('network_domain'),
'username': self.module.params.get('username'),
'password': self.module.params.get('password'),
'firstname': self.module.params.get('first_name'),
'lastname': self.module.params.get('last_name'),
'email': self.module.params.get('email'),
'timezone': self.module.params.get('timezone'),
'roleid': self.get_role_id()
}
if not self.module.check_mode:
res = self.query_api('createAccount', **args)
account = res['account']
return account
def absent_account(self):
account = self.get_account()
if account:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('deleteAccount', id=account['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'account')
return account
def get_result(self, account):
super(AnsibleCloudStackAccount, self).get_result(account)
if account:
if 'accounttype' in account:
for key, value in self.account_types.items():
if value == account['accounttype']:
self.result['account_type'] = key
break
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
state=dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'),
account_type=dict(choices=['user', 'root_admin', 'domain_admin'], default='user'),
network_domain=dict(),
domain=dict(default='ROOT'),
email=dict(),
first_name=dict(),
last_name=dict(),
username=dict(),
password=dict(no_log=True),
timezone=dict(),
role=dict(),
ldap_domain=dict(),
ldap_type=dict(choices=['GROUP', 'OU'], default='GROUP'),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_acc = AnsibleCloudStackAccount(module)
state = module.params.get('state')
if state in ['absent']:
account = acs_acc.absent_account()
elif state in ['enabled', 'unlocked']:
account = acs_acc.enable_account()
elif state in ['disabled']:
account = acs_acc.disable_account()
elif state in ['locked']:
account = acs_acc.lock_account()
else:
account = acs_acc.present_account()
result = acs_acc.get_result(account)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,235 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_affinitygroup
short_description: Manages affinity groups on Apache CloudStack based clouds.
description:
- Create and remove affinity groups.
author: René Moser (@resmo)
options:
name:
description:
- Name of the affinity group.
type: str
required: true
affinity_type:
description:
- Type of the affinity group. If not specified, first found affinity type is used.
type: str
description:
description:
- Description of the affinity group.
type: str
state:
description:
- State of the affinity group.
type: str
choices: [ present, absent ]
default: present
domain:
description:
- Domain the affinity group is related to.
type: str
account:
description:
- Account the affinity group is related to.
type: str
project:
description:
- Name of the project the affinity group is related to.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a affinity group
cs_affinitygroup:
name: haproxy
affinity_type: host anti-affinity
delegate_to: localhost
- name: Remove a affinity group
cs_affinitygroup:
name: haproxy
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the affinity group.
returned: success
type: str
sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8
name:
description: Name of affinity group.
returned: success
type: str
sample: app
description:
description: Description of affinity group.
returned: success
type: str
sample: application affinity group
affinity_type:
description: Type of affinity group.
returned: success
type: str
sample: host anti-affinity
project:
description: Name of project the affinity group is related to.
returned: success
type: str
sample: Production
domain:
description: Domain the affinity group is related to.
returned: success
type: str
sample: example domain
account:
description: Account the affinity group is related to.
returned: success
type: str
sample: example account
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackAffinityGroup(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackAffinityGroup, self).__init__(module)
self.returns = {
'type': 'affinity_type',
}
self.affinity_group = None
def get_affinity_group(self):
if not self.affinity_group:
args = {
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'name': self.module.params.get('name'),
}
affinity_groups = self.query_api('listAffinityGroups', **args)
if affinity_groups:
self.affinity_group = affinity_groups['affinitygroup'][0]
return self.affinity_group
def get_affinity_type(self):
affinity_type = self.module.params.get('affinity_type')
affinity_types = self.query_api('listAffinityGroupTypes', )
if affinity_types:
if not affinity_type:
return affinity_types['affinityGroupType'][0]['type']
for a in affinity_types['affinityGroupType']:
if a['type'] == affinity_type:
return a['type']
self.module.fail_json(msg="affinity group type not found: %s" % affinity_type)
def create_affinity_group(self):
affinity_group = self.get_affinity_group()
if not affinity_group:
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'type': self.get_affinity_type(),
'description': self.module.params.get('description'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
}
if not self.module.check_mode:
res = self.query_api('createAffinityGroup', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
affinity_group = self.poll_job(res, 'affinitygroup')
return affinity_group
def remove_affinity_group(self):
affinity_group = self.get_affinity_group()
if affinity_group:
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
}
if not self.module.check_mode:
res = self.query_api('deleteAffinityGroup', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
self.poll_job(res, 'affinitygroup')
return affinity_group
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
affinity_type=dict(),
description=dict(),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_ag = AnsibleCloudStackAffinityGroup(module)
state = module.params.get('state')
if state in ['absent']:
affinity_group = acs_ag.remove_affinity_group()
else:
affinity_group = acs_ag.create_affinity_group()
result = acs_ag.get_result(affinity_group)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,393 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_cluster
short_description: Manages host clusters on Apache CloudStack based clouds.
description:
- Create, update and remove clusters.
author: René Moser (@resmo)
options:
name:
description:
- name of the cluster.
type: str
required: true
zone:
description:
- Name of the zone in which the cluster belongs to.
- If not set, default zone is used.
type: str
pod:
description:
- Name of the pod in which the cluster belongs to.
type: str
cluster_type:
description:
- Type of the cluster.
- Required if I(state=present)
type: str
choices: [ CloudManaged, ExternalManaged ]
hypervisor:
description:
- Name the hypervisor to be used.
- Required if I(state=present).
- Possible values are C(KVM), C(VMware), C(BareMetal), C(XenServer), C(LXC), C(HyperV), C(UCS), C(OVM), C(Simulator).
type: str
url:
description:
- URL for the cluster
type: str
username:
description:
- Username for the cluster.
type: str
password:
description:
- Password for the cluster.
type: str
guest_vswitch_name:
description:
- Name of virtual switch used for guest traffic in the cluster.
- This would override zone wide traffic label setting.
type: str
guest_vswitch_type:
description:
- Type of virtual switch used for guest traffic in the cluster.
- Allowed values are, vmwaresvs (for VMware standard vSwitch) and vmwaredvs (for VMware distributed vSwitch)
type: str
choices: [ vmwaresvs, vmwaredvs ]
public_vswitch_name:
description:
- Name of virtual switch used for public traffic in the cluster.
- This would override zone wide traffic label setting.
type: str
public_vswitch_type:
description:
- Type of virtual switch used for public traffic in the cluster.
- Allowed values are, vmwaresvs (for VMware standard vSwitch) and vmwaredvs (for VMware distributed vSwitch)
type: str
choices: [ vmwaresvs, vmwaredvs ]
vms_ip_address:
description:
- IP address of the VSM associated with this cluster.
type: str
vms_username:
description:
- Username for the VSM associated with this cluster.
type: str
vms_password:
description:
- Password for the VSM associated with this cluster.
type: str
ovm3_cluster:
description:
- Ovm3 native OCFS2 clustering enabled for cluster.
type: str
ovm3_pool:
description:
- Ovm3 native pooling enabled for cluster.
type: str
ovm3_vip:
description:
- Ovm3 vip to use for pool (and cluster).
type: str
state:
description:
- State of the cluster.
type: str
choices: [ present, absent, disabled, enabled ]
default: present
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a cluster is present
cs_cluster:
name: kvm-cluster-01
zone: ch-zrh-ix-01
hypervisor: KVM
cluster_type: CloudManaged
delegate_to: localhost
- name: Ensure a cluster is disabled
cs_cluster:
name: kvm-cluster-01
zone: ch-zrh-ix-01
state: disabled
delegate_to: localhost
- name: Ensure a cluster is enabled
cs_cluster:
name: kvm-cluster-01
zone: ch-zrh-ix-01
state: enabled
delegate_to: localhost
- name: Ensure a cluster is absent
cs_cluster:
name: kvm-cluster-01
zone: ch-zrh-ix-01
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the cluster.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the cluster.
returned: success
type: str
sample: cluster01
allocation_state:
description: State of the cluster.
returned: success
type: str
sample: Enabled
cluster_type:
description: Type of the cluster.
returned: success
type: str
sample: ExternalManaged
cpu_overcommit_ratio:
description: The CPU overcommit ratio of the cluster.
returned: success
type: str
sample: 1.0
memory_overcommit_ratio:
description: The memory overcommit ratio of the cluster.
returned: success
type: str
sample: 1.0
managed_state:
description: Whether this cluster is managed by CloudStack.
returned: success
type: str
sample: Managed
ovm3_vip:
description: Ovm3 VIP to use for pooling and/or clustering
returned: success
type: str
sample: 10.10.10.101
hypervisor:
description: Hypervisor of the cluster
returned: success
type: str
sample: VMware
zone:
description: Name of zone the cluster is in.
returned: success
type: str
sample: ch-gva-2
pod:
description: Name of pod the cluster is in.
returned: success
type: str
sample: pod01
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackCluster(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackCluster, self).__init__(module)
self.returns = {
'allocationstate': 'allocation_state',
'hypervisortype': 'hypervisor',
'clustertype': 'cluster_type',
'podname': 'pod',
'managedstate': 'managed_state',
'memoryovercommitratio': 'memory_overcommit_ratio',
'cpuovercommitratio': 'cpu_overcommit_ratio',
'ovm3vip': 'ovm3_vip',
}
self.cluster = None
def _get_common_cluster_args(self):
args = {
'clustername': self.module.params.get('name'),
'hypervisor': self.module.params.get('hypervisor'),
'clustertype': self.module.params.get('cluster_type'),
}
state = self.module.params.get('state')
if state in ['enabled', 'disabled']:
args['allocationstate'] = state.capitalize()
return args
def get_pod(self, key=None):
args = {
'name': self.module.params.get('pod'),
'zoneid': self.get_zone(key='id'),
}
pods = self.query_api('listPods', **args)
if pods:
return self._get_by_key(key, pods['pod'][0])
self.module.fail_json(msg="Pod %s not found in zone %s" % (self.module.params.get('pod'), self.get_zone(key='name')))
def get_cluster(self):
if not self.cluster:
args = {}
uuid = self.module.params.get('id')
if uuid:
args['id'] = uuid
clusters = self.query_api('listClusters', **args)
if clusters:
self.cluster = clusters['cluster'][0]
return self.cluster
args['name'] = self.module.params.get('name')
clusters = self.query_api('listClusters', **args)
if clusters:
self.cluster = clusters['cluster'][0]
# fix different return from API then request argument given
self.cluster['hypervisor'] = self.cluster['hypervisortype']
self.cluster['clustername'] = self.cluster['name']
return self.cluster
def present_cluster(self):
cluster = self.get_cluster()
if cluster:
cluster = self._update_cluster()
else:
cluster = self._create_cluster()
return cluster
def _create_cluster(self):
required_params = [
'cluster_type',
'hypervisor',
]
self.module.fail_on_missing_params(required_params=required_params)
args = self._get_common_cluster_args()
args['zoneid'] = self.get_zone(key='id')
args['podid'] = self.get_pod(key='id')
args['url'] = self.module.params.get('url')
args['username'] = self.module.params.get('username')
args['password'] = self.module.params.get('password')
args['guestvswitchname'] = self.module.params.get('guest_vswitch_name')
args['guestvswitchtype'] = self.module.params.get('guest_vswitch_type')
args['publicvswitchtype'] = self.module.params.get('public_vswitch_name')
args['publicvswitchtype'] = self.module.params.get('public_vswitch_type')
args['vsmipaddress'] = self.module.params.get('vms_ip_address')
args['vsmusername'] = self.module.params.get('vms_username')
args['vmspassword'] = self.module.params.get('vms_password')
args['ovm3cluster'] = self.module.params.get('ovm3_cluster')
args['ovm3pool'] = self.module.params.get('ovm3_pool')
args['ovm3vip'] = self.module.params.get('ovm3_vip')
self.result['changed'] = True
cluster = None
if not self.module.check_mode:
res = self.query_api('addCluster', **args)
# API returns a list as result CLOUDSTACK-9205
if isinstance(res['cluster'], list):
cluster = res['cluster'][0]
else:
cluster = res['cluster']
return cluster
def _update_cluster(self):
cluster = self.get_cluster()
args = self._get_common_cluster_args()
args['id'] = cluster['id']
if self.has_changed(args, cluster):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateCluster', **args)
cluster = res['cluster']
return cluster
def absent_cluster(self):
cluster = self.get_cluster()
if cluster:
self.result['changed'] = True
args = {
'id': cluster['id'],
}
if not self.module.check_mode:
self.query_api('deleteCluster', **args)
return cluster
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
zone=dict(),
pod=dict(),
cluster_type=dict(choices=['CloudManaged', 'ExternalManaged']),
hypervisor=dict(),
state=dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'),
url=dict(),
username=dict(),
password=dict(no_log=True),
guest_vswitch_name=dict(),
guest_vswitch_type=dict(choices=['vmwaresvs', 'vmwaredvs']),
public_vswitch_name=dict(),
public_vswitch_type=dict(choices=['vmwaresvs', 'vmwaredvs']),
vms_ip_address=dict(),
vms_username=dict(),
vms_password=dict(no_log=True),
ovm3_cluster=dict(),
ovm3_pool=dict(),
ovm3_vip=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_cluster = AnsibleCloudStackCluster(module)
state = module.params.get('state')
if state in ['absent']:
cluster = acs_cluster.absent_cluster()
else:
cluster = acs_cluster.present_cluster()
result = acs_cluster.get_result(cluster)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,277 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_configuration
short_description: Manages configuration on Apache CloudStack based clouds.
description:
- Manages global, zone, account, storage and cluster configurations.
author: René Moser (@resmo)
options:
name:
description:
- Name of the configuration.
type: str
required: true
value:
description:
- Value of the configuration.
type: str
required: true
account:
description:
- Ensure the value for corresponding account.
type: str
domain:
description:
- Domain the account is related to.
- Only considered if I(account) is used.
type: str
default: ROOT
zone:
description:
- Ensure the value for corresponding zone.
type: str
storage:
description:
- Ensure the value for corresponding storage pool.
type: str
cluster:
description:
- Ensure the value for corresponding cluster.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure global configuration
cs_configuration:
name: router.reboot.when.outofband.migrated
value: false
delegate_to: localhost
- name: Ensure zone configuration
cs_configuration:
name: router.reboot.when.outofband.migrated
zone: ch-gva-01
value: true
delegate_to: localhost
- name: Ensure storage configuration
cs_configuration:
name: storage.overprovisioning.factor
storage: storage01
value: 2.0
delegate_to: localhost
- name: Ensure account configuration
cs_configuration:
name: allow.public.user.templates
value: false
account: acme inc
domain: customers
delegate_to: localhost
'''
RETURN = '''
---
category:
description: Category of the configuration.
returned: success
type: str
sample: Advanced
scope:
description: Scope (zone/cluster/storagepool/account) of the parameter that needs to be updated.
returned: success
type: str
sample: storagepool
description:
description: Description of the configuration.
returned: success
type: str
sample: Setup the host to do multipath
name:
description: Name of the configuration.
returned: success
type: str
sample: zone.vlan.capacity.notificationthreshold
value:
description: Value of the configuration.
returned: success
type: str
sample: "0.75"
account:
description: Account of the configuration.
returned: success
type: str
sample: admin
Domain:
description: Domain of account of the configuration.
returned: success
type: str
sample: ROOT
zone:
description: Zone of the configuration.
returned: success
type: str
sample: ch-gva-01
cluster:
description: Cluster of the configuration.
returned: success
type: str
sample: cluster01
storage:
description: Storage of the configuration.
returned: success
type: str
sample: storage01
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackConfiguration(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackConfiguration, self).__init__(module)
self.returns = {
'category': 'category',
'scope': 'scope',
'value': 'value',
}
self.storage = None
self.account = None
self.cluster = None
def _get_common_configuration_args(self):
args = {
'name': self.module.params.get('name'),
'accountid': self.get_account(key='id'),
'storageid': self.get_storage(key='id'),
'zoneid': self.get_zone(key='id'),
'clusterid': self.get_cluster(key='id'),
}
return args
def get_zone(self, key=None):
# make sure we do net use the default zone
zone = self.module.params.get('zone')
if zone:
return super(AnsibleCloudStackConfiguration, self).get_zone(key=key)
def get_cluster(self, key=None):
if not self.cluster:
cluster_name = self.module.params.get('cluster')
if not cluster_name:
return None
args = {
'name': cluster_name,
}
clusters = self.query_api('listClusters', **args)
if clusters:
self.cluster = clusters['cluster'][0]
self.result['cluster'] = self.cluster['name']
else:
self.module.fail_json(msg="Cluster %s not found." % cluster_name)
return self._get_by_key(key=key, my_dict=self.cluster)
def get_storage(self, key=None):
if not self.storage:
storage_pool_name = self.module.params.get('storage')
if not storage_pool_name:
return None
args = {
'name': storage_pool_name,
}
storage_pools = self.query_api('listStoragePools', **args)
if storage_pools:
self.storage = storage_pools['storagepool'][0]
self.result['storage'] = self.storage['name']
else:
self.module.fail_json(msg="Storage pool %s not found." % storage_pool_name)
return self._get_by_key(key=key, my_dict=self.storage)
def get_configuration(self):
configuration = None
args = self._get_common_configuration_args()
args['fetch_list'] = True
configurations = self.query_api('listConfigurations', **args)
if not configurations:
self.module.fail_json(msg="Configuration %s not found." % args['name'])
for config in configurations:
if args['name'] == config['name']:
configuration = config
return configuration
def get_value(self):
value = str(self.module.params.get('value'))
if value in ('True', 'False'):
value = value.lower()
return value
def present_configuration(self):
configuration = self.get_configuration()
args = self._get_common_configuration_args()
args['value'] = self.get_value()
if self.has_changed(args, configuration, ['value']):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateConfiguration', **args)
configuration = res['configuration']
return configuration
def get_result(self, configuration):
self.result = super(AnsibleCloudStackConfiguration, self).get_result(configuration)
if self.account:
self.result['account'] = self.account['name']
self.result['domain'] = self.domain['path']
elif self.zone:
self.result['zone'] = self.zone['name']
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
value=dict(type='str', required=True),
zone=dict(),
storage=dict(),
cluster=dict(),
account=dict(),
domain=dict(default='ROOT')
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_configuration = AnsibleCloudStackConfiguration(module)
configuration = acs_configuration.present_configuration()
result = acs_configuration.get_result(configuration)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,381 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2018, David Passante <@dpassante>
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_disk_offering
description:
- Create and delete disk offerings for guest VMs.
- Update display_text or display_offering of existing disk offering.
short_description: Manages disk offerings on Apache CloudStack based clouds.
author:
- David Passante (@dpassante)
- René Moser (@resmo)
options:
disk_size:
description:
- Size of the disk offering in GB (1GB = 1,073,741,824 bytes).
type: int
bytes_read_rate:
description:
- Bytes read rate of the disk offering.
type: int
bytes_write_rate:
description:
- Bytes write rate of the disk offering.
type: int
display_text:
description:
- Display text of the disk offering.
- If not set, C(name) will be used as C(display_text) while creating.
type: str
domain:
description:
- Domain the disk offering is related to.
- Public for all domains and subdomains if not set.
type: str
hypervisor_snapshot_reserve:
description:
- Hypervisor snapshot reserve space as a percent of a volume.
- Only for managed storage using Xen or VMware.
type: int
customized:
description:
- Whether disk offering iops is custom or not.
type: bool
default: no
iops_read_rate:
description:
- IO requests read rate of the disk offering.
type: int
iops_write_rate:
description:
- IO requests write rate of the disk offering.
type: int
iops_max:
description:
- Max. iops of the disk offering.
type: int
iops_min:
description:
- Min. iops of the disk offering.
type: int
name:
description:
- Name of the disk offering.
type: str
required: true
provisioning_type:
description:
- Provisioning type used to create volumes.
type: str
choices: [ thin, sparse, fat ]
state:
description:
- State of the disk offering.
type: str
choices: [ present, absent ]
default: present
storage_type:
description:
- The storage type of the disk offering.
type: str
choices: [ local, shared ]
storage_tags:
description:
- The storage tags for this disk offering.
type: list
aliases: [ storage_tag ]
display_offering:
description:
- An optional field, whether to display the offering to the end user or not.
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a disk offering with local storage
cs_disk_offering:
name: small
display_text: Small 10GB
disk_size: 10
storage_type: local
delegate_to: localhost
- name: Create or update a disk offering with shared storage
cs_disk_offering:
name: small
display_text: Small 10GB
disk_size: 10
storage_type: shared
storage_tags: SAN01
delegate_to: localhost
- name: Remove a disk offering
cs_disk_offering:
name: small
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the disk offering
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
disk_size:
description: Size of the disk offering in GB
returned: success
type: int
sample: 10
iops_max:
description: Max iops of the disk offering
returned: success
type: int
sample: 1000
iops_min:
description: Min iops of the disk offering
returned: success
type: int
sample: 500
bytes_read_rate:
description: Bytes read rate of the disk offering
returned: success
type: int
sample: 1000
bytes_write_rate:
description: Bytes write rate of the disk offering
returned: success
type: int
sample: 1000
iops_read_rate:
description: IO requests per second read rate of the disk offering
returned: success
type: int
sample: 1000
iops_write_rate:
description: IO requests per second write rate of the disk offering
returned: success
type: int
sample: 1000
created:
description: Date the offering was created
returned: success
type: str
sample: 2017-11-19T10:48:59+0000
display_text:
description: Display text of the offering
returned: success
type: str
sample: Small 10GB
domain:
description: Domain the offering is into
returned: success
type: str
sample: ROOT
storage_tags:
description: List of storage tags
returned: success
type: list
sample: [ 'eco' ]
customized:
description: Whether the offering uses custom IOPS or not
returned: success
type: bool
sample: false
name:
description: Name of the system offering
returned: success
type: str
sample: Micro
provisioning_type:
description: Provisioning type used to create volumes
returned: success
type: str
sample: thin
storage_type:
description: Storage type used to create volumes
returned: success
type: str
sample: shared
display_offering:
description: Whether to display the offering to the end user or not.
returned: success
type: bool
sample: false
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackDiskOffering(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackDiskOffering, self).__init__(module)
self.returns = {
'disksize': 'disk_size',
'diskBytesReadRate': 'bytes_read_rate',
'diskBytesWriteRate': 'bytes_write_rate',
'diskIopsReadRate': 'iops_read_rate',
'diskIopsWriteRate': 'iops_write_rate',
'maxiops': 'iops_max',
'miniops': 'iops_min',
'hypervisorsnapshotreserve': 'hypervisor_snapshot_reserve',
'customized': 'customized',
'provisioningtype': 'provisioning_type',
'storagetype': 'storage_type',
'tags': 'storage_tags',
'displayoffering': 'display_offering',
}
self.disk_offering = None
def get_disk_offering(self):
args = {
'name': self.module.params.get('name'),
'domainid': self.get_domain(key='id'),
}
disk_offerings = self.query_api('listDiskOfferings', **args)
if disk_offerings:
for disk_offer in disk_offerings['diskoffering']:
if args['name'] == disk_offer['name']:
self.disk_offering = disk_offer
return self.disk_offering
def present_disk_offering(self):
disk_offering = self.get_disk_offering()
if not disk_offering:
disk_offering = self._create_offering(disk_offering)
else:
disk_offering = self._update_offering(disk_offering)
return disk_offering
def absent_disk_offering(self):
disk_offering = self.get_disk_offering()
if disk_offering:
self.result['changed'] = True
if not self.module.check_mode:
args = {
'id': disk_offering['id'],
}
self.query_api('deleteDiskOffering', **args)
return disk_offering
def _create_offering(self, disk_offering):
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'disksize': self.module.params.get('disk_size'),
'bytesreadrate': self.module.params.get('bytes_read_rate'),
'byteswriterate': self.module.params.get('bytes_write_rate'),
'customized': self.module.params.get('customized'),
'domainid': self.get_domain(key='id'),
'hypervisorsnapshotreserve': self.module.params.get('hypervisor_snapshot_reserve'),
'iopsreadrate': self.module.params.get('iops_read_rate'),
'iopswriterate': self.module.params.get('iops_write_rate'),
'maxiops': self.module.params.get('iops_max'),
'miniops': self.module.params.get('iops_min'),
'provisioningtype': self.module.params.get('provisioning_type'),
'diskofferingdetails': self.module.params.get('disk_offering_details'),
'storagetype': self.module.params.get('storage_type'),
'tags': self.module.params.get('storage_tags'),
'displayoffering': self.module.params.get('display_offering'),
}
if not self.module.check_mode:
res = self.query_api('createDiskOffering', **args)
disk_offering = res['diskoffering']
return disk_offering
def _update_offering(self, disk_offering):
args = {
'id': disk_offering['id'],
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'displayoffering': self.module.params.get('display_offering'),
}
if self.has_changed(args, disk_offering):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateDiskOffering', **args)
disk_offering = res['diskoffering']
return disk_offering
def get_result(self, disk_offering):
super(AnsibleCloudStackDiskOffering, self).get_result(disk_offering)
if disk_offering:
# Prevent confusion, the api returns a tags key for storage tags.
if 'tags' in disk_offering:
self.result['storage_tags'] = disk_offering['tags'].split(',') or [disk_offering['tags']]
if 'tags' in self.result:
del self.result['tags']
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
domain=dict(),
disk_size=dict(type='int'),
display_offering=dict(type='bool'),
hypervisor_snapshot_reserve=dict(type='int'),
bytes_read_rate=dict(type='int'),
bytes_write_rate=dict(type='int'),
customized=dict(type='bool'),
iops_read_rate=dict(type='int'),
iops_write_rate=dict(type='int'),
iops_max=dict(type='int'),
iops_min=dict(type='int'),
provisioning_type=dict(choices=['thin', 'sparse', 'fat']),
storage_type=dict(choices=['local', 'shared']),
storage_tags=dict(type='list', aliases=['storage_tag']),
state=dict(choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_do = AnsibleCloudStackDiskOffering(module)
state = module.params.get('state')
if state == "absent":
disk_offering = acs_do.absent_disk_offering()
else:
disk_offering = acs_do.present_disk_offering()
result = acs_do.get_result(disk_offering)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,251 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_domain
short_description: Manages domains on Apache CloudStack based clouds.
description:
- Create, update and remove domains.
author: René Moser (@resmo)
options:
path:
description:
- Path of the domain.
- Prefix C(ROOT/) or C(/ROOT/) in path is optional.
type: str
required: true
network_domain:
description:
- Network domain for networks in the domain.
type: str
clean_up:
description:
- Clean up all domain resources like child domains and accounts.
- Considered on I(state=absent).
type: bool
default: no
state:
description:
- State of the domain.
type: str
choices: [ present, absent ]
default: present
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a domain
cs_domain:
path: ROOT/customers
network_domain: customers.example.com
delegate_to: localhost
- name: Create another subdomain
cs_domain:
path: ROOT/customers/xy
network_domain: xy.customers.example.com
delegate_to: localhost
- name: Remove a domain
cs_domain:
path: ROOT/customers/xy
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the domain.
returned: success
type: str
sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8
name:
description: Name of the domain.
returned: success
type: str
sample: customers
path:
description: Domain path.
returned: success
type: str
sample: /ROOT/customers
parent_domain:
description: Parent domain of the domain.
returned: success
type: str
sample: ROOT
network_domain:
description: Network domain of the domain.
returned: success
type: str
sample: example.local
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackDomain(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackDomain, self).__init__(module)
self.returns = {
'path': 'path',
'networkdomain': 'network_domain',
'parentdomainname': 'parent_domain',
}
self.domain = None
def _get_domain_internal(self, path=None):
if not path:
path = self.module.params.get('path')
if path.endswith('/'):
self.module.fail_json(msg="Path '%s' must not end with /" % path)
path = path.lower()
if path.startswith('/') and not path.startswith('/root/'):
path = "root" + path
elif not path.startswith('root/'):
path = "root/" + path
args = {
'listall': True,
'fetch_list': True,
}
domains = self.query_api('listDomains', **args)
if domains:
for d in domains:
if path == d['path'].lower():
return d
return None
def get_name(self):
# last part of the path is the name
name = self.module.params.get('path').split('/')[-1:]
return name
def get_domain(self, key=None):
if not self.domain:
self.domain = self._get_domain_internal()
return self._get_by_key(key, self.domain)
def get_parent_domain(self, key=None):
path = self.module.params.get('path')
# cut off last /*
path = '/'.join(path.split('/')[:-1])
if not path:
return None
parent_domain = self._get_domain_internal(path=path)
if not parent_domain:
self.module.fail_json(msg="Parent domain path %s does not exist" % path)
return self._get_by_key(key, parent_domain)
def present_domain(self):
domain = self.get_domain()
if not domain:
domain = self.create_domain(domain)
else:
domain = self.update_domain(domain)
return domain
def create_domain(self, domain):
self.result['changed'] = True
args = {
'name': self.get_name(),
'parentdomainid': self.get_parent_domain(key='id'),
'networkdomain': self.module.params.get('network_domain')
}
if not self.module.check_mode:
res = self.query_api('createDomain', **args)
domain = res['domain']
return domain
def update_domain(self, domain):
args = {
'id': domain['id'],
'networkdomain': self.module.params.get('network_domain')
}
if self.has_changed(args, domain):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateDomain', **args)
domain = res['domain']
return domain
def absent_domain(self):
domain = self.get_domain()
if domain:
self.result['changed'] = True
if not self.module.check_mode:
args = {
'id': domain['id'],
'cleanup': self.module.params.get('clean_up')
}
res = self.query_api('deleteDomain', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
res = self.poll_job(res, 'domain')
return domain
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
path=dict(required=True),
state=dict(choices=['present', 'absent'], default='present'),
network_domain=dict(),
clean_up=dict(type='bool', default=False),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_dom = AnsibleCloudStackDomain(module)
state = module.params.get('state')
if state in ['absent']:
domain = acs_dom.absent_domain()
else:
domain = acs_dom.present_domain()
result = acs_dom.get_result(domain)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,234 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_facts
short_description: Gather facts on instances of Apache CloudStack based clouds.
description:
- This module fetches data from the metadata API in CloudStack. The module must be called from within the instance itself.
author: René Moser (@resmo)
options:
filter:
description:
- Filter for a specific fact.
type: str
choices:
- cloudstack_service_offering
- cloudstack_availability_zone
- cloudstack_public_hostname
- cloudstack_public_ipv4
- cloudstack_local_hostname
- cloudstack_local_ipv4
- cloudstack_instance_id
- cloudstack_user_data
meta_data_host:
description:
- Host or IP of the meta data API service.
- If not set, determination by parsing the dhcp lease file.
type: str
requirements: [ yaml ]
'''
EXAMPLES = '''
# Gather all facts on instances
- name: Gather cloudstack facts
cs_facts:
# Gather specific fact on instances
- name: Gather cloudstack facts
cs_facts: filter=cloudstack_instance_id
# Gather specific fact on instances with a given meta_data_host
- name: Gather cloudstack facts
cs_facts:
filter: cloudstack_instance_id
meta_data_host: 169.254.169.254
'''
RETURN = '''
---
cloudstack_availability_zone:
description: zone the instance is deployed in.
returned: success
type: str
sample: ch-gva-2
cloudstack_instance_id:
description: UUID of the instance.
returned: success
type: str
sample: ab4e80b0-3e7e-4936-bdc5-e334ba5b0139
cloudstack_local_hostname:
description: local hostname of the instance.
returned: success
type: str
sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139
cloudstack_local_ipv4:
description: local IPv4 of the instance.
returned: success
type: str
sample: 185.19.28.35
cloudstack_public_hostname:
description: public IPv4 of the router. Same as I(cloudstack_public_ipv4).
returned: success
type: str
sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139
cloudstack_public_ipv4:
description: public IPv4 of the router.
returned: success
type: str
sample: 185.19.28.35
cloudstack_service_offering:
description: service offering of the instance.
returned: success
type: str
sample: Micro 512mb 1cpu
cloudstack_user_data:
description: data of the instance provided by users.
returned: success
type: dict
sample: { "bla": "foo" }
'''
import os
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.facts import ansible_collector, default_collectors
YAML_IMP_ERR = None
try:
import yaml
HAS_LIB_YAML = True
except ImportError:
YAML_IMP_ERR = traceback.format_exc()
HAS_LIB_YAML = False
CS_METADATA_BASE_URL = "http://%s/latest/meta-data"
CS_USERDATA_BASE_URL = "http://%s/latest/user-data"
class CloudStackFacts(object):
def __init__(self):
collector = ansible_collector.get_ansible_collector(all_collector_classes=default_collectors.collectors,
filter_spec='default_ipv4',
gather_subset=['!all', 'network'],
gather_timeout=10)
self.facts = collector.collect(module)
self.api_ip = None
self.fact_paths = {
'cloudstack_service_offering': 'service-offering',
'cloudstack_availability_zone': 'availability-zone',
'cloudstack_public_hostname': 'public-hostname',
'cloudstack_public_ipv4': 'public-ipv4',
'cloudstack_local_hostname': 'local-hostname',
'cloudstack_local_ipv4': 'local-ipv4',
'cloudstack_instance_id': 'instance-id'
}
def run(self):
result = {}
filter = module.params.get('filter')
if not filter:
for key, path in self.fact_paths.items():
result[key] = self._fetch(CS_METADATA_BASE_URL + "/" + path)
result['cloudstack_user_data'] = self._get_user_data_json()
else:
if filter == 'cloudstack_user_data':
result['cloudstack_user_data'] = self._get_user_data_json()
elif filter in self.fact_paths:
result[filter] = self._fetch(CS_METADATA_BASE_URL + "/" + self.fact_paths[filter])
return result
def _get_user_data_json(self):
try:
# this data come form users, we try what we can to parse it...
return yaml.safe_load(self._fetch(CS_USERDATA_BASE_URL))
except Exception:
return None
def _fetch(self, path):
api_ip = self._get_api_ip()
if not api_ip:
return None
api_url = path % api_ip
(response, info) = fetch_url(module, api_url, force=True)
if response:
data = response.read()
else:
data = None
return data
def _get_dhcp_lease_file(self):
"""Return the path of the lease file."""
default_iface = self.facts['default_ipv4']['interface']
dhcp_lease_file_locations = [
'/var/lib/dhcp/dhclient.%s.leases' % default_iface, # debian / ubuntu
'/var/lib/dhclient/dhclient-%s.leases' % default_iface, # centos 6
'/var/lib/dhclient/dhclient--%s.lease' % default_iface, # centos 7
'/var/db/dhclient.leases.%s' % default_iface, # openbsd
]
for file_path in dhcp_lease_file_locations:
if os.path.exists(file_path):
return file_path
module.fail_json(msg="Could not find dhclient leases file.")
def _get_api_ip(self):
"""Return the IP of the DHCP server."""
if module.params.get('meta_data_host'):
return module.params.get('meta_data_host')
elif not self.api_ip:
dhcp_lease_file = self._get_dhcp_lease_file()
for line in open(dhcp_lease_file):
if 'dhcp-server-identifier' in line:
# get IP of string "option dhcp-server-identifier 185.19.28.176;"
line = line.translate(None, ';')
self.api_ip = line.split()[2]
break
if not self.api_ip:
module.fail_json(msg="No dhcp-server-identifier found in leases file.")
return self.api_ip
def main():
global module
module = AnsibleModule(
argument_spec=dict(
filter=dict(default=None, choices=[
'cloudstack_service_offering',
'cloudstack_availability_zone',
'cloudstack_public_hostname',
'cloudstack_public_ipv4',
'cloudstack_local_hostname',
'cloudstack_local_ipv4',
'cloudstack_instance_id',
'cloudstack_user_data',
]),
meta_data_host=dict(),
),
supports_check_mode=True
)
if not HAS_LIB_YAML:
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
cs_facts = CloudStackFacts().run()
cs_facts_result = dict(changed=False, ansible_facts=cs_facts)
module.exit_json(**cs_facts_result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,448 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_firewall
short_description: Manages firewall rules on Apache CloudStack based clouds.
description:
- Creates and removes firewall rules.
author: René Moser (@resmo)
options:
ip_address:
description:
- Public IP address the ingress rule is assigned to.
- Required if I(type=ingress).
type: str
network:
description:
- Network the egress rule is related to.
- Required if I(type=egress).
type: str
state:
description:
- State of the firewall rule.
type: str
default: present
choices: [ present, absent ]
type:
description:
- Type of the firewall rule.
type: str
default: ingress
choices: [ ingress, egress ]
protocol:
description:
- Protocol of the firewall rule.
- C(all) is only available if I(type=egress).
type: str
default: tcp
choices: [ tcp, udp, icmp, all ]
cidrs:
description:
- List of CIDRs (full notation) to be used for firewall rule.
- Since version 2.5, it is a list of CIDR.
type: list
default: 0.0.0.0/0
aliases: [ cidr ]
start_port:
description:
- Start port for this rule.
- Considered if I(protocol=tcp) or I(protocol=udp).
type: int
aliases: [ port ]
end_port:
description:
- End port for this rule. Considered if I(protocol=tcp) or I(protocol=udp).
- If not specified, equal I(start_port).
type: int
icmp_type:
description:
- Type of the icmp message being sent.
- Considered if I(protocol=icmp).
type: int
icmp_code:
description:
- Error code for this icmp message.
- Considered if I(protocol=icmp).
type: int
domain:
description:
- Domain the firewall rule is related to.
type: str
account:
description:
- Account the firewall rule is related to.
type: str
project:
description:
- Name of the project the firewall rule is related to.
type: str
zone:
description:
- Name of the zone in which the virtual machine is in.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set an empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Allow inbound port 80/tcp from 1.2.3.4 to 4.3.2.1
cs_firewall:
ip_address: 4.3.2.1
port: 80
cidr: 1.2.3.4/32
delegate_to: localhost
- name: Allow inbound tcp/udp port 53 to 4.3.2.1
cs_firewall:
ip_address: 4.3.2.1
port: 53
protocol: '{{ item }}'
with_items:
- tcp
- udp
delegate_to: localhost
- name: Ensure firewall rule is removed
cs_firewall:
ip_address: 4.3.2.1
start_port: 8000
end_port: 8888
cidr: 17.0.0.0/8
state: absent
delegate_to: localhost
- name: Allow all outbound traffic
cs_firewall:
network: my_network
type: egress
protocol: all
delegate_to: localhost
- name: Allow only HTTP outbound traffic for an IP
cs_firewall:
network: my_network
type: egress
port: 80
cidr: 10.101.1.20
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the rule.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
ip_address:
description: IP address of the rule if C(type=ingress)
returned: success
type: str
sample: 10.100.212.10
type:
description: Type of the rule.
returned: success
type: str
sample: ingress
cidr:
description: CIDR string of the rule.
returned: success
type: str
sample: 0.0.0.0/0
cidrs:
description: CIDR list of the rule.
returned: success
type: list
sample: [ '0.0.0.0/0' ]
version_added: '2.5'
protocol:
description: Protocol of the rule.
returned: success
type: str
sample: tcp
start_port:
description: Start port of the rule.
returned: success
type: int
sample: 80
end_port:
description: End port of the rule.
returned: success
type: int
sample: 80
icmp_code:
description: ICMP code of the rule.
returned: success
type: int
sample: 1
icmp_type:
description: ICMP type of the rule.
returned: success
type: int
sample: 1
network:
description: Name of the network if C(type=egress)
returned: success
type: str
sample: my_network
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackFirewall(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackFirewall, self).__init__(module)
self.returns = {
'cidrlist': 'cidr',
'startport': 'start_port',
'endport': 'end_port',
'protocol': 'protocol',
'ipaddress': 'ip_address',
'icmpcode': 'icmp_code',
'icmptype': 'icmp_type',
}
self.firewall_rule = None
self.network = None
def get_firewall_rule(self):
if not self.firewall_rule:
cidrs = self.module.params.get('cidrs')
protocol = self.module.params.get('protocol')
start_port = self.module.params.get('start_port')
end_port = self.get_or_fallback('end_port', 'start_port')
icmp_code = self.module.params.get('icmp_code')
icmp_type = self.module.params.get('icmp_type')
fw_type = self.module.params.get('type')
if protocol in ['tcp', 'udp'] and not (start_port and end_port):
self.module.fail_json(msg="missing required argument for protocol '%s': start_port or end_port" % protocol)
if protocol == 'icmp' and not icmp_type:
self.module.fail_json(msg="missing required argument for protocol 'icmp': icmp_type")
if protocol == 'all' and fw_type != 'egress':
self.module.fail_json(msg="protocol 'all' could only be used for type 'egress'")
args = {
'account': self.get_account('name'),
'domainid': self.get_domain('id'),
'projectid': self.get_project('id'),
'fetch_list': True,
}
if fw_type == 'egress':
args['networkid'] = self.get_network(key='id')
if not args['networkid']:
self.module.fail_json(msg="missing required argument for type egress: network")
# CloudStack 4.11 use the network cidr for 0.0.0.0/0 in egress
# That is why we need to replace it.
network_cidr = self.get_network(key='cidr')
egress_cidrs = [network_cidr if cidr == '0.0.0.0/0' else cidr for cidr in cidrs]
firewall_rules = self.query_api('listEgressFirewallRules', **args)
else:
args['ipaddressid'] = self.get_ip_address('id')
if not args['ipaddressid']:
self.module.fail_json(msg="missing required argument for type ingress: ip_address")
egress_cidrs = None
firewall_rules = self.query_api('listFirewallRules', **args)
if firewall_rules:
for rule in firewall_rules:
type_match = self._type_cidrs_match(rule, cidrs, egress_cidrs)
protocol_match = (
self._tcp_udp_match(rule, protocol, start_port, end_port) or
self._icmp_match(rule, protocol, icmp_code, icmp_type) or
self._egress_all_match(rule, protocol, fw_type)
)
if type_match and protocol_match:
self.firewall_rule = rule
break
return self.firewall_rule
def _tcp_udp_match(self, rule, protocol, start_port, end_port):
return (
protocol in ['tcp', 'udp'] and
protocol == rule['protocol'] and
start_port == int(rule['startport']) and
end_port == int(rule['endport'])
)
def _egress_all_match(self, rule, protocol, fw_type):
return (
protocol in ['all'] and
protocol == rule['protocol'] and
fw_type == 'egress'
)
def _icmp_match(self, rule, protocol, icmp_code, icmp_type):
return (
protocol == 'icmp' and
protocol == rule['protocol'] and
icmp_code == rule['icmpcode'] and
icmp_type == rule['icmptype']
)
def _type_cidrs_match(self, rule, cidrs, egress_cidrs):
if egress_cidrs is not None:
return ",".join(egress_cidrs) == rule['cidrlist'] or ",".join(cidrs) == rule['cidrlist']
else:
return ",".join(cidrs) == rule['cidrlist']
def create_firewall_rule(self):
firewall_rule = self.get_firewall_rule()
if not firewall_rule:
self.result['changed'] = True
args = {
'cidrlist': self.module.params.get('cidrs'),
'protocol': self.module.params.get('protocol'),
'startport': self.module.params.get('start_port'),
'endport': self.get_or_fallback('end_port', 'start_port'),
'icmptype': self.module.params.get('icmp_type'),
'icmpcode': self.module.params.get('icmp_code')
}
fw_type = self.module.params.get('type')
if not self.module.check_mode:
if fw_type == 'egress':
args['networkid'] = self.get_network(key='id')
res = self.query_api('createEgressFirewallRule', **args)
else:
args['ipaddressid'] = self.get_ip_address('id')
res = self.query_api('createFirewallRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
firewall_rule = self.poll_job(res, 'firewallrule')
if firewall_rule:
firewall_rule = self.ensure_tags(resource=firewall_rule, resource_type='Firewallrule')
self.firewall_rule = firewall_rule
return firewall_rule
def remove_firewall_rule(self):
firewall_rule = self.get_firewall_rule()
if firewall_rule:
self.result['changed'] = True
args = {
'id': firewall_rule['id']
}
fw_type = self.module.params.get('type')
if not self.module.check_mode:
if fw_type == 'egress':
res = self.query_api('deleteEgressFirewallRule', **args)
else:
res = self.query_api('deleteFirewallRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'firewallrule')
return firewall_rule
def get_result(self, firewall_rule):
super(AnsibleCloudStackFirewall, self).get_result(firewall_rule)
if firewall_rule:
self.result['type'] = self.module.params.get('type')
if self.result['type'] == 'egress':
self.result['network'] = self.get_network(key='displaytext')
if 'cidrlist' in firewall_rule:
self.result['cidrs'] = firewall_rule['cidrlist'].split(',') or [firewall_rule['cidrlist']]
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
ip_address=dict(),
network=dict(),
cidrs=dict(type='list', default='0.0.0.0/0', aliases=['cidr']),
protocol=dict(choices=['tcp', 'udp', 'icmp', 'all'], default='tcp'),
type=dict(choices=['ingress', 'egress'], default='ingress'),
icmp_type=dict(type='int'),
icmp_code=dict(type='int'),
start_port=dict(type='int', aliases=['port']),
end_port=dict(type='int'),
state=dict(choices=['present', 'absent'], default='present'),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag'], default=None),
))
required_together = cs_required_together()
required_together.extend([
['icmp_type', 'icmp_code'],
])
module = AnsibleModule(
argument_spec=argument_spec,
required_together=required_together,
required_one_of=(
['ip_address', 'network'],
),
mutually_exclusive=(
['icmp_type', 'start_port'],
['icmp_type', 'end_port'],
['ip_address', 'network'],
),
supports_check_mode=True
)
acs_fw = AnsibleCloudStackFirewall(module)
state = module.params.get('state')
if state in ['absent']:
fw_rule = acs_fw.remove_firewall_rule()
else:
fw_rule = acs_fw.create_firewall_rule()
result = acs_fw.get_result(fw_rule)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,627 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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: cs_host
short_description: Manages hosts on Apache CloudStack based clouds.
description:
- Create, update and remove hosts.
author: René Moser (@resmo)
options:
name:
description:
- Name of the host.
type: str
required: true
aliases: [ ip_address ]
url:
description:
- Url of the host used to create a host.
- If not provided, C(http://) and param I(name) is used as url.
- Only considered if I(state=present) and host does not yet exist.
type: str
username:
description:
- Username for the host.
- Required if I(state=present) and host does not yet exist.
type: str
password:
description:
- Password for the host.
- Required if I(state=present) and host does not yet exist.
type: str
pod:
description:
- Name of the pod.
- Required if I(state=present) and host does not yet exist.
type: str
cluster:
description:
- Name of the cluster.
type: str
hypervisor:
description:
- Name of the cluster.
- Required if I(state=present) and host does not yet exist.
- Possible values are C(KVM), C(VMware), C(BareMetal), C(XenServer), C(LXC), C(HyperV), C(UCS), C(OVM), C(Simulator).
type: str
allocation_state:
description:
- Allocation state of the host.
type: str
choices: [ enabled, disabled, maintenance ]
host_tags:
description:
- Tags of the host.
type: list
aliases: [ host_tag ]
state:
description:
- State of the host.
type: str
default: present
choices: [ present, absent ]
zone:
description:
- Name of the zone in which the host should be deployed.
- If not set, default zone is used.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a host is present but disabled
cs_host:
name: pod01.zone01.example.com
cluster: vcenter.example.com/zone01/cluster01
pod: pod01
zone: zone01
hypervisor: VMware
allocation_state: disabled
host_tags:
- perf
- gpu
delegate_to: localhost
- name: Ensure an existing host is disabled
cs_host:
name: pod01.zone01.example.com
zone: zone01
allocation_state: disabled
delegate_to: localhost
- name: Ensure an existing host is enabled
cs_host:
name: pod01.zone01.example.com
zone: zone01
allocation_state: enabled
delegate_to: localhost
- name: Ensure a host is absent
cs_host:
name: pod01.zone01.example.com
zone: zone01
state: absent
delegate_to: localhost
'''
RETURN = '''
---
capabilities:
description: Capabilities of the host.
returned: success
type: str
sample: hvm
cluster:
description: Cluster of the host.
returned: success
type: str
sample: vcenter.example.com/zone/cluster01
cluster_type:
description: Type of the cluster of the host.
returned: success
type: str
sample: ExternalManaged
cpu_allocated:
description: Amount in percent of the host's CPU currently allocated.
returned: success
type: str
sample: 166.25%
cpu_number:
description: Number of CPUs of the host.
returned: success
type: str
sample: 24
cpu_sockets:
description: Number of CPU sockets of the host.
returned: success
type: int
sample: 2
cpu_speed:
description: CPU speed in Mhz
returned: success
type: int
sample: 1999
cpu_used:
description: Amount of the host's CPU currently used.
returned: success
type: str
sample: 33.6%
cpu_with_overprovisioning:
description: Amount of the host's CPU after applying the cpu.overprovisioning.factor.
returned: success
type: str
sample: 959520.0
created:
description: Date when the host was created.
returned: success
type: str
sample: 2015-05-03T15:05:51+0200
disconnected:
description: Date when the host was disconnected.
returned: success
type: str
sample: 2015-05-03T15:05:51+0200
disk_size_allocated:
description: Host's currently allocated disk size.
returned: success
type: int
sample: 2593
disk_size_total:
description: Total disk size of the host
returned: success
type: int
sample: 259300
events:
description: Events available for the host
returned: success
type: str
sample: "Ping; HostDown; AgentConnected; AgentDisconnected; PingTimeout; ShutdownRequested; Remove; StartAgentRebalance; ManagementServerDown"
ha_host:
description: Whether the host is a HA host.
returned: success
type: bool
sample: false
has_enough_capacity:
description: Whether the host has enough CPU and RAM capacity to migrate a VM to it.
returned: success
type: bool
sample: true
host_tags:
description: Comma-separated list of tags for the host.
returned: success
type: str
sample: "perf"
hypervisor:
description: Host's hypervisor.
returned: success
type: str
sample: VMware
hypervisor_version:
description: Hypervisor version.
returned: success
type: str
sample: 5.1
ip_address:
description: IP address of the host
returned: success
type: str
sample: 10.10.10.1
is_local_storage_active:
description: Whether the local storage is available or not.
returned: success
type: bool
sample: false
last_pinged:
description: Date and time the host was last pinged.
returned: success
type: str
sample: "1970-01-17T17:27:32+0100"
management_server_id:
description: Management server ID of the host.
returned: success
type: int
sample: 345050593418
memory_allocated:
description: Amount of the host's memory currently allocated.
returned: success
type: int
sample: 69793218560
memory_total:
description: Total of memory of the host.
returned: success
type: int
sample: 206085263360
memory_used:
description: Amount of the host's memory currently used.
returned: success
type: int
sample: 65504776192
name:
description: Name of the host.
returned: success
type: str
sample: esx32.example.com
network_kbs_read:
description: Incoming network traffic on the host.
returned: success
type: int
sample: 0
network_kbs_write:
description: Outgoing network traffic on the host.
returned: success
type: int
sample: 0
os_category:
description: OS category name of the host.
returned: success
type: str
sample: ...
out_of_band_management:
description: Host out-of-band management information.
returned: success
type: str
sample: ...
pod:
description: Pod name of the host.
returned: success
type: str
sample: Pod01
removed:
description: Date and time the host was removed.
returned: success
type: str
sample: "1970-01-17T17:27:32+0100"
resource_state:
description: Resource state of the host.
returned: success
type: str
sample: Enabled
allocation_state::
description: Allocation state of the host.
returned: success
type: str
sample: enabled
state:
description: State of the host.
returned: success
type: str
sample: Up
suitable_for_migration:
description: Whether this host is suitable (has enough capacity and satisfies all conditions like hosttags, max guests VM limit, etc) to migrate a VM
to it or not.
returned: success
type: str
sample: true
host_type:
description: Type of the host.
returned: success
type: str
sample: Routing
host_version:
description: Version of the host.
returned: success
type: str
sample: 4.5.2
gpu_group:
description: GPU cards present in the host.
returned: success
type: list
sample: []
zone:
description: Zone of the host.
returned: success
type: str
sample: zone01
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
import time
class AnsibleCloudStackHost(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackHost, self).__init__(module)
self.returns = {
'averageload': 'average_load',
'capabilities': 'capabilities',
'clustername': 'cluster',
'clustertype': 'cluster_type',
'cpuallocated': 'cpu_allocated',
'cpunumber': 'cpu_number',
'cpusockets': 'cpu_sockets',
'cpuspeed': 'cpu_speed',
'cpuused': 'cpu_used',
'cpuwithoverprovisioning': 'cpu_with_overprovisioning',
'disconnected': 'disconnected',
'details': 'details',
'disksizeallocated': 'disk_size_allocated',
'disksizetotal': 'disk_size_total',
'events': 'events',
'hahost': 'ha_host',
'hasenoughcapacity': 'has_enough_capacity',
'hypervisor': 'hypervisor',
'hypervisorversion': 'hypervisor_version',
'ipaddress': 'ip_address',
'islocalstorageactive': 'is_local_storage_active',
'lastpinged': 'last_pinged',
'managementserverid': 'management_server_id',
'memoryallocated': 'memory_allocated',
'memorytotal': 'memory_total',
'memoryused': 'memory_used',
'networkkbsread': 'network_kbs_read',
'networkkbswrite': 'network_kbs_write',
'oscategoryname': 'os_category',
'outofbandmanagement': 'out_of_band_management',
'podname': 'pod',
'removed': 'removed',
'resourcestate': 'resource_state',
'suitableformigration': 'suitable_for_migration',
'type': 'host_type',
'version': 'host_version',
'gpugroup': 'gpu_group',
}
# States only usable by the updateHost API
self.allocation_states_for_update = {
'enabled': 'Enable',
'disabled': 'Disable',
}
self.host = None
def get_pod(self, key=None):
pod_name = self.module.params.get('pod')
if not pod_name:
return None
args = {
'name': pod_name,
'zoneid': self.get_zone(key='id'),
}
pods = self.query_api('listPods', **args)
if pods:
return self._get_by_key(key, pods['pod'][0])
self.module.fail_json(msg="Pod %s not found" % pod_name)
def get_cluster(self, key=None):
cluster_name = self.module.params.get('cluster')
if not cluster_name:
return None
args = {
'name': cluster_name,
'zoneid': self.get_zone(key='id'),
}
clusters = self.query_api('listClusters', **args)
if clusters:
return self._get_by_key(key, clusters['cluster'][0])
self.module.fail_json(msg="Cluster %s not found" % cluster_name)
def get_host_tags(self):
host_tags = self.module.params.get('host_tags')
if host_tags is None:
return None
return ','.join(host_tags)
def get_host(self, refresh=False):
if self.host is not None and not refresh:
return self.host
name = self.module.params.get('name')
args = {
'zoneid': self.get_zone(key='id'),
'fetch_list': True,
}
res = self.query_api('listHosts', **args)
if res:
for h in res:
if name in [h['ipaddress'], h['name']]:
self.host = h
return self.host
def _handle_allocation_state(self, host):
allocation_state = self.module.params.get('allocation_state')
if not allocation_state:
return host
host = self._set_host_allocation_state(host)
# In case host in maintenance and target is maintenance
if host['allocationstate'].lower() == allocation_state and allocation_state == 'maintenance':
return host
# Cancel maintenance if target state is enabled/disabled
elif allocation_state in list(self.allocation_states_for_update.keys()):
host = self.disable_maintenance(host)
host = self._update_host(host, self.allocation_states_for_update[allocation_state])
# Only an enabled host can put in maintenance
elif allocation_state == 'maintenance':
host = self._update_host(host, 'Enable')
host = self.enable_maintenance(host)
return host
def _set_host_allocation_state(self, host):
if host is None:
host['allocationstate'] = 'Enable'
# Set host allocationstate to be disabled/enabled
elif host['resourcestate'].lower() in list(self.allocation_states_for_update.keys()):
host['allocationstate'] = self.allocation_states_for_update[host['resourcestate'].lower()]
else:
host['allocationstate'] = host['resourcestate']
return host
def present_host(self):
host = self.get_host()
if not host:
host = self._create_host(host)
else:
host = self._update_host(host)
if host:
host = self._handle_allocation_state(host)
return host
def _get_url(self):
url = self.module.params.get('url')
if url:
return url
else:
return "http://%s" % self.module.params.get('name')
def _create_host(self, host):
required_params = [
'password',
'username',
'hypervisor',
'pod',
]
self.module.fail_on_missing_params(required_params=required_params)
self.result['changed'] = True
args = {
'hypervisor': self.module.params.get('hypervisor'),
'url': self._get_url(),
'username': self.module.params.get('username'),
'password': self.module.params.get('password'),
'podid': self.get_pod(key='id'),
'zoneid': self.get_zone(key='id'),
'clusterid': self.get_cluster(key='id'),
'hosttags': self.get_host_tags(),
}
if not self.module.check_mode:
host = self.query_api('addHost', **args)
host = host['host'][0]
return host
def _update_host(self, host, allocation_state=None):
args = {
'id': host['id'],
'hosttags': self.get_host_tags(),
'allocationstate': allocation_state,
}
if allocation_state is not None:
host = self._set_host_allocation_state(host)
if self.has_changed(args, host):
self.result['changed'] = True
if not self.module.check_mode:
host = self.query_api('updateHost', **args)
host = host['host']
return host
def absent_host(self):
host = self.get_host()
if host:
self.result['changed'] = True
args = {
'id': host['id'],
}
if not self.module.check_mode:
res = self.enable_maintenance(host)
if res:
res = self.query_api('deleteHost', **args)
return host
def enable_maintenance(self, host):
if host['resourcestate'] not in ['PrepareForMaintenance', 'Maintenance']:
self.result['changed'] = True
args = {
'id': host['id'],
}
if not self.module.check_mode:
res = self.query_api('prepareHostForMaintenance', **args)
self.poll_job(res, 'host')
host = self._poll_for_maintenance()
return host
def disable_maintenance(self, host):
if host['resourcestate'] in ['PrepareForMaintenance', 'Maintenance']:
self.result['changed'] = True
args = {
'id': host['id'],
}
if not self.module.check_mode:
res = self.query_api('cancelHostMaintenance', **args)
host = self.poll_job(res, 'host')
return host
def _poll_for_maintenance(self):
for i in range(0, 300):
time.sleep(2)
host = self.get_host(refresh=True)
if not host:
return None
elif host['resourcestate'] != 'PrepareForMaintenance':
return host
self.fail_json(msg="Polling for maintenance timed out")
def get_result(self, host):
super(AnsibleCloudStackHost, self).get_result(host)
if host:
self.result['allocation_state'] = host['resourcestate'].lower()
self.result['host_tags'] = host['hosttags'].split(',') if host.get('hosttags') else []
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True, aliases=['ip_address']),
url=dict(),
password=dict(no_log=True),
username=dict(),
hypervisor=dict(),
allocation_state=dict(choices=['enabled', 'disabled', 'maintenance']),
pod=dict(),
cluster=dict(),
host_tags=dict(type='list', aliases=['host_tag']),
zone=dict(),
state=dict(choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_host = AnsibleCloudStackHost(module)
state = module.params.get('state')
if state == 'absent':
host = acs_host.absent_host()
else:
host = acs_host.present_host()
result = acs_host.get_result(host)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,252 @@
#!/usr/bin/python
# Copyright: (c) 2019, Patryk Cichy @PatTheSilent
# 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: cs_image_store
short_description: Manages CloudStack Image Stores.
description:
- Deploy, remove, recreate CloudStack Image Stores.
options:
url:
description:
- The URL for the Image Store.
- Required when I(state=present).
type: str
name:
description:
- The ID of the Image Store. Required when deleting a Image Store.
required: true
type: str
zone:
description:
- The Zone name for the Image Store.
required: true
type: str
state:
description:
- Stage of the Image Store
choices: [present, absent]
default: present
type: str
provider:
description:
- The image store provider name. Required when creating a new Image Store
type: str
force_recreate:
description:
- Set to C(yes) if you're changing an existing Image Store.
- This will force the recreation of the Image Store.
- Recreation might fail if there are snapshots present on the Image Store. Delete them before running the recreation.
type: bool
default: no
extends_documentation_fragment:
- community.general.cloudstack
author:
- Patryk Cichy (@PatTheSilent)
'''
EXAMPLES = '''
- name: Add a Image Store (NFS)
cs_image_store:
zone: zone-01
name: nfs-01
provider: NFS
url: nfs://192.168.21.16/exports/secondary
delegate_to: localhost
# Change the NFS share URL and force a Image Store recreation
- name: Change the NFS url
cs_image_store:
zone: zone-01
name: nfs-01
provider: NFS
force_recreate: yes
url: nfs://192.168.21.10/shares/secondary
delegate_to: localhost
- name: delete the image store
cs_image_store:
name: nfs-01
zone: zone-01
state: absent
delegate_to: localhost
'''
RETURN = '''
id:
description: the ID of the image store
type: str
returned: success
sample: feb11a84-a093-45eb-b84d-7f680313c40b
name:
description: the name of the image store
type: str
returned: success
sample: nfs-01
protocol:
description: the protocol of the image store
type: str
returned: success
sample: nfs
provider_name:
description: the provider name of the image store
type: str
returned: success
sample: NFS
scope:
description: the scope of the image store
type: str
returned: success
sample: ZONE
url:
description: the url of the image store
type: str
sample: nfs://192.168.21.16/exports/secondary
returned: success
zone:
description: the Zone name of the image store
type: str
returned: success
sample: zone-01
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together
class AnsibleCloudstackImageStore(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudstackImageStore, self).__init__(module)
self.returns = {
'protocol': 'protocol',
'providername': 'provider_name',
'scope': 'scope',
'url': 'url'
}
self.image_store = None
def get_storage_providers(self, storage_type="image"):
args = {
'type': storage_type
}
storage_provides = self.query_api('listStorageProviders', **args)
return [provider.get('name') for provider in storage_provides.get('dataStoreProvider')]
def get_image_store(self):
if self.image_store:
return self.image_store
image_store = self.module.params.get('name')
args = {
'name': self.module.params.get('name'),
'zoneid': self.get_zone(key='id')
}
image_stores = self.query_api('listImageStores', **args)
if image_stores:
for img_s in image_stores.get('imagestore'):
if image_store.lower() in [img_s['name'].lower(), img_s['id']]:
self.image_store = img_s
break
return self.image_store
def present_image_store(self):
provider_list = self.get_storage_providers()
image_store = self.get_image_store()
if self.module.params.get('provider') not in provider_list:
self.module.fail_json(
msg='Provider %s is not in the provider list (%s). Please specify a correct provider' % (
self.module.params.get('provider'), provider_list))
args = {
'name': self.module.params.get('name'),
'url': self.module.params.get('url'),
'zoneid': self.get_zone(key='id'),
'provider': self.module.params.get('provider')
}
if not image_store:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('addImageStore', **args)
self.image_store = res.get('imagestore')
else:
# Cloudstack API expects 'provider' but returns 'providername'
args['providername'] = args.pop('provider')
if self.has_changed(args, image_store):
if self.module.params.get('force_recreate'):
self.absent_image_store()
self.image_store = None
self.image_store = self.present_image_store()
else:
self.module.warn("Changes to the Image Store won't be applied"
"Use force_recreate=yes to allow the store to be recreated")
return self.image_store
def absent_image_store(self):
image_store = self.get_image_store()
if image_store:
self.result['changed'] = True
if not self.module.check_mode:
args = {
'id': image_store.get('id')
}
self.query_api('deleteImageStore', **args)
return image_store
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
url=dict(),
name=dict(required=True),
zone=dict(required=True),
provider=dict(),
force_recreate=dict(type='bool', default=False),
state=dict(choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
required_if=[
('state', 'present', ['url', 'provider']),
],
supports_check_mode=True
)
acis_do = AnsibleCloudstackImageStore(module)
state = module.params.get('state')
if state == "absent":
image_store = acis_do.absent_image_store()
else:
image_store = acis_do.present_image_store()
result = acis_do.get_result(image_store)
module.exit_json(**result)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,375 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['deprecated'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_instance_facts
short_description: Gathering facts from the API of instances from Apache CloudStack based clouds.
description:
- Gathering facts from the API of an instance.
deprecated:
removed_in: "2.13"
why: Transformed into an info module.
alternative: Use M(cs_instance_info) instead.
author: René Moser (@resmo)
options:
name:
description:
- Name or display name of the instance.
type: str
required: true
domain:
description:
- Domain the instance is related to.
type: str
account:
description:
- Account the instance is related to.
type: str
project:
description:
- Project the instance is related to.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: gather instance facts
cs_instance_facts:
name: web-vm-1
delegate_to: localhost
register: vm
- debug:
var: cloudstack_instance
- debug:
var: vm
'''
RETURN = '''
---
id:
description: UUID of the instance.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the instance.
returned: success
type: str
sample: web-01
display_name:
description: Display name of the instance.
returned: success
type: str
sample: web-01
group:
description: Group name of the instance is related.
returned: success
type: str
sample: web
created:
description: Date of the instance was created.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
password_enabled:
description: True if password setting is enabled.
returned: success
type: bool
sample: true
password:
description: The password of the instance if exists.
returned: success
type: str
sample: Ge2oe7Do
ssh_key:
description: Name of SSH key deployed to instance.
returned: success
type: str
sample: key@work
domain:
description: Domain the instance is related to.
returned: success
type: str
sample: example domain
account:
description: Account the instance is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the instance is related to.
returned: success
type: str
sample: Production
default_ip:
description: Default IP address of the instance.
returned: success
type: str
sample: 10.23.37.42
public_ip:
description: Public IP address with instance via static NAT rule.
returned: success
type: str
sample: 1.2.3.4
iso:
description: Name of ISO the instance was deployed with.
returned: success
type: str
sample: Debian-8-64bit
template:
description: Name of template the instance was deployed with.
returned: success
type: str
sample: Debian-8-64bit
service_offering:
description: Name of the service offering the instance has.
returned: success
type: str
sample: 2cpu_2gb
zone:
description: Name of zone the instance is in.
returned: success
type: str
sample: ch-gva-2
state:
description: State of the instance.
returned: success
type: str
sample: Running
security_groups:
description: Security groups the instance is in.
returned: success
type: list
sample: '[ "default" ]'
affinity_groups:
description: Affinity groups the instance is in.
returned: success
type: list
sample: '[ "webservers" ]'
tags:
description: List of resource tags associated with the instance.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
hypervisor:
description: Hypervisor related to this instance.
returned: success
type: str
sample: KVM
host:
description: Host the instance is running on.
returned: success and instance is running
type: str
sample: host01.example.com
version_added: '2.6'
instance_name:
description: Internal name of the instance (ROOT admin only).
returned: success
type: str
sample: i-44-3992-VM
volumes:
description: List of dictionaries of the volumes attached to the instance.
returned: success
type: list
sample: '[ { name: "ROOT-1369", type: "ROOT", size: 10737418240 }, { name: "data01, type: "DATADISK", size: 10737418240 } ]'
nic:
description: List of dictionaries of the instance nics.
returned: success
type: complex
version_added: '2.8'
contains:
broadcasturi:
description: The broadcast uri of the nic.
returned: success
type: str
sample: vlan://2250
gateway:
description: The gateway of the nic.
returned: success
type: str
sample: 10.1.2.1
id:
description: The ID of the nic.
returned: success
type: str
sample: 5dc74fa3-2ec3-48a0-9e0d-6f43365336a9
ipaddress:
description: The ip address of the nic.
returned: success
type: str
sample: 10.1.2.3
isdefault:
description: True if nic is default, false otherwise.
returned: success
type: bool
sample: true
isolationuri:
description: The isolation uri of the nic.
returned: success
type: str
sample: vlan://2250
macaddress:
description: The mac address of the nic.
returned: success
type: str
sample: 06:a2:03:00:08:12
netmask:
description: The netmask of the nic.
returned: success
type: str
sample: 255.255.255.0
networkid:
description: The ID of the corresponding network.
returned: success
type: str
sample: 432ce27b-c2bb-4e12-a88c-a919cd3a3017
networkname:
description: The name of the corresponding network.
returned: success
type: str
sample: network1
traffictype:
description: The traffic type of the nic.
returned: success
type: str
sample: Guest
type:
description: The type of the network.
returned: success
type: str
sample: Shared
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec
class AnsibleCloudStackInstanceFacts(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackInstanceFacts, self).__init__(module)
self.instance = None
self.returns = {
'group': 'group',
'hypervisor': 'hypervisor',
'instancename': 'instance_name',
'publicip': 'public_ip',
'passwordenabled': 'password_enabled',
'password': 'password',
'serviceofferingname': 'service_offering',
'isoname': 'iso',
'templatename': 'template',
'keypair': 'ssh_key',
'hostname': 'host',
}
self.facts = {
'cloudstack_instance': None,
}
def get_instance(self):
instance = self.instance
if not instance:
instance_name = self.module.params.get('name')
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'fetch_list': True,
}
# Do not pass zoneid, as the instance name must be unique across zones.
instances = self.query_api('listVirtualMachines', **args)
if instances:
for v in instances:
if instance_name.lower() in [v['name'].lower(), v['displayname'].lower(), v['id']]:
self.instance = v
break
return self.instance
def get_volumes(self, instance):
volume_details = []
if instance:
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'virtualmachineid': instance['id'],
'fetch_list': True,
}
volumes = self.query_api('listVolumes', **args)
if volumes:
for vol in volumes:
volume_details.append({'size': vol['size'], 'type': vol['type'], 'name': vol['name']})
return volume_details
def run(self):
instance = self.get_instance()
if not instance:
self.module.fail_json(msg="Instance not found: %s" % self.module.params.get('name'))
return instance
def get_result(self, instance):
super(AnsibleCloudStackInstanceFacts, self).get_result(instance)
if instance:
if 'securitygroup' in instance:
security_groups = []
for securitygroup in instance['securitygroup']:
security_groups.append(securitygroup['name'])
self.result['security_groups'] = security_groups
if 'affinitygroup' in instance:
affinity_groups = []
for affinitygroup in instance['affinitygroup']:
affinity_groups.append(affinitygroup['name'])
self.result['affinity_groups'] = affinity_groups
if 'nic' in instance:
for nic in instance['nic']:
if nic['isdefault'] and 'ipaddress' in nic:
self.result['default_ip'] = nic['ipaddress']
self.result['nic'] = instance['nic']
volumes = self.get_volumes(instance)
if volumes:
self.result['volumes'] = volumes
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
domain=dict(),
account=dict(),
project=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
acs_instance_facts = AnsibleCloudStackInstanceFacts(module=module)
cs_instance_facts = acs_instance_facts.get_result_and_facts(
facts_name='cloudstack_instance',
resource=acs_instance_facts.run()
)
module.exit_json(**cs_instance_facts)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,379 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_instance_info
short_description: Gathering information from the API of instances from Apache CloudStack based clouds.
description:
- Gathering information from the API of an instance.
author: René Moser (@resmo)
options:
name:
description:
- Name or display name of the instance.
- If not specified, all instances are returned
type: str
required: false
domain:
description:
- Domain the instance is related to.
type: str
account:
description:
- Account the instance is related to.
type: str
project:
description:
- Project the instance is related to.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Gather instance information
cs_instance_info:
name: web-vm-1
delegate_to: localhost
register: vm
- name: Show the returned results of the registered variable
debug:
msg: "{{ vm }}"
- name: Gather information from all instances
cs_instance_info:
delegate_to: localhost
register: vms
- name: Show information on all instances
debug:
msg: "{{ vms }}"
'''
RETURN = '''
---
instances:
description: A list of matching instances.
type: list
returned: success
contains:
id:
description: UUID of the instance.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the instance.
returned: success
type: str
sample: web-01
display_name:
description: Display name of the instance.
returned: success
type: str
sample: web-01
group:
description: Group name of the instance is related.
returned: success
type: str
sample: web
created:
description: Date of the instance was created.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
password_enabled:
description: True if password setting is enabled.
returned: success
type: bool
sample: true
password:
description: The password of the instance if exists.
returned: success
type: str
sample: Ge2oe7Do
ssh_key:
description: Name of SSH key deployed to instance.
returned: success
type: str
sample: key@work
domain:
description: Domain the instance is related to.
returned: success
type: str
sample: example domain
account:
description: Account the instance is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the instance is related to.
returned: success
type: str
sample: Production
default_ip:
description: Default IP address of the instance.
returned: success
type: str
sample: 10.23.37.42
public_ip:
description: Public IP address with instance via static NAT rule.
returned: success
type: str
sample: 1.2.3.4
iso:
description: Name of ISO the instance was deployed with.
returned: success
type: str
sample: Debian-8-64bit
template:
description: Name of template the instance was deployed with.
returned: success
type: str
sample: Debian-8-64bit
service_offering:
description: Name of the service offering the instance has.
returned: success
type: str
sample: 2cpu_2gb
zone:
description: Name of zone the instance is in.
returned: success
type: str
sample: ch-gva-2
state:
description: State of the instance.
returned: success
type: str
sample: Running
security_groups:
description: Security groups the instance is in.
returned: success
type: list
sample: '[ "default" ]'
affinity_groups:
description: Affinity groups the instance is in.
returned: success
type: list
sample: '[ "webservers" ]'
tags:
description: List of resource tags associated with the instance.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
hypervisor:
description: Hypervisor related to this instance.
returned: success
type: str
sample: KVM
host:
description: Host the instance is running on.
returned: success and instance is running
type: str
sample: host01.example.com
version_added: '2.6'
instance_name:
description: Internal name of the instance (ROOT admin only).
returned: success
type: str
sample: i-44-3992-VM
volumes:
description: List of dictionaries of the volumes attached to the instance.
returned: success
type: list
sample: '[ { name: "ROOT-1369", type: "ROOT", size: 10737418240 }, { name: "data01, type: "DATADISK", size: 10737418240 } ]'
nic:
description: List of dictionaries of the instance nics.
returned: success
type: complex
version_added: '2.8'
contains:
broadcasturi:
description: The broadcast uri of the nic.
returned: success
type: str
sample: vlan://2250
gateway:
description: The gateway of the nic.
returned: success
type: str
sample: 10.1.2.1
id:
description: The ID of the nic.
returned: success
type: str
sample: 5dc74fa3-2ec3-48a0-9e0d-6f43365336a9
ipaddress:
description: The ip address of the nic.
returned: success
type: str
sample: 10.1.2.3
isdefault:
description: True if nic is default, false otherwise.
returned: success
type: bool
sample: true
isolationuri:
description: The isolation uri of the nic.
returned: success
type: str
sample: vlan://2250
macaddress:
description: The mac address of the nic.
returned: success
type: str
sample: 06:a2:03:00:08:12
netmask:
description: The netmask of the nic.
returned: success
type: str
sample: 255.255.255.0
networkid:
description: The ID of the corresponding network.
returned: success
type: str
sample: 432ce27b-c2bb-4e12-a88c-a919cd3a3017
networkname:
description: The name of the corresponding network.
returned: success
type: str
sample: network1
traffictype:
description: The traffic type of the nic.
returned: success
type: str
sample: Guest
type:
description: The type of the network.
returned: success
type: str
sample: Shared
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec
class AnsibleCloudStackInstanceInfo(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackInstanceInfo, self).__init__(module)
self.returns = {
'group': 'group',
'hypervisor': 'hypervisor',
'instancename': 'instance_name',
'publicip': 'public_ip',
'passwordenabled': 'password_enabled',
'password': 'password',
'serviceofferingname': 'service_offering',
'isoname': 'iso',
'templatename': 'template',
'keypair': 'ssh_key',
'hostname': 'host',
}
def get_instances(self):
instance_name = self.module.params.get('name')
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'fetch_list': True,
}
# Do not pass zoneid, as the instance name must be unique across zones.
instances = self.query_api('listVirtualMachines', **args)
if not instance_name:
return instances or []
if instances:
for v in instances:
if instance_name.lower() in [v['name'].lower(), v['displayname'].lower(), v['id']]:
return [v]
return []
def get_volumes(self, instance):
volume_details = []
if instance:
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'virtualmachineid': instance['id'],
'fetch_list': True,
}
volumes = self.query_api('listVolumes', **args)
if volumes:
for vol in volumes:
volume_details.append({'size': vol['size'], 'type': vol['type'], 'name': vol['name']})
return volume_details
def run(self):
instances = self.get_instances()
if self.module.params.get('name') and not instances:
self.module.fail_json(msg="Instance not found: %s" % self.module.params.get('name'))
return {
'instances': [self.update_result(resource) for resource in instances]
}
def update_result(self, instance, result=None):
result = super(AnsibleCloudStackInstanceInfo, self).update_result(instance, result)
if instance:
if 'securitygroup' in instance:
security_groups = []
for securitygroup in instance['securitygroup']:
security_groups.append(securitygroup['name'])
result['security_groups'] = security_groups
if 'affinitygroup' in instance:
affinity_groups = []
for affinitygroup in instance['affinitygroup']:
affinity_groups.append(affinitygroup['name'])
result['affinity_groups'] = affinity_groups
if 'nic' in instance:
for nic in instance['nic']:
if nic['isdefault'] and 'ipaddress' in nic:
result['default_ip'] = nic['ipaddress']
result['nic'] = instance['nic']
volumes = self.get_volumes(instance)
if volumes:
result['volumes'] = volumes
return result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(),
domain=dict(),
account=dict(),
project=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
acs_instance_info = AnsibleCloudStackInstanceInfo(module=module)
cs_instance_info = acs_instance_info.run()
module.exit_json(**cs_instance_info)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,290 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, Marc-Aurèle Brothier @marcaurele
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_instance_nic
short_description: Manages NICs of an instance on Apache CloudStack based clouds.
description:
- Add and remove nic to and from network
author:
- Marc-Aurèle Brothier (@marcaurele)
- René Moser (@resmo)
options:
vm:
description:
- Name of instance.
required: true
type: str
aliases: [ name ]
network:
description:
- Name of the network.
type: str
required: true
ip_address:
description:
- IP address to be used for the nic.
type: str
vpc:
description:
- Name of the VPC the I(vm) is related to.
type: str
domain:
description:
- Domain the instance is related to.
type: str
account:
description:
- Account the instance is related to.
type: str
project:
description:
- Name of the project the instance is deployed in.
type: str
zone:
description:
- Name of the zone in which the instance is deployed in.
- If not set, default zone is used.
type: str
state:
description:
- State of the nic.
type: str
default: present
choices: [ present, absent ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Add a nic on another network
cs_instance_nic:
vm: privnet
network: privNetForBasicZone
delegate_to: localhost
- name: Ensure IP address on a nic
cs_instance_nic:
vm: privnet
ip_address: 10.10.11.32
network: privNetForBasicZone
delegate_to: localhost
- name: Remove a secondary nic
cs_instance_nic:
vm: privnet
state: absent
network: privNetForBasicZone
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the nic.
returned: success
type: str
sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8
vm:
description: Name of the VM.
returned: success
type: str
sample: web-01
ip_address:
description: Primary IP of the NIC.
returned: success
type: str
sample: 10.10.10.10
netmask:
description: Netmask of the NIC.
returned: success
type: str
sample: 255.255.255.0
mac_address:
description: MAC address of the NIC.
returned: success
type: str
sample: 02:00:33:31:00:e4
network:
description: Name of the network if not default.
returned: success
type: str
sample: sync network
domain:
description: Domain the VM is related to.
returned: success
type: str
sample: example domain
account:
description: Account the VM is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the VM is related to.
returned: success
type: str
sample: Production
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (AnsibleCloudStack,
cs_argument_spec,
cs_required_together)
class AnsibleCloudStackInstanceNic(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackInstanceNic, self).__init__(module)
self.nic = None
self.returns = {
'ipaddress': 'ip_address',
'macaddress': 'mac_address',
'netmask': 'netmask',
}
def get_nic(self):
if self.nic:
return self.nic
args = {
'virtualmachineid': self.get_vm(key='id'),
'networkid': self.get_network(key='id'),
}
nics = self.query_api('listNics', **args)
if nics:
self.nic = nics['nic'][0]
return self.nic
return None
def get_nic_from_result(self, result):
for nic in result.get('nic') or []:
if nic['networkid'] == self.get_network(key='id'):
return nic
def add_nic(self):
self.result['changed'] = True
args = {
'virtualmachineid': self.get_vm(key='id'),
'networkid': self.get_network(key='id'),
'ipaddress': self.module.params.get('ip_address'),
}
if not self.module.check_mode:
res = self.query_api('addNicToVirtualMachine', **args)
if self.module.params.get('poll_async'):
vm = self.poll_job(res, 'virtualmachine')
self.nic = self.get_nic_from_result(result=vm)
return self.nic
def update_nic(self, nic):
# Do not try to update if no IP address is given
ip_address = self.module.params.get('ip_address')
if not ip_address:
return nic
args = {
'nicid': nic['id'],
'ipaddress': ip_address,
}
if self.has_changed(args, nic, ['ipaddress']):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateVmNicIp', **args)
if self.module.params.get('poll_async'):
vm = self.poll_job(res, 'virtualmachine')
self.nic = self.get_nic_from_result(result=vm)
return self.nic
def remove_nic(self, nic):
self.result['changed'] = True
args = {
'virtualmachineid': self.get_vm(key='id'),
'nicid': nic['id'],
}
if not self.module.check_mode:
res = self.query_api('removeNicFromVirtualMachine', **args)
if self.module.params.get('poll_async'):
self.poll_job(res, 'virtualmachine')
return nic
def present_nic(self):
nic = self.get_nic()
if not nic:
nic = self.add_nic()
else:
nic = self.update_nic(nic)
return nic
def absent_nic(self):
nic = self.get_nic()
if nic:
return self.remove_nic(nic)
return nic
def get_result(self, nic):
super(AnsibleCloudStackInstanceNic, self).get_result(nic)
if nic and not self.module.params.get('network'):
self.module.params['network'] = nic.get('networkid')
self.result['network'] = self.get_network(key='name')
self.result['vm'] = self.get_vm(key='name')
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
vm=dict(required=True, aliases=['name']),
network=dict(required=True),
vpc=dict(),
ip_address=dict(),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True,
)
acs_nic = AnsibleCloudStackInstanceNic(module)
state = module.params.get('state')
if state == 'absent':
nic = acs_nic.absent_nic()
else:
nic = acs_nic.present_nic()
result = acs_nic.get_result(nic)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,273 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_instance_nic_secondaryip
short_description: Manages secondary IPs of an instance on Apache CloudStack based clouds.
description:
- Add and remove secondary IPs to and from a NIC of an instance.
author: René Moser (@resmo)
options:
vm:
description:
- Name of instance.
type: str
required: true
aliases: [ name ]
network:
description:
- Name of the network.
- Required to find the NIC if instance has multiple networks assigned.
type: str
vm_guest_ip:
description:
- Secondary IP address to be added to the instance nic.
- If not set, the API always returns a new IP address and idempotency is not given.
type: str
aliases: [ secondary_ip ]
vpc:
description:
- Name of the VPC the I(vm) is related to.
type: str
domain:
description:
- Domain the instance is related to.
type: str
account:
description:
- Account the instance is related to.
type: str
project:
description:
- Name of the project the instance is deployed in.
type: str
zone:
description:
- Name of the zone in which the instance is deployed in.
- If not set, default zone is used.
type: str
state:
description:
- State of the ipaddress.
type: str
default: present
choices: [ present, absent ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Assign a specific IP to the default NIC of the VM
cs_instance_nic_secondaryip:
vm: customer_xy
vm_guest_ip: 10.10.10.10
delegate_to: localhost
# Note: If vm_guest_ip is not set, you will get a new IP address on every run.
- name: Assign an IP to the default NIC of the VM
cs_instance_nic_secondaryip:
vm: customer_xy
delegate_to: localhost
- name: Remove a specific IP from the default NIC
cs_instance_nic_secondaryip:
vm: customer_xy
vm_guest_ip: 10.10.10.10
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the NIC.
returned: success
type: str
sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8
vm:
description: Name of the VM.
returned: success
type: str
sample: web-01
ip_address:
description: Primary IP of the NIC.
returned: success
type: str
sample: 10.10.10.10
netmask:
description: Netmask of the NIC.
returned: success
type: str
sample: 255.255.255.0
mac_address:
description: MAC address of the NIC.
returned: success
type: str
sample: 02:00:33:31:00:e4
vm_guest_ip:
description: Secondary IP of the NIC.
returned: success
type: str
sample: 10.10.10.10
network:
description: Name of the network if not default.
returned: success
type: str
sample: sync network
domain:
description: Domain the VM is related to.
returned: success
type: str
sample: example domain
account:
description: Account the VM is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the VM is related to.
returned: success
type: str
sample: Production
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackInstanceNicSecondaryIp(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackInstanceNicSecondaryIp, self).__init__(module)
self.vm_guest_ip = self.module.params.get('vm_guest_ip')
self.nic = None
self.returns = {
'ipaddress': 'ip_address',
'macaddress': 'mac_address',
'netmask': 'netmask',
}
def get_nic(self):
if self.nic:
return self.nic
args = {
'virtualmachineid': self.get_vm(key='id'),
'networkid': self.get_network(key='id'),
}
nics = self.query_api('listNics', **args)
if nics:
self.nic = nics['nic'][0]
return self.nic
self.fail_json(msg="NIC for VM %s in network %s not found" % (self.get_vm(key='name'), self.get_network(key='name')))
def get_secondary_ip(self):
nic = self.get_nic()
if self.vm_guest_ip:
secondary_ips = nic.get('secondaryip') or []
for secondary_ip in secondary_ips:
if secondary_ip['ipaddress'] == self.vm_guest_ip:
return secondary_ip
return None
def present_nic_ip(self):
nic = self.get_nic()
if not self.get_secondary_ip():
self.result['changed'] = True
args = {
'nicid': nic['id'],
'ipaddress': self.vm_guest_ip,
}
if not self.module.check_mode:
res = self.query_api('addIpToNic', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
nic = self.poll_job(res, 'nicsecondaryip')
# Save result for RETURNS
self.vm_guest_ip = nic['ipaddress']
return nic
def absent_nic_ip(self):
nic = self.get_nic()
secondary_ip = self.get_secondary_ip()
if secondary_ip:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('removeIpFromNic', id=secondary_ip['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'nicsecondaryip')
return nic
def get_result(self, nic):
super(AnsibleCloudStackInstanceNicSecondaryIp, self).get_result(nic)
if nic and not self.module.params.get('network'):
self.module.params['network'] = nic.get('networkid')
self.result['network'] = self.get_network(key='name')
self.result['vm'] = self.get_vm(key='name')
self.result['vm_guest_ip'] = self.vm_guest_ip
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
vm=dict(required=True, aliases=['name']),
vm_guest_ip=dict(aliases=['secondary_ip']),
network=dict(),
vpc=dict(),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True,
required_if=([
('state', 'absent', ['vm_guest_ip'])
])
)
acs_instance_nic_secondaryip = AnsibleCloudStackInstanceNicSecondaryIp(module)
state = module.params.get('state')
if state == 'absent':
nic = acs_instance_nic_secondaryip.absent_nic_ip()
else:
nic = acs_instance_nic_secondaryip.present_nic_ip()
result = acs_instance_nic_secondaryip.get_result(nic)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,158 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2018, Gregor Riepl <onitake@gmail.com>
# based on cs_sshkeypair (c) 2015, René Moser <mail@renemoser.net>
# 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: cs_instance_password_reset
short_description: Allows resetting VM the default passwords on Apache CloudStack based clouds.
description:
- Resets the default user account's password on an instance.
- Requires cloud-init to be installed in the virtual machine.
- The passwordenabled flag must be set on the template associated with the VM.
author: Gregor Riepl (@onitake)
options:
vm:
description:
- Name of the virtual machine to reset the password on.
type: str
required: true
domain:
description:
- Name of the domain the virtual machine belongs to.
type: str
account:
description:
- Account the virtual machine belongs to.
type: str
project:
description:
- Name of the project the virtual machine belongs to.
type: str
zone:
description:
- Name of the zone in which the instance is deployed.
- If not set, the default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: stop the virtual machine before resetting the password
cs_instance:
name: myvirtualmachine
state: stopped
delegate_to: localhost
- name: reset and get new default password
cs_instance_password_reset:
vm: myvirtualmachine
register: root
delegate_to: localhost
- debug:
msg: "new default password is {{ root.password }}"
- name: boot the virtual machine to activate the new password
cs_instance:
name: myvirtualmachine
state: started
delegate_to: localhost
when: root is changed
'''
RETURN = '''
---
id:
description: ID of the virtual machine.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
password:
description: The new default password.
returned: success
type: str
sample: ahQu5nuNge3keesh
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_required_together,
cs_argument_spec
)
class AnsibleCloudStackPasswordReset(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackPasswordReset, self).__init__(module)
self.returns = {
'password': 'password',
}
self.password = None
def reset_password(self):
args = {
'id': self.get_vm(key='id'),
}
res = None
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('resetPasswordForVirtualMachine', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
res = self.poll_job(res, 'virtualmachine')
if res and 'password' in res:
self.password = res['password']
return self.password
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
vm=dict(required=True),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_password = AnsibleCloudStackPasswordReset(module)
password = acs_password.reset_password()
result = acs_password.get_result({'password': password})
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,187 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_instancegroup
short_description: Manages instance groups on Apache CloudStack based clouds.
description:
- Create and remove instance groups.
author: René Moser (@resmo)
options:
name:
description:
- Name of the instance group.
type: str
required: true
domain:
description:
- Domain the instance group is related to.
type: str
account:
description:
- Account the instance group is related to.
type: str
project:
description:
- Project the instance group is related to.
type: str
state:
description:
- State of the instance group.
type: str
default: present
choices: [ present, absent ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create an instance group
cs_instancegroup:
name: loadbalancers
delegate_to: localhost
- name: Remove an instance group
cs_instancegroup:
name: loadbalancers
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the instance group.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the instance group.
returned: success
type: str
sample: webservers
created:
description: Date when the instance group was created.
returned: success
type: str
sample: 2015-05-03T15:05:51+0200
domain:
description: Domain the instance group is related to.
returned: success
type: str
sample: example domain
account:
description: Account the instance group is related to.
returned: success
type: str
sample: example account
project:
description: Project the instance group is related to.
returned: success
type: str
sample: example project
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackInstanceGroup(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackInstanceGroup, self).__init__(module)
self.instance_group = None
def get_instance_group(self):
if self.instance_group:
return self.instance_group
name = self.module.params.get('name')
args = {
'account': self.get_account('name'),
'domainid': self.get_domain('id'),
'projectid': self.get_project('id'),
'fetch_list': True,
}
instance_groups = self.query_api('listInstanceGroups', **args)
if instance_groups:
for g in instance_groups:
if name in [g['name'], g['id']]:
self.instance_group = g
break
return self.instance_group
def present_instance_group(self):
instance_group = self.get_instance_group()
if not instance_group:
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'account': self.get_account('name'),
'domainid': self.get_domain('id'),
'projectid': self.get_project('id'),
}
if not self.module.check_mode:
res = self.query_api('createInstanceGroup', **args)
instance_group = res['instancegroup']
return instance_group
def absent_instance_group(self):
instance_group = self.get_instance_group()
if instance_group:
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('deleteInstanceGroup', id=instance_group['id'])
return instance_group
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
state=dict(default='present', choices=['present', 'absent']),
domain=dict(),
account=dict(),
project=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_ig = AnsibleCloudStackInstanceGroup(module)
state = module.params.get('state')
if state in ['absent']:
instance_group = acs_ig.absent_instance_group()
else:
instance_group = acs_ig.present_instance_group()
result = acs_ig.get_result(instance_group)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,285 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015, Darren Worrall <darren@iweb.co.uk>
# Copyright (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_ip_address
short_description: Manages public IP address associations on Apache CloudStack based clouds.
description:
- Acquires and associates a public IP to an account or project.
- Due to API limitations this is not an idempotent call, so be sure to only
conditionally call this when I(state=present).
- Tagging the IP address can also make the call idempotent.
author:
- Darren Worrall (@dazworrall)
- René Moser (@resmo)
options:
ip_address:
description:
- Public IP address.
- Required if I(state=absent) and I(tags) is not set.
type: str
domain:
description:
- Domain the IP address is related to.
type: str
network:
description:
- Network the IP address is related to.
- Mutually exclusive with I(vpc).
type: str
vpc:
description:
- VPC the IP address is related to.
- Mutually exclusive with I(network).
type: str
account:
description:
- Account the IP address is related to.
type: str
project:
description:
- Name of the project the IP address is related to.
type: str
zone:
description:
- Name of the zone in which the IP address is in.
- If not set, default zone is used.
type: str
state:
description:
- State of the IP address.
type: str
default: present
choices: [ present, absent ]
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- Tags can be used as an unique identifier for the IP Addresses.
- In this case, at least one of them must be unique to ensure idempotency.
type: list
aliases: [ tag ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Associate an IP address conditionally
cs_ip_address:
network: My Network
register: ip_address
when: instance.public_ip is undefined
delegate_to: localhost
- name: Disassociate an IP address
cs_ip_address:
ip_address: 1.2.3.4
state: absent
delegate_to: localhost
- name: Associate an IP address with tags
cs_ip_address:
network: My Network
tags:
- key: myCustomID
- value: 5510c31a-416e-11e8-9013-02000a6b00bf
register: ip_address
delegate_to: localhost
- name: Disassociate an IP address with tags
cs_ip_address:
state: absent
tags:
- key: myCustomID
- value: 5510c31a-416e-11e8-9013-02000a6b00bf
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the Public IP address.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
ip_address:
description: Public IP address.
returned: success
type: str
sample: 1.2.3.4
zone:
description: Name of zone the IP address is related to.
returned: success
type: str
sample: ch-gva-2
project:
description: Name of project the IP address is related to.
returned: success
type: str
sample: Production
account:
description: Account the IP address is related to.
returned: success
type: str
sample: example account
domain:
description: Domain the IP address is related to.
returned: success
type: str
sample: example domain
tags:
description: List of resource tags associated with the IP address.
returned: success
type: dict
sample: '[ { "key": "myCustomID", "value": "5510c31a-416e-11e8-9013-02000a6b00bf" } ]'
version_added: '2.6'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackIPAddress(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackIPAddress, self).__init__(module)
self.returns = {
'ipaddress': 'ip_address',
}
def get_ip_address(self, key=None):
if self.ip_address:
return self._get_by_key(key, self.ip_address)
args = {
'ipaddress': self.module.params.get('ip_address'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'vpcid': self.get_vpc(key='id'),
}
ip_addresses = self.query_api('listPublicIpAddresses', **args)
if ip_addresses:
tags = self.module.params.get('tags')
for ip_addr in ip_addresses['publicipaddress']:
if ip_addr['ipaddress'] == args['ipaddress'] != '':
self.ip_address = ip_addresses['publicipaddress'][0]
elif tags:
if sorted([tag for tag in tags if tag in ip_addr['tags']]) == sorted(tags):
self.ip_address = ip_addr
return self._get_by_key(key, self.ip_address)
def present_ip_address(self):
ip_address = self.get_ip_address()
if not ip_address:
ip_address = self.associate_ip_address(ip_address)
if ip_address:
ip_address = self.ensure_tags(resource=ip_address, resource_type='publicipaddress')
return ip_address
def associate_ip_address(self, ip_address):
self.result['changed'] = True
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
# For the VPC case networkid is irrelevant, special case and we have to ignore it here.
'networkid': self.get_network(key='id') if not self.module.params.get('vpc') else None,
'zoneid': self.get_zone(key='id'),
'vpcid': self.get_vpc(key='id'),
}
ip_address = None
if not self.module.check_mode:
res = self.query_api('associateIpAddress', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
ip_address = self.poll_job(res, 'ipaddress')
return ip_address
def disassociate_ip_address(self):
ip_address = self.get_ip_address()
if not ip_address:
return None
if ip_address['isstaticnat']:
self.module.fail_json(msg="IP address is allocated via static nat")
self.result['changed'] = True
if not self.module.check_mode:
self.module.params['tags'] = []
ip_address = self.ensure_tags(resource=ip_address, resource_type='publicipaddress')
res = self.query_api('disassociateIpAddress', id=ip_address['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'ipaddress')
return ip_address
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
ip_address=dict(required=False),
state=dict(choices=['present', 'absent'], default='present'),
vpc=dict(),
network=dict(),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
tags=dict(type='list', aliases=['tag']),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
required_if=[
('state', 'absent', ['ip_address', 'tags'], True),
],
mutually_exclusive=(
['vpc', 'network'],
),
supports_check_mode=True
)
acs_ip_address = AnsibleCloudStackIPAddress(module)
state = module.params.get('state')
if state in ['absent']:
ip_address = acs_ip_address.disassociate_ip_address()
else:
ip_address = acs_ip_address.present_ip_address()
result = acs_ip_address.get_result(ip_address)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,450 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_iso
short_description: Manages ISO images on Apache CloudStack based clouds.
description:
- Register and remove ISO images.
author: René Moser (@resmo)
options:
name:
description:
- Name of the ISO.
type: str
required: true
display_text:
description:
- Display text of the ISO.
- If not specified, I(name) will be used.
type: str
url:
description:
- URL where the ISO can be downloaded from. Required if I(state) is present.
type: str
os_type:
description:
- Name of the OS that best represents the OS of this ISO. If the iso is bootable this parameter needs to be passed. Required if I(state) is present.
type: str
is_ready:
description:
- This flag is used for searching existing ISOs. If set to C(yes), it will only list ISO ready for deployment e.g.
successfully downloaded and installed. Recommended to set it to C(no).
type: bool
default: no
is_public:
description:
- Register the ISO to be publicly available to all users. Only used if I(state) is present.
type: bool
is_featured:
description:
- Register the ISO to be featured. Only used if I(state) is present.
type: bool
is_dynamically_scalable:
description:
- Register the ISO having XS/VMware tools installed inorder to support dynamic scaling of VM cpu/memory. Only used if I(state) is present.
type: bool
checksum:
description:
- The MD5 checksum value of this ISO. If set, we search by checksum instead of name.
type: str
bootable:
description:
- Register the ISO to be bootable. Only used if I(state) is present.
type: bool
domain:
description:
- Domain the ISO is related to.
type: str
account:
description:
- Account the ISO is related to.
type: str
project:
description:
- Name of the project the ISO to be registered in.
type: str
zone:
description:
- Name of the zone you wish the ISO to be registered or deleted from.
- If not specified, first zone found will be used.
type: str
cross_zones:
description:
- Whether the ISO should be synced or removed across zones.
- Mutually exclusive with I(zone).
type: bool
default: no
iso_filter:
description:
- Name of the filter used to search for the ISO.
type: str
default: self
choices: [ featured, self, selfexecutable,sharedexecutable,executable, community ]
state:
description:
- State of the ISO.
type: str
default: present
choices: [ present, absent ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Register an ISO if ISO name does not already exist
cs_iso:
name: Debian 7 64-bit
url: http://mirror.switch.ch/ftp/mirror/debian-cd/current/amd64/iso-cd/debian-7.7.0-amd64-netinst.iso
os_type: Debian GNU/Linux 7(64-bit)
delegate_to: localhost
- name: Register an ISO with given name if ISO md5 checksum does not already exist
cs_iso:
name: Debian 7 64-bit
url: http://mirror.switch.ch/ftp/mirror/debian-cd/current/amd64/iso-cd/debian-7.7.0-amd64-netinst.iso
os_type: Debian GNU/Linux 7(64-bit)
checksum: 0b31bccccb048d20b551f70830bb7ad0
delegate_to: localhost
- name: Remove an ISO by name
cs_iso:
name: Debian 7 64-bit
state: absent
delegate_to: localhost
- name: Remove an ISO by checksum
cs_iso:
name: Debian 7 64-bit
checksum: 0b31bccccb048d20b551f70830bb7ad0
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the ISO.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: Name of the ISO.
returned: success
type: str
sample: Debian 7 64-bit
display_text:
description: Text to be displayed of the ISO.
returned: success
type: str
sample: Debian 7.7 64-bit minimal 2015-03-19
zone:
description: Name of zone the ISO is registered in.
returned: success
type: str
sample: zuerich
status:
description: Status of the ISO.
returned: success
type: str
sample: Successfully Installed
is_ready:
description: True if the ISO is ready to be deployed from.
returned: success
type: bool
sample: true
is_public:
description: True if the ISO is public.
returned: success
type: bool
sample: true
version_added: '2.4'
bootable:
description: True if the ISO is bootable.
returned: success
type: bool
sample: true
version_added: '2.4'
is_featured:
description: True if the ISO is featured.
returned: success
type: bool
sample: true
version_added: '2.4'
format:
description: Format of the ISO.
returned: success
type: str
sample: ISO
version_added: '2.4'
os_type:
description: Typo of the OS.
returned: success
type: str
sample: CentOS 6.5 (64-bit)
version_added: '2.4'
checksum:
description: MD5 checksum of the ISO.
returned: success
type: str
sample: 0b31bccccb048d20b551f70830bb7ad0
created:
description: Date of registering.
returned: success
type: str
sample: 2015-03-29T14:57:06+0200
cross_zones:
description: true if the ISO is managed across all zones, false otherwise.
returned: success
type: bool
sample: false
version_added: '2.4'
domain:
description: Domain the ISO is related to.
returned: success
type: str
sample: example domain
account:
description: Account the ISO is related to.
returned: success
type: str
sample: example account
project:
description: Project the ISO is related to.
returned: success
type: str
sample: example project
tags:
description: List of resource tags associated with the ISO.
returned: success
type: dict
sample: '[ { "key": "foo", "value": "bar" } ]'
version_added: '2.4'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackIso(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackIso, self).__init__(module)
self.returns = {
'checksum': 'checksum',
'status': 'status',
'isready': 'is_ready',
'crossZones': 'cross_zones',
'format': 'format',
'ostypename': 'os_type',
'isfeatured': 'is_featured',
'bootable': 'bootable',
'ispublic': 'is_public',
}
self.iso = None
def _get_common_args(self):
return {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'isdynamicallyscalable': self.module.params.get('is_dynamically_scalable'),
'ostypeid': self.get_os_type('id'),
'bootable': self.module.params.get('bootable'),
}
def register_iso(self):
args = self._get_common_args()
args.update({
'domainid': self.get_domain('id'),
'account': self.get_account('name'),
'projectid': self.get_project('id'),
'checksum': self.module.params.get('checksum'),
'isfeatured': self.module.params.get('is_featured'),
'ispublic': self.module.params.get('is_public'),
})
if not self.module.params.get('cross_zones'):
args['zoneid'] = self.get_zone(key='id')
else:
args['zoneid'] = -1
if args['bootable'] and not args['ostypeid']:
self.module.fail_json(msg="OS type 'os_type' is required if 'bootable=true'.")
args['url'] = self.module.params.get('url')
if not args['url']:
self.module.fail_json(msg="URL is required.")
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('registerIso', **args)
self.iso = res['iso'][0]
return self.iso
def present_iso(self):
iso = self.get_iso()
if not iso:
iso = self.register_iso()
else:
iso = self.update_iso(iso)
if iso:
iso = self.ensure_tags(resource=iso, resource_type='ISO')
self.iso = iso
return iso
def update_iso(self, iso):
args = self._get_common_args()
args.update({
'id': iso['id'],
})
if self.has_changed(args, iso):
self.result['changed'] = True
if not self.module.params.get('cross_zones'):
args['zoneid'] = self.get_zone(key='id')
else:
# Workaround API does not return cross_zones=true
self.result['cross_zones'] = True
args['zoneid'] = -1
if not self.module.check_mode:
res = self.query_api('updateIso', **args)
self.iso = res['iso']
return self.iso
def get_iso(self):
if not self.iso:
args = {
'isready': self.module.params.get('is_ready'),
'isofilter': self.module.params.get('iso_filter'),
'domainid': self.get_domain('id'),
'account': self.get_account('name'),
'projectid': self.get_project('id'),
}
if not self.module.params.get('cross_zones'):
args['zoneid'] = self.get_zone(key='id')
# if checksum is set, we only look on that.
checksum = self.module.params.get('checksum')
if not checksum:
args['name'] = self.module.params.get('name')
isos = self.query_api('listIsos', **args)
if isos:
if not checksum:
self.iso = isos['iso'][0]
else:
for i in isos['iso']:
if i['checksum'] == checksum:
self.iso = i
break
return self.iso
def absent_iso(self):
iso = self.get_iso()
if iso:
self.result['changed'] = True
args = {
'id': iso['id'],
'projectid': self.get_project('id'),
}
if not self.module.params.get('cross_zones'):
args['zoneid'] = self.get_zone(key='id')
if not self.module.check_mode:
res = self.query_api('deleteIso', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'iso')
return iso
def get_result(self, iso):
super(AnsibleCloudStackIso, self).get_result(iso)
# Workaround API does not return cross_zones=true
if self.module.params.get('cross_zones'):
self.result['cross_zones'] = True
if 'zone' in self.result:
del self.result['zone']
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
url=dict(),
os_type=dict(),
zone=dict(),
cross_zones=dict(type='bool', default=False),
iso_filter=dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']),
domain=dict(),
account=dict(),
project=dict(),
checksum=dict(),
is_ready=dict(type='bool', default=False),
bootable=dict(type='bool'),
is_featured=dict(type='bool'),
is_public=dict(type='bool'),
is_dynamically_scalable=dict(type='bool'),
state=dict(choices=['present', 'absent'], default='present'),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
mutually_exclusive=(
['zone', 'cross_zones'],
),
supports_check_mode=True
)
acs_iso = AnsibleCloudStackIso(module)
state = module.params.get('state')
if state in ['absent']:
iso = acs_iso.absent_iso()
else:
iso = acs_iso.present_iso()
result = acs_iso.get_result(iso)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,378 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, Darren Worrall <darren@iweb.co.uk>
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_loadbalancer_rule
short_description: Manages load balancer rules on Apache CloudStack based clouds.
description:
- Add, update and remove load balancer rules.
author:
- Darren Worrall (@dazworrall)
- René Moser (@resmo)
options:
name:
description:
- The name of the load balancer rule.
type: str
required: true
description:
description:
- The description of the load balancer rule.
type: str
algorithm:
description:
- Load balancer algorithm
- Required when using I(state=present).
type: str
choices: [ source, roundrobin, leastconn ]
default: source
private_port:
description:
- The private port of the private ip address/virtual machine where the network traffic will be load balanced to.
- Required when using I(state=present).
- Can not be changed once the rule exists due API limitation.
type: int
public_port:
description:
- The public port from where the network traffic will be load balanced from.
- Required when using I(state=present).
- Can not be changed once the rule exists due API limitation.
type: int
required: true
ip_address:
description:
- Public IP address from where the network traffic will be load balanced from.
type: str
required: true
aliases: [ public_ip ]
open_firewall:
description:
- Whether the firewall rule for public port should be created, while creating the new rule.
- Use M(cs_firewall) for managing firewall rules.
type: bool
default: no
cidr:
description:
- CIDR (full notation) to be used for firewall rule if required.
type: str
protocol:
description:
- The protocol to be used on the load balancer
type: str
project:
description:
- Name of the project the load balancer IP address is related to.
type: str
state:
description:
- State of the rule.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the rule is related to.
type: str
account:
description:
- Account the rule is related to.
type: str
zone:
description:
- Name of the zone in which the rule should be created.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
network:
description:
- Name of the network.
type: str
vpc:
description:
- Name of the VPC.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a load balancer rule
cs_loadbalancer_rule:
name: balance_http
public_ip: 1.2.3.4
algorithm: leastconn
public_port: 80
private_port: 8080
delegate_to: localhost
- name: Update algorithm of an existing load balancer rule
cs_loadbalancer_rule:
name: balance_http
public_ip: 1.2.3.4
algorithm: roundrobin
public_port: 80
private_port: 8080
delegate_to: localhost
- name: Delete a load balancer rule
cs_loadbalancer_rule:
name: balance_http
public_ip: 1.2.3.4
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the rule.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
zone:
description: Name of zone the rule is related to.
returned: success
type: str
sample: ch-gva-2
project:
description: Name of project the rule is related to.
returned: success
type: str
sample: Production
account:
description: Account the rule is related to.
returned: success
type: str
sample: example account
domain:
description: Domain the rule is related to.
returned: success
type: str
sample: example domain
algorithm:
description: Load balancer algorithm used.
returned: success
type: str
sample: source
cidr:
description: CIDR to forward traffic from.
returned: success
type: str
sample: 0.0.0.0/0
name:
description: Name of the rule.
returned: success
type: str
sample: http-lb
description:
description: Description of the rule.
returned: success
type: str
sample: http load balancer rule
protocol:
description: Protocol of the rule.
returned: success
type: str
sample: tcp
public_port:
description: Public port.
returned: success
type: int
sample: 80
private_port:
description: Private IP address.
returned: success
type: int
sample: 80
public_ip:
description: Public IP address.
returned: success
type: str
sample: 1.2.3.4
tags:
description: List of resource tags associated with the rule.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
state:
description: State of the rule.
returned: success
type: str
sample: Add
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackLBRule(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackLBRule, self).__init__(module)
self.returns = {
'publicip': 'public_ip',
'algorithm': 'algorithm',
'cidrlist': 'cidr',
'protocol': 'protocol',
}
# these values will be casted to int
self.returns_to_int = {
'publicport': 'public_port',
'privateport': 'private_port',
}
def get_rule(self, **kwargs):
rules = self.query_api('listLoadBalancerRules', **kwargs)
if rules:
return rules['loadbalancerrule'][0]
def _get_common_args(self):
return {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'zoneid': self.get_zone(key='id') if self.module.params.get('zone') else None,
'publicipid': self.get_ip_address(key='id'),
'name': self.module.params.get('name'),
}
def present_lb_rule(self):
required_params = [
'algorithm',
'private_port',
'public_port',
]
self.module.fail_on_missing_params(required_params=required_params)
args = self._get_common_args()
rule = self.get_rule(**args)
if rule:
rule = self._update_lb_rule(rule)
else:
rule = self._create_lb_rule(rule)
if rule:
rule = self.ensure_tags(resource=rule, resource_type='LoadBalancer')
return rule
def _create_lb_rule(self, rule):
self.result['changed'] = True
if not self.module.check_mode:
args = self._get_common_args()
args.update({
'algorithm': self.module.params.get('algorithm'),
'privateport': self.module.params.get('private_port'),
'publicport': self.module.params.get('public_port'),
'cidrlist': self.module.params.get('cidr'),
'description': self.module.params.get('description'),
'protocol': self.module.params.get('protocol'),
'networkid': self.get_network(key='id'),
})
res = self.query_api('createLoadBalancerRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
rule = self.poll_job(res, 'loadbalancer')
return rule
def _update_lb_rule(self, rule):
args = {
'id': rule['id'],
'algorithm': self.module.params.get('algorithm'),
'description': self.module.params.get('description'),
}
if self.has_changed(args, rule):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateLoadBalancerRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
rule = self.poll_job(res, 'loadbalancer')
return rule
def absent_lb_rule(self):
args = self._get_common_args()
rule = self.get_rule(**args)
if rule:
self.result['changed'] = True
if rule and not self.module.check_mode:
res = self.query_api('deleteLoadBalancerRule', id=rule['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'loadbalancer')
return rule
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
description=dict(),
algorithm=dict(choices=['source', 'roundrobin', 'leastconn'], default='source'),
private_port=dict(type='int'),
public_port=dict(type='int'),
protocol=dict(),
state=dict(choices=['present', 'absent'], default='present'),
ip_address=dict(required=True, aliases=['public_ip']),
cidr=dict(),
project=dict(),
open_firewall=dict(type='bool', default=False),
tags=dict(type='list', aliases=['tag']),
zone=dict(),
domain=dict(),
account=dict(),
vpc=dict(),
network=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_lb_rule = AnsibleCloudStackLBRule(module)
state = module.params.get('state')
if state in ['absent']:
rule = acs_lb_rule.absent_lb_rule()
else:
rule = acs_lb_rule.present_lb_rule()
result = acs_lb_rule.get_result(rule)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,350 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015, Darren Worrall <darren@iweb.co.uk>
# Copyright (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_loadbalancer_rule_member
short_description: Manages load balancer rule members on Apache CloudStack based clouds.
description:
- Add and remove load balancer rule members.
author:
- Darren Worrall (@dazworrall)
- René Moser (@resmo)
options:
name:
description:
- The name of the load balancer rule.
type: str
required: true
ip_address:
description:
- Public IP address from where the network traffic will be load balanced from.
- Only needed to find the rule if I(name) is not unique.
type: str
aliases: [ public_ip ]
vms:
description:
- List of VMs to assign to or remove from the rule.
type: list
required: true
aliases: [ vm ]
state:
description:
- Should the VMs be present or absent from the rule.
type: str
default: present
choices: [ present, absent ]
project:
description:
- Name of the project the firewall rule is related to.
type: str
domain:
description:
- Domain the rule is related to.
type: str
account:
description:
- Account the rule is related to.
type: str
zone:
description:
- Name of the zone in which the rule should be located.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Add VMs to an existing load balancer
cs_loadbalancer_rule_member:
name: balance_http
vms:
- web01
- web02
delegate_to: localhost
- name: Remove a VM from an existing load balancer
cs_loadbalancer_rule_member:
name: balance_http
vms:
- web01
- web02
state: absent
delegate_to: localhost
# Rolling upgrade of hosts
- hosts: webservers
serial: 1
pre_tasks:
- name: Remove from load balancer
cs_loadbalancer_rule_member:
name: balance_http
vm: "{{ ansible_hostname }}"
state: absent
delegate_to: localhost
tasks:
# Perform update
post_tasks:
- name: Add to load balancer
cs_loadbalancer_rule_member:
name: balance_http
vm: "{{ ansible_hostname }}"
state: present
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the rule.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
zone:
description: Name of zone the rule is related to.
returned: success
type: str
sample: ch-gva-2
project:
description: Name of project the rule is related to.
returned: success
type: str
sample: Production
account:
description: Account the rule is related to.
returned: success
type: str
sample: example account
domain:
description: Domain the rule is related to.
returned: success
type: str
sample: example domain
algorithm:
description: Load balancer algorithm used.
returned: success
type: str
sample: source
cidr:
description: CIDR to forward traffic from.
returned: success
type: str
sample: 0.0.0.0/0
name:
description: Name of the rule.
returned: success
type: str
sample: http-lb
description:
description: Description of the rule.
returned: success
type: str
sample: http load balancer rule
protocol:
description: Protocol of the rule.
returned: success
type: str
sample: tcp
public_port:
description: Public port.
returned: success
type: int
sample: 80
private_port:
description: Private IP address.
returned: success
type: int
sample: 80
public_ip:
description: Public IP address.
returned: success
type: str
sample: 1.2.3.4
vms:
description: Rule members.
returned: success
type: list
sample: '[ "web01", "web02" ]'
tags:
description: List of resource tags associated with the rule.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
state:
description: State of the rule.
returned: success
type: str
sample: Add
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackLBRuleMember(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackLBRuleMember, self).__init__(module)
self.returns = {
'publicip': 'public_ip',
'algorithm': 'algorithm',
'cidrlist': 'cidr',
'protocol': 'protocol',
}
# these values will be casted to int
self.returns_to_int = {
'publicport': 'public_port',
'privateport': 'private_port',
}
def get_rule(self):
args = self._get_common_args()
args.update({
'name': self.module.params.get('name'),
'zoneid': self.get_zone(key='id') if self.module.params.get('zone') else None,
})
if self.module.params.get('ip_address'):
args['publicipid'] = self.get_ip_address(key='id')
rules = self.query_api('listLoadBalancerRules', **args)
if rules:
if len(rules['loadbalancerrule']) > 1:
self.module.fail_json(msg="More than one rule having name %s. Please pass 'ip_address' as well." % args['name'])
return rules['loadbalancerrule'][0]
return None
def _get_common_args(self):
return {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
}
def _get_members_of_rule(self, rule):
res = self.query_api('listLoadBalancerRuleInstances', id=rule['id'])
return res.get('loadbalancerruleinstance', [])
def _ensure_members(self, operation):
if operation not in ['add', 'remove']:
self.module.fail_json(msg="Bad operation: %s" % operation)
rule = self.get_rule()
if not rule:
self.module.fail_json(msg="Unknown rule: %s" % self.module.params.get('name'))
existing = {}
for vm in self._get_members_of_rule(rule=rule):
existing[vm['name']] = vm['id']
wanted_names = self.module.params.get('vms')
if operation == 'add':
cs_func = 'assignToLoadBalancerRule'
to_change = set(wanted_names) - set(existing.keys())
else:
cs_func = 'removeFromLoadBalancerRule'
to_change = set(wanted_names) & set(existing.keys())
if not to_change:
return rule
args = self._get_common_args()
args['fetch_list'] = True
vms = self.query_api('listVirtualMachines', **args)
to_change_ids = []
for name in to_change:
for vm in vms:
if vm['name'] == name:
to_change_ids.append(vm['id'])
break
else:
self.module.fail_json(msg="Unknown VM: %s" % name)
if to_change_ids:
self.result['changed'] = True
if to_change_ids and not self.module.check_mode:
res = self.query_api(
cs_func,
id=rule['id'],
virtualmachineids=to_change_ids,
)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res)
rule = self.get_rule()
return rule
def add_members(self):
return self._ensure_members('add')
def remove_members(self):
return self._ensure_members('remove')
def get_result(self, rule):
super(AnsibleCloudStackLBRuleMember, self).get_result(rule)
if rule:
self.result['vms'] = []
for vm in self._get_members_of_rule(rule=rule):
self.result['vms'].append(vm['name'])
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
ip_address=dict(aliases=['public_ip']),
vms=dict(required=True, aliases=['vm'], type='list'),
state=dict(choices=['present', 'absent'], default='present'),
zone=dict(),
domain=dict(),
project=dict(),
account=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_lb_rule_member = AnsibleCloudStackLBRuleMember(module)
state = module.params.get('state')
if state in ['absent']:
rule = acs_lb_rule_member.remove_members()
else:
rule = acs_lb_rule_member.add_members()
result = acs_lb_rule_member.get_result(rule)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,642 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_network
short_description: Manages networks on Apache CloudStack based clouds.
description:
- Create, update, restart and delete networks.
author: René Moser (@resmo)
options:
name:
description:
- Name (case sensitive) of the network.
type: str
required: true
display_text:
description:
- Display text of the network.
- If not specified, I(name) will be used as I(display_text).
type: str
network_offering:
description:
- Name of the offering for the network.
- Required if I(state=present).
type: str
start_ip:
description:
- The beginning IPv4 address of the network belongs to.
- Only considered on create.
type: str
end_ip:
description:
- The ending IPv4 address of the network belongs to.
- If not specified, value of I(start_ip) is used.
- Only considered on create.
type: str
gateway:
description:
- The gateway of the network.
- Required for shared networks and isolated networks when it belongs to a VPC.
- Only considered on create.
type: str
netmask:
description:
- The netmask of the network.
- Required for shared networks and isolated networks when it belongs to a VPC.
- Only considered on create.
type: str
start_ipv6:
description:
- The beginning IPv6 address of the network belongs to.
- Only considered on create.
type: str
end_ipv6:
description:
- The ending IPv6 address of the network belongs to.
- If not specified, value of I(start_ipv6) is used.
- Only considered on create.
type: str
cidr_ipv6:
description:
- CIDR of IPv6 network, must be at least /64.
- Only considered on create.
type: str
gateway_ipv6:
description:
- The gateway of the IPv6 network.
- Required for shared networks.
- Only considered on create.
type: str
vlan:
description:
- The ID or VID of the network.
type: str
vpc:
description:
- Name of the VPC of the network.
type: str
isolated_pvlan:
description:
- The isolated private VLAN for this network.
type: str
clean_up:
description:
- Cleanup old network elements.
- Only considered on I(state=restarted).
default: no
type: bool
acl_type:
description:
- Access control type for the network.
- If not specified, Cloudstack will default to C(account) for isolated networks
- and C(domain) for shared networks.
- Only considered on create.
type: str
choices: [ account, domain ]
acl:
description:
- The name of the access control list for the VPC network tier.
type: str
subdomain_access:
description:
- Defines whether to allow subdomains to use networks dedicated to their parent domain(s).
- Should be used with I(acl_type=domain).
- Only considered on create.
type: bool
network_domain:
description:
- The network domain.
type: str
state:
description:
- State of the network.
type: str
default: present
choices: [ present, absent, restarted ]
zone:
description:
- Name of the zone in which the network should be deployed.
- If not set, default zone is used.
type: str
project:
description:
- Name of the project the network to be deployed in.
type: str
domain:
description:
- Domain the network is related to.
type: str
account:
description:
- Account the network is related to.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a network
cs_network:
name: my network
zone: gva-01
network_offering: DefaultIsolatedNetworkOfferingWithSourceNatService
network_domain: example.com
delegate_to: localhost
- name: Create a VPC tier
cs_network:
name: my VPC tier 1
zone: gva-01
vpc: my VPC
network_offering: DefaultIsolatedNetworkOfferingForVpcNetworks
gateway: 10.43.0.1
netmask: 255.255.255.0
acl: my web acl
delegate_to: localhost
- name: Update a network
cs_network:
name: my network
display_text: network of domain example.local
network_domain: example.local
delegate_to: localhost
- name: Restart a network with clean up
cs_network:
name: my network
clean_up: yes
state: restarted
delegate_to: localhost
- name: Remove a network
cs_network:
name: my network
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the network.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the network.
returned: success
type: str
sample: web project
display_text:
description: Display text of the network.
returned: success
type: str
sample: web project
dns1:
description: IP address of the 1st nameserver.
returned: success
type: str
sample: 1.2.3.4
dns2:
description: IP address of the 2nd nameserver.
returned: success
type: str
sample: 1.2.3.4
cidr:
description: IPv4 network CIDR.
returned: success
type: str
sample: 10.101.64.0/24
gateway:
description: IPv4 gateway.
returned: success
type: str
sample: 10.101.64.1
netmask:
description: IPv4 netmask.
returned: success
type: str
sample: 255.255.255.0
cidr_ipv6:
description: IPv6 network CIDR.
returned: if available
type: str
sample: 2001:db8::/64
gateway_ipv6:
description: IPv6 gateway.
returned: if available
type: str
sample: 2001:db8::1
zone:
description: Name of zone.
returned: success
type: str
sample: ch-gva-2
domain:
description: Domain the network is related to.
returned: success
type: str
sample: ROOT
account:
description: Account the network is related to.
returned: success
type: str
sample: example account
project:
description: Name of project.
returned: success
type: str
sample: Production
tags:
description: List of resource tags associated with the network.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
acl_type:
description: Access type of the network (Domain, Account).
returned: success
type: str
sample: Account
acl:
description: Name of the access control list for the VPC network tier.
returned: success
type: str
sample: My ACL
version_added: '2.5'
acl_id:
description: ID of the access control list for the VPC network tier.
returned: success
type: str
sample: dfafcd55-0510-4b8c-b6c5-b8cedb4cfd88
version_added: '2.5'
broadcast_domain_type:
description: Broadcast domain type of the network.
returned: success
type: str
sample: Vlan
type:
description: Type of the network.
returned: success
type: str
sample: Isolated
traffic_type:
description: Traffic type of the network.
returned: success
type: str
sample: Guest
state:
description: State of the network (Allocated, Implemented, Setup).
returned: success
type: str
sample: Allocated
is_persistent:
description: Whether the network is persistent or not.
returned: success
type: bool
sample: false
network_domain:
description: The network domain
returned: success
type: str
sample: example.local
network_offering:
description: The network offering name.
returned: success
type: str
sample: DefaultIsolatedNetworkOfferingWithSourceNatService
network_offering_display_text:
description: The network offering display text.
returned: success
type: str
sample: Offering for Isolated Vpc networks with Source Nat service enabled
version_added: '2.5'
network_offering_conserve_mode:
description: Whether the network offering has IP conserve mode enabled or not.
returned: success
type: bool
sample: false
version_added: '2.5'
network_offering_availability:
description: The availability of the network offering the network is created from
returned: success
type: str
sample: Optional
version_added: '2.5'
is_system:
description: Whether the network is system related or not.
returned: success
type: bool
sample: false
version_added: '2.5'
vpc:
description: Name of the VPC.
returned: if available
type: str
sample: My VPC
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackNetwork(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackNetwork, self).__init__(module)
self.returns = {
'networkdomain': 'network_domain',
'networkofferingname': 'network_offering',
'networkofferingdisplaytext': 'network_offering_display_text',
'networkofferingconservemode': 'network_offering_conserve_mode',
'networkofferingavailability': 'network_offering_availability',
'aclid': 'acl_id',
'issystem': 'is_system',
'ispersistent': 'is_persistent',
'acltype': 'acl_type',
'type': 'type',
'traffictype': 'traffic_type',
'ip6gateway': 'gateway_ipv6',
'ip6cidr': 'cidr_ipv6',
'gateway': 'gateway',
'cidr': 'cidr',
'netmask': 'netmask',
'broadcastdomaintype': 'broadcast_domain_type',
'dns1': 'dns1',
'dns2': 'dns2',
}
self.network = None
def get_network_acl(self, key=None, acl_id=None):
if acl_id is not None:
args = {
'id': acl_id,
'vpcid': self.get_vpc(key='id'),
}
else:
acl_name = self.module.params.get('acl')
if not acl_name:
return
args = {
'name': acl_name,
'vpcid': self.get_vpc(key='id'),
}
network_acls = self.query_api('listNetworkACLLists', **args)
if network_acls:
acl = network_acls['networkacllist'][0]
return self._get_by_key(key, acl)
def get_network_offering(self, key=None):
network_offering = self.module.params.get('network_offering')
if not network_offering:
self.module.fail_json(msg="missing required arguments: network_offering")
args = {
'zoneid': self.get_zone(key='id'),
'fetch_list': True,
}
network_offerings = self.query_api('listNetworkOfferings', **args)
if network_offerings:
for no in network_offerings:
if network_offering in [no['name'], no['displaytext'], no['id']]:
return self._get_by_key(key, no)
self.module.fail_json(msg="Network offering '%s' not found" % network_offering)
def _get_args(self):
args = {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'networkdomain': self.module.params.get('network_domain'),
'networkofferingid': self.get_network_offering(key='id')
}
return args
def get_network(self, refresh=False):
if not self.network or refresh:
network = self.module.params.get('name')
args = {
'zoneid': self.get_zone(key='id'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'vpcid': self.get_vpc(key='id'),
'fetch_list': True,
}
networks = self.query_api('listNetworks', **args)
if networks:
for n in networks:
if network in [n['name'], n['displaytext'], n['id']]:
self.network = n
self.network['acl'] = self.get_network_acl(key='name', acl_id=n.get('aclid'))
break
return self.network
def present_network(self):
if self.module.params.get('acl') is not None and self.module.params.get('vpc') is None:
self.module.fail_json(msg="Missing required params: vpc")
network = self.get_network()
if not network:
network = self.create_network(network)
else:
network = self.update_network(network)
if network:
network = self.ensure_tags(resource=network, resource_type='Network')
return network
def update_network(self, network):
args = self._get_args()
args['id'] = network['id']
if self.has_changed(args, network):
self.result['changed'] = True
if not self.module.check_mode:
network = self.query_api('updateNetwork', **args)
poll_async = self.module.params.get('poll_async')
if network and poll_async:
network = self.poll_job(network, 'network')
# Skip ACL check if the network is not a VPC tier
if network.get('aclid') != self.get_network_acl(key='id'):
self.result['changed'] = True
if not self.module.check_mode:
args = {
'aclid': self.get_network_acl(key='id'),
'networkid': network['id'],
}
network = self.query_api('replaceNetworkACLList', **args)
if self.module.params.get('poll_async'):
self.poll_job(network, 'networkacllist')
network = self.get_network(refresh=True)
return network
def create_network(self, network):
self.result['changed'] = True
args = self._get_args()
args.update({
'acltype': self.module.params.get('acl_type'),
'aclid': self.get_network_acl(key='id'),
'zoneid': self.get_zone(key='id'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'startip': self.module.params.get('start_ip'),
'endip': self.get_or_fallback('end_ip', 'start_ip'),
'netmask': self.module.params.get('netmask'),
'gateway': self.module.params.get('gateway'),
'startipv6': self.module.params.get('start_ipv6'),
'endipv6': self.get_or_fallback('end_ipv6', 'start_ipv6'),
'ip6cidr': self.module.params.get('cidr_ipv6'),
'ip6gateway': self.module.params.get('gateway_ipv6'),
'vlan': self.module.params.get('vlan'),
'isolatedpvlan': self.module.params.get('isolated_pvlan'),
'subdomainaccess': self.module.params.get('subdomain_access'),
'vpcid': self.get_vpc(key='id')
})
if not self.module.check_mode:
res = self.query_api('createNetwork', **args)
network = res['network']
return network
def restart_network(self):
network = self.get_network()
if not network:
self.module.fail_json(msg="No network named '%s' found." % self.module.params('name'))
# Restarting only available for these states
if network['state'].lower() in ['implemented', 'setup']:
self.result['changed'] = True
args = {
'id': network['id'],
'cleanup': self.module.params.get('clean_up')
}
if not self.module.check_mode:
network = self.query_api('restartNetwork', **args)
poll_async = self.module.params.get('poll_async')
if network and poll_async:
network = self.poll_job(network, 'network')
return network
def absent_network(self):
network = self.get_network()
if network:
self.result['changed'] = True
args = {
'id': network['id']
}
if not self.module.check_mode:
res = self.query_api('deleteNetwork', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
self.poll_job(res, 'network')
return network
def get_result(self, network):
super(AnsibleCloudStackNetwork, self).get_result(network)
if network:
self.result['acl'] = self.get_network_acl(key='name', acl_id=network.get('aclid'))
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
network_offering=dict(),
zone=dict(),
start_ip=dict(),
end_ip=dict(),
gateway=dict(),
netmask=dict(),
start_ipv6=dict(),
end_ipv6=dict(),
cidr_ipv6=dict(),
gateway_ipv6=dict(),
vlan=dict(),
vpc=dict(),
isolated_pvlan=dict(),
clean_up=dict(type='bool', default=False),
network_domain=dict(),
subdomain_access=dict(type='bool'),
state=dict(choices=['present', 'absent', 'restarted'], default='present'),
acl=dict(),
acl_type=dict(choices=['account', 'domain']),
project=dict(),
domain=dict(),
account=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
))
required_together = cs_required_together()
required_together.extend([
['netmask', 'gateway'],
])
module = AnsibleModule(
argument_spec=argument_spec,
required_together=required_together,
supports_check_mode=True
)
acs_network = AnsibleCloudStackNetwork(module)
state = module.params.get('state')
if state == 'absent':
network = acs_network.absent_network()
elif state == 'restarted':
network = acs_network.restart_network()
else:
network = acs_network.present_network()
result = acs_network.get_result(network)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,202 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_network_acl
short_description: Manages network access control lists (ACL) on Apache CloudStack based clouds.
description:
- Create and remove network ACLs.
author: René Moser (@resmo)
options:
name:
description:
- Name of the network ACL.
type: str
required: true
description:
description:
- Description of the network ACL.
- If not set, identical to I(name).
type: str
vpc:
description:
- VPC the network ACL is related to.
type: str
required: true
state:
description:
- State of the network ACL.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the network ACL rule is related to.
type: str
account:
description:
- Account the network ACL rule is related to.
type: str
project:
description:
- Name of the project the network ACL is related to.
type: str
zone:
description:
- Name of the zone the VPC is related to.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create a network ACL
cs_network_acl:
name: Webserver ACL
description: a more detailed description of the ACL
vpc: customers
delegate_to: localhost
- name: remove a network ACL
cs_network_acl:
name: Webserver ACL
vpc: customers
state: absent
delegate_to: localhost
'''
RETURN = '''
---
name:
description: Name of the network ACL.
returned: success
type: str
sample: customer acl
description:
description: Description of the network ACL.
returned: success
type: str
sample: Example description of a network ACL
vpc:
description: VPC of the network ACL.
returned: success
type: str
sample: customer vpc
zone:
description: Zone the VPC is related to.
returned: success
type: str
sample: ch-gva-2
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackNetworkAcl(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackNetworkAcl, self).__init__(module)
def get_network_acl(self):
args = {
'name': self.module.params.get('name'),
'vpcid': self.get_vpc(key='id'),
}
network_acls = self.query_api('listNetworkACLLists', **args)
if network_acls:
return network_acls['networkacllist'][0]
return None
def present_network_acl(self):
network_acl = self.get_network_acl()
if not network_acl:
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'description': self.get_or_fallback('description', 'name'),
'vpcid': self.get_vpc(key='id')
}
if not self.module.check_mode:
res = self.query_api('createNetworkACLList', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
network_acl = self.poll_job(res, 'networkacllist')
return network_acl
def absent_network_acl(self):
network_acl = self.get_network_acl()
if network_acl:
self.result['changed'] = True
args = {
'id': network_acl['id'],
}
if not self.module.check_mode:
res = self.query_api('deleteNetworkACLList', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'networkacllist')
return network_acl
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
description=dict(),
vpc=dict(required=True),
state=dict(choices=['present', 'absent'], default='present'),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_network_acl = AnsibleCloudStackNetworkAcl(module)
state = module.params.get('state')
if state == 'absent':
network_acl = acs_network_acl.absent_network_acl()
else:
network_acl = acs_network_acl.present_network_acl()
result = acs_network_acl.get_result(network_acl)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,462 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_network_acl_rule
short_description: Manages network access control list (ACL) rules on Apache CloudStack based clouds.
description:
- Add, update and remove network ACL rules.
author: René Moser (@resmo)
options:
network_acl:
description:
- Name of the network ACL.
type: str
required: true
aliases: [ acl ]
cidrs:
description:
- CIDRs of the rule.
type: list
default: [ 0.0.0.0/0 ]
aliases: [ cidr ]
rule_position:
description:
- The position of the network ACL rule.
type: int
required: true
aliases: [ number ]
protocol:
description:
- Protocol of the rule
choices: [ tcp, udp, icmp, all, by_number ]
type: str
default: tcp
protocol_number:
description:
- Protocol number from 1 to 256 required if I(protocol=by_number).
type: int
start_port:
description:
- Start port for this rule.
- Considered if I(protocol=tcp) or I(protocol=udp).
type: int
aliases: [ port ]
end_port:
description:
- End port for this rule.
- Considered if I(protocol=tcp) or I(protocol=udp).
- If not specified, equal I(start_port).
type: int
icmp_type:
description:
- Type of the icmp message being sent.
- Considered if I(protocol=icmp).
type: int
icmp_code:
description:
- Error code for this icmp message.
- Considered if I(protocol=icmp).
type: int
vpc:
description:
- VPC the network ACL is related to.
type: str
required: true
traffic_type:
description:
- Traffic type of the rule.
type: str
choices: [ ingress, egress ]
default: ingress
aliases: [ type ]
action_policy:
description:
- Action policy of the rule.
type: str
choices: [ allow, deny ]
default: allow
aliases: [ action ]
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "If you want to delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
domain:
description:
- Domain the VPC is related to.
type: str
account:
description:
- Account the VPC is related to.
type: str
project:
description:
- Name of the project the VPC is related to.
type: str
zone:
description:
- Name of the zone the VPC related to.
- If not set, default zone is used.
type: str
state:
description:
- State of the network ACL rule.
type: str
default: present
choices: [ present, absent ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create a network ACL rule, allow port 80 ingress
cs_network_acl_rule:
network_acl: web
rule_position: 1
vpc: my vpc
traffic_type: ingress
action_policy: allow
port: 80
cidr: 0.0.0.0/0
delegate_to: localhost
- name: create a network ACL rule, deny port range 8000-9000 ingress for 10.20.0.0/16 and 10.22.0.0/16
cs_network_acl_rule:
network_acl: web
rule_position: 1
vpc: my vpc
traffic_type: ingress
action_policy: deny
start_port: 8000
end_port: 9000
cidrs:
- 10.20.0.0/16
- 10.22.0.0/16
delegate_to: localhost
- name: remove a network ACL rule
cs_network_acl_rule:
network_acl: web
rule_position: 1
vpc: my vpc
state: absent
delegate_to: localhost
'''
RETURN = '''
---
network_acl:
description: Name of the network ACL.
returned: success
type: str
sample: customer acl
cidr:
description: CIDR of the network ACL rule.
returned: success
type: str
sample: 0.0.0.0/0
cidrs:
description: CIDRs of the network ACL rule.
returned: success
type: list
sample: [ 0.0.0.0/0 ]
version_added: '2.9'
rule_position:
description: Position of the network ACL rule.
returned: success
type: int
sample: 1
action_policy:
description: Action policy of the network ACL rule.
returned: success
type: str
sample: deny
traffic_type:
description: Traffic type of the network ACL rule.
returned: success
type: str
sample: ingress
protocol:
description: Protocol of the network ACL rule.
returned: success
type: str
sample: tcp
protocol_number:
description: Protocol number in case protocol is by number.
returned: success
type: int
sample: 8
start_port:
description: Start port of the network ACL rule.
returned: success
type: int
sample: 80
end_port:
description: End port of the network ACL rule.
returned: success
type: int
sample: 80
icmp_code:
description: ICMP code of the network ACL rule.
returned: success
type: int
sample: 8
icmp_type:
description: ICMP type of the network ACL rule.
returned: success
type: int
sample: 0
state:
description: State of the network ACL rule.
returned: success
type: str
sample: Active
vpc:
description: VPC of the network ACL.
returned: success
type: str
sample: customer vpc
tags:
description: List of resource tags associated with the network ACL rule.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
domain:
description: Domain the network ACL rule is related to.
returned: success
type: str
sample: example domain
account:
description: Account the network ACL rule is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the network ACL rule is related to.
returned: success
type: str
sample: Production
zone:
description: Zone the VPC is related to.
returned: success
type: str
sample: ch-gva-2
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackNetworkAclRule(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackNetworkAclRule, self).__init__(module)
self.returns = {
'cidrlist': 'cidr',
'action': 'action_policy',
'protocol': 'protocol',
'icmpcode': 'icmp_code',
'icmptype': 'icmp_type',
'number': 'rule_position',
'traffictype': 'traffic_type',
}
# these values will be casted to int
self.returns_to_int = {
'startport': 'start_port',
'endport': 'end_port',
}
def get_network_acl_rule(self):
args = {
'aclid': self.get_network_acl(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
}
network_acl_rules = self.query_api('listNetworkACLs', **args)
for acl_rule in network_acl_rules.get('networkacl', []):
if acl_rule['number'] == self.module.params.get('rule_position'):
return acl_rule
return None
def present_network_acl_rule(self):
network_acl_rule = self.get_network_acl_rule()
protocol = self.module.params.get('protocol')
start_port = self.module.params.get('start_port')
end_port = self.get_or_fallback('end_port', 'start_port')
icmp_type = self.module.params.get('icmp_type')
icmp_code = self.module.params.get('icmp_code')
if protocol in ['tcp', 'udp'] and (start_port is None or end_port is None):
self.module.fail_json(msg="protocol is %s but the following are missing: start_port, end_port" % protocol)
elif protocol == 'icmp' and (icmp_type is None or icmp_code is None):
self.module.fail_json(msg="protocol is icmp but the following are missing: icmp_type, icmp_code")
elif protocol == 'by_number' and self.module.params.get('protocol_number') is None:
self.module.fail_json(msg="protocol is by_number but the following are missing: protocol_number")
if not network_acl_rule:
network_acl_rule = self._create_network_acl_rule(network_acl_rule)
else:
network_acl_rule = self._update_network_acl_rule(network_acl_rule)
if network_acl_rule:
network_acl_rule = self.ensure_tags(resource=network_acl_rule, resource_type='NetworkACL')
return network_acl_rule
def absent_network_acl_rule(self):
network_acl_rule = self.get_network_acl_rule()
if network_acl_rule:
self.result['changed'] = True
args = {
'id': network_acl_rule['id'],
}
if not self.module.check_mode:
res = self.query_api('deleteNetworkACL', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'networkacl')
return network_acl_rule
def _create_network_acl_rule(self, network_acl_rule):
self.result['changed'] = True
protocol = self.module.params.get('protocol')
args = {
'aclid': self.get_network_acl(key='id'),
'action': self.module.params.get('action_policy'),
'protocol': protocol if protocol != 'by_number' else self.module.params.get('protocol_number'),
'startport': self.module.params.get('start_port'),
'endport': self.get_or_fallback('end_port', 'start_port'),
'number': self.module.params.get('rule_position'),
'icmpcode': self.module.params.get('icmp_code'),
'icmptype': self.module.params.get('icmp_type'),
'traffictype': self.module.params.get('traffic_type'),
'cidrlist': self.module.params.get('cidrs'),
}
if not self.module.check_mode:
res = self.query_api('createNetworkACL', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
network_acl_rule = self.poll_job(res, 'networkacl')
return network_acl_rule
def _update_network_acl_rule(self, network_acl_rule):
protocol = self.module.params.get('protocol')
args = {
'id': network_acl_rule['id'],
'action': self.module.params.get('action_policy'),
'protocol': protocol if protocol != 'by_number' else str(self.module.params.get('protocol_number')),
'startport': self.module.params.get('start_port'),
'endport': self.get_or_fallback('end_port', 'start_port'),
'icmpcode': self.module.params.get('icmp_code'),
'icmptype': self.module.params.get('icmp_type'),
'traffictype': self.module.params.get('traffic_type'),
'cidrlist': ",".join(self.module.params.get('cidrs')),
}
if self.has_changed(args, network_acl_rule):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateNetworkACLItem', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
network_acl_rule = self.poll_job(res, 'networkacl')
return network_acl_rule
def get_result(self, network_acl_rule):
super(AnsibleCloudStackNetworkAclRule, self).get_result(network_acl_rule)
if network_acl_rule:
if 'cidrlist' in network_acl_rule:
self.result['cidrs'] = network_acl_rule['cidrlist'].split(',') or [network_acl_rule['cidrlist']]
if network_acl_rule['protocol'] not in ['tcp', 'udp', 'icmp', 'all']:
self.result['protocol_number'] = int(network_acl_rule['protocol'])
self.result['protocol'] = 'by_number'
self.result['action_policy'] = self.result['action_policy'].lower()
self.result['traffic_type'] = self.result['traffic_type'].lower()
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
network_acl=dict(required=True, aliases=['acl']),
rule_position=dict(required=True, type='int', aliases=['number']),
vpc=dict(required=True),
cidrs=dict(type='list', default=['0.0.0.0/0'], aliases=['cidr']),
protocol=dict(choices=['tcp', 'udp', 'icmp', 'all', 'by_number'], default='tcp'),
protocol_number=dict(type='int'),
traffic_type=dict(choices=['ingress', 'egress'], aliases=['type'], default='ingress'),
action_policy=dict(choices=['allow', 'deny'], aliases=['action'], default='allow'),
icmp_type=dict(type='int'),
icmp_code=dict(type='int'),
start_port=dict(type='int', aliases=['port']),
end_port=dict(type='int'),
state=dict(choices=['present', 'absent'], default='present'),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
tags=dict(type='list', aliases=['tag']),
poll_async=dict(type='bool', default=True),
))
required_together = cs_required_together()
required_together.extend([
['icmp_type', 'icmp_code'],
])
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
mutually_exclusive=(
['icmp_type', 'start_port'],
['icmp_type', 'end_port'],
),
supports_check_mode=True
)
acs_network_acl_rule = AnsibleCloudStackNetworkAclRule(module)
state = module.params.get('state')
if state == 'absent':
network_acl_rule = acs_network_acl_rule.absent_network_acl_rule()
else:
network_acl_rule = acs_network_acl_rule.present_network_acl_rule()
result = acs_network_acl_rule.get_result(network_acl_rule)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,425 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, David Passante (@dpassante)
# Copyright (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_network_offering
short_description: Manages network offerings on Apache CloudStack based clouds.
description:
- Create, update, enable, disable and remove network offerings.
author: David Passante (@dpassante)
options:
state:
description:
- State of the network offering.
type: str
choices: [ enabled, present, disabled, absent]
default: present
display_text:
description:
- Display text of the network offerings.
type: str
guest_ip_type:
description:
- Guest type of the network offering.
type: str
choices: [ Shared, Isolated ]
name:
description:
- The name of the network offering.
type: str
required: true
supported_services:
description:
- Services supported by the network offering.
- A list of one or more items from the choice list.
type: list
choices: [ Dns, PortForwarding, Dhcp, SourceNat, UserData, Firewall, StaticNat, Vpn, Lb ]
aliases: [ supported_service ]
traffic_type:
description:
- The traffic type for the network offering.
type: str
default: Guest
availability:
description:
- The availability of network offering. Default value is Optional
type: str
conserve_mode:
description:
- Whether the network offering has IP conserve mode enabled.
type: bool
details:
description:
- Network offering details in key/value pairs.
- with service provider as a value
type: list
egress_default_policy:
description:
- Whether the default egress policy is allow or to deny.
type: str
choices: [ allow, deny ]
persistent:
description:
- True if network offering supports persistent networks
- defaulted to false if not specified
type: bool
keepalive_enabled:
description:
- If true keepalive will be turned on in the loadbalancer.
- At the time of writing this has only an effect on haproxy.
- the mode http and httpclose options are unset in the haproxy conf file.
type: bool
max_connections:
description:
- Maximum number of concurrent connections supported by the network offering.
type: int
network_rate:
description:
- Data transfer rate in megabits per second allowed.
type: int
service_capabilities:
description:
- Desired service capabilities as part of network offering.
type: list
aliases: [ service_capability ]
service_offering:
description:
- The service offering name or ID used by virtual router provider.
type: str
service_providers:
description:
- Provider to service mapping.
- If not specified, the provider for the service will be mapped to the default provider on the physical network.
type: list
aliases: [ service_provider ]
specify_ip_ranges:
description:
- Whether the network offering supports specifying IP ranges.
- Defaulted to C(no) by the API if not specified.
type: bool
specify_vlan:
description:
- Whether the network offering supports vlans or not.
type: bool
for_vpc:
description:
- Whether the offering is meant to be used for VPC or not.
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a network offering and enable it
cs_network_offering:
name: my_network_offering
display_text: network offering description
state: enabled
guest_ip_type: Isolated
supported_services: [ Dns, PortForwarding, Dhcp, SourceNat, UserData, Firewall, StaticNat, Vpn, Lb ]
service_providers:
- { service: 'dns', provider: 'virtualrouter' }
- { service: 'dhcp', provider: 'virtualrouter' }
delegate_to: localhost
- name: Remove a network offering
cs_network_offering:
name: my_network_offering
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the network offering.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: The name of the network offering.
returned: success
type: str
sample: MyCustomNetworkOffering
display_text:
description: The display text of the network offering.
returned: success
type: str
sample: My network offering
state:
description: The state of the network offering.
returned: success
type: str
sample: Enabled
guest_ip_type:
description: Guest type of the network offering.
returned: success
type: str
sample: Isolated
availability:
description: The availability of network offering.
returned: success
type: str
sample: Optional
service_offering_id:
description: The service offering ID.
returned: success
type: str
sample: c5f7a5fc-43f8-11e5-a151-feff819cdc9f
max_connections:
description: The maximum number of concurrent connections to be handled by LB.
returned: success
type: int
sample: 300
network_rate:
description: The network traffic transfer ate in Mbit/s.
returned: success
type: int
sample: 200
traffic_type:
description: The traffic type.
returned: success
type: str
sample: Guest
egress_default_policy:
description: Default egress policy.
returned: success
type: str
sample: allow
is_persistent:
description: Whether persistent networks are supported or not.
returned: success
type: bool
sample: false
is_default:
description: Whether network offering is the default offering or not.
returned: success
type: bool
sample: false
for_vpc:
description: Whether the offering is meant to be used for VPC or not.
returned: success
type: bool
sample: false
version_added: '2.8'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackNetworkOffering(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackNetworkOffering, self).__init__(module)
self.returns = {
'guestiptype': 'guest_ip_type',
'availability': 'availability',
'serviceofferingid': 'service_offering_id',
'networkrate': 'network_rate',
'maxconnections': 'max_connections',
'traffictype': 'traffic_type',
'isdefault': 'is_default',
'ispersistent': 'is_persistent',
'forvpc': 'for_vpc'
}
self.network_offering = None
def get_service_offering_id(self):
service_offering = self.module.params.get('service_offering')
if not service_offering:
return None
args = {
'issystem': True
}
service_offerings = self.query_api('listServiceOfferings', **args)
if service_offerings:
for s in service_offerings['serviceoffering']:
if service_offering in [s['name'], s['id']]:
return s['id']
self.fail_json(msg="Service offering '%s' not found" % service_offering)
def get_network_offering(self):
if self.network_offering:
return self.network_offering
args = {
'name': self.module.params.get('name'),
'guestiptype': self.module.params.get('guest_type'),
}
no = self.query_api('listNetworkOfferings', **args)
if no:
self.network_offering = no['networkoffering'][0]
return self.network_offering
def create_or_update(self):
network_offering = self.get_network_offering()
if not network_offering:
network_offering = self.create_network_offering()
return self.update_network_offering(network_offering=network_offering)
def create_network_offering(self):
network_offering = None
self.result['changed'] = True
args = {
'state': self.module.params.get('state'),
'displaytext': self.module.params.get('display_text'),
'guestiptype': self.module.params.get('guest_ip_type'),
'name': self.module.params.get('name'),
'supportedservices': self.module.params.get('supported_services'),
'traffictype': self.module.params.get('traffic_type'),
'availability': self.module.params.get('availability'),
'conservemode': self.module.params.get('conserve_mode'),
'details': self.module.params.get('details'),
'egressdefaultpolicy': self.module.params.get('egress_default_policy') == 'allow',
'ispersistent': self.module.params.get('persistent'),
'keepaliveenabled': self.module.params.get('keepalive_enabled'),
'maxconnections': self.module.params.get('max_connections'),
'networkrate': self.module.params.get('network_rate'),
'servicecapabilitylist': self.module.params.get('service_capabilities'),
'serviceofferingid': self.get_service_offering_id(),
'serviceproviderlist': self.module.params.get('service_providers'),
'specifyipranges': self.module.params.get('specify_ip_ranges'),
'specifyvlan': self.module.params.get('specify_vlan'),
'forvpc': self.module.params.get('for_vpc'),
}
required_params = [
'display_text',
'guest_ip_type',
'supported_services',
'service_providers',
]
self.module.fail_on_missing_params(required_params=required_params)
if not self.module.check_mode:
res = self.query_api('createNetworkOffering', **args)
network_offering = res['networkoffering']
return network_offering
def delete_network_offering(self):
network_offering = self.get_network_offering()
if network_offering:
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('deleteNetworkOffering', id=network_offering['id'])
return network_offering
def update_network_offering(self, network_offering):
if not network_offering:
return network_offering
args = {
'id': network_offering['id'],
'state': self.module.params.get('state'),
'displaytext': self.module.params.get('display_text'),
'name': self.module.params.get('name'),
'availability': self.module.params.get('availability'),
'maxconnections': self.module.params.get('max_connections'),
}
if args['state'] in ['enabled', 'disabled']:
args['state'] = args['state'].title()
else:
del args['state']
if self.has_changed(args, network_offering):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateNetworkOffering', **args)
network_offering = res['networkoffering']
return network_offering
def get_result(self, network_offering):
super(AnsibleCloudStackNetworkOffering, self).get_result(network_offering)
if network_offering:
self.result['egress_default_policy'] = 'allow' if network_offering.get('egressdefaultpolicy') else 'deny'
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
state=dict(choices=['enabled', 'present', 'disabled', 'absent'], default='present'),
display_text=dict(),
guest_ip_type=dict(choices=['Shared', 'Isolated']),
name=dict(required=True),
supported_services=dict(type='list', aliases=['supported_service'], choices=[
'Dns',
'PortForwarding',
'Dhcp',
'SourceNat',
'UserData',
'Firewall',
'StaticNat',
'Vpn',
'Lb',
]),
traffic_type=dict(default='Guest'),
availability=dict(),
conserve_mode=dict(type='bool'),
details=dict(type='list'),
egress_default_policy=dict(choices=['allow', 'deny']),
persistent=dict(type='bool'),
keepalive_enabled=dict(type='bool'),
max_connections=dict(type='int'),
network_rate=dict(type='int'),
service_capabilities=dict(type='list', aliases=['service_capability']),
service_offering=dict(),
service_providers=dict(type='list', aliases=['service_provider']),
specify_ip_ranges=dict(type='bool'),
specify_vlan=dict(type='bool'),
for_vpc=dict(type='bool'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_network_offering = AnsibleCloudStackNetworkOffering(module)
state = module.params.get('state')
if state in ['absent']:
network_offering = acs_network_offering.delete_network_offering()
else:
network_offering = acs_network_offering.create_or_update()
result = acs_network_offering.get_result(network_offering)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,483 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, Netservers Ltd. <support@netservers.co.uk>
# 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: cs_physical_network
short_description: Manages physical networks on Apache CloudStack based clouds.
description:
- Create, update and remove networks.
- Enabled and disabled Network Service Providers
- Enables Internal LoadBalancer and VPC/VirtualRouter elements as required
author:
- Netservers Ltd. (@netservers)
- Patryk Cichy (@PatTheSilent)
options:
name:
description:
- Name of the physical network.
required: true
aliases:
- physical_network
type: str
zone:
description:
- Name of the zone in which the network belongs.
- If not set, default zone is used.
type: str
broadcast_domain_range:
description:
- broadcast domain range for the physical network[Pod or Zone].
choices: [ POD, ZONE ]
type: str
domain:
description:
- Domain the network is owned by.
type: str
isolation_method:
description:
- Isolation method for the physical network.
choices: [ VLAN, GRE, L3 ]
type: str
network_speed:
description:
- The speed for the physical network.
choices: [1G, 10G]
type: str
tags:
description:
- A tag to identify this network.
- Physical networks support only one tag.
- To remove an existing tag pass an empty string.
aliases:
- tag
type: str
vlan:
description:
- The VLAN/VNI Ranges of the physical network.
type: str
nsps_enabled:
description:
- List of Network Service Providers to enable.
type: list
nsps_disabled:
description:
- List of Network Service Providers to disable.
type: list
state:
description:
- State of the physical network.
default: present
type: str
choices: [ present, absent, disabled, enabled ]
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a network is present
cs_physical_network:
name: net01
zone: zone01
isolation_method: VLAN
broadcast_domain_range: ZONE
delegate_to: localhost
- name: Set a tag on a network
cs_physical_network:
name: net01
tag: overlay
delegate_to: localhost
- name: Remove tag on a network
cs_physical_network:
name: net01
tag: ""
delegate_to: localhost
- name: Ensure a network is enabled with specific nsps enabled
cs_physical_network:
name: net01
zone: zone01
isolation_method: VLAN
vlan: 100-200,300-400
broadcast_domain_range: ZONE
state: enabled
nsps_enabled:
- virtualrouter
- internallbvm
- vpcvirtualrouter
delegate_to: localhost
- name: Ensure a network is disabled
cs_physical_network:
name: net01
zone: zone01
state: disabled
delegate_to: localhost
- name: Ensure a network is enabled
cs_physical_network:
name: net01
zone: zone01
state: enabled
delegate_to: localhost
- name: Ensure a network is absent
cs_physical_network:
name: net01
zone: zone01
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the network.
returned: success
type: str
sample: 3f8f25cd-c498-443f-9058-438cfbcbff50
name:
description: Name of the network.
returned: success
type: str
sample: net01
state:
description: State of the network [Enabled/Disabled].
returned: success
type: str
sample: Enabled
broadcast_domain_range:
description: broadcastdomainrange of the network [POD / ZONE].
returned: success
type: str
sample: ZONE
isolation_method:
description: isolationmethod of the network [VLAN/GRE/L3].
returned: success
type: str
sample: VLAN
network_speed:
description: networkspeed of the network [1G/10G].
returned: success
type: str
sample: 1G
zone:
description: Name of zone the physical network is in.
returned: success
type: str
sample: ch-gva-2
domain:
description: Name of domain the network is in.
returned: success
type: str
sample: domain1
nsps:
description: list of enabled or disabled Network Service Providers
type: complex
returned: on enabling/disabling of Network Service Providers
contains:
enabled:
description: list of Network Service Providers that were enabled
returned: on Network Service Provider enabling
type: list
sample:
- virtualrouter
disabled:
description: list of Network Service Providers that were disabled
returned: on Network Service Provider disabling
type: list
sample:
- internallbvm
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackPhysicalNetwork(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackPhysicalNetwork, self).__init__(module)
self.returns = {
'isolationmethods': 'isolation_method',
'broadcastdomainrange': 'broadcast_domain_range',
'networkspeed': 'network_speed',
'vlan': 'vlan',
'tags': 'tags',
}
self.nsps = []
self.vrouters = None
self.loadbalancers = None
def _get_common_args(self):
args = {
'name': self.module.params.get('name'),
'isolationmethods': self.module.params.get('isolation_method'),
'broadcastdomainrange': self.module.params.get('broadcast_domain_range'),
'networkspeed': self.module.params.get('network_speed'),
'tags': self.module.params.get('tags'),
'vlan': self.module.params.get('vlan'),
}
state = self.module.params.get('state')
if state in ['enabled', 'disabled']:
args['state'] = state.capitalize()
return args
def get_physical_network(self, key=None):
physical_network = self.module.params.get('name')
if self.physical_network:
return self._get_by_key(key, self.physical_network)
args = {
'zoneid': self.get_zone(key='id')
}
physical_networks = self.query_api('listPhysicalNetworks', **args)
if physical_networks:
for net in physical_networks['physicalnetwork']:
if physical_network.lower() in [net['name'].lower(), net['id']]:
self.physical_network = net
self.result['physical_network'] = net['name']
break
return self._get_by_key(key, self.physical_network)
def get_nsp(self, name=None):
if not self.nsps:
args = {
'physicalnetworkid': self.get_physical_network(key='id')
}
res = self.query_api('listNetworkServiceProviders', **args)
self.nsps = res['networkserviceprovider']
names = []
for nsp in self.nsps:
names.append(nsp['name'])
if nsp['name'].lower() == name.lower():
return nsp
self.module.fail_json(msg="Failed: '{0}' not in network service providers list '[{1}]'".format(name, names))
def update_nsp(self, name=None, state=None, service_list=None):
nsp = self.get_nsp(name)
if not service_list and nsp['state'] == state:
return nsp
args = {
'id': nsp['id'],
'servicelist': service_list,
'state': state
}
if not self.module.check_mode:
res = self.query_api('updateNetworkServiceProvider', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
nsp = self.poll_job(res, 'networkserviceprovider')
self.result['changed'] = True
return nsp
def get_vrouter_element(self, nsp_name='virtualrouter'):
nsp = self.get_nsp(nsp_name)
nspid = nsp['id']
if self.vrouters is None:
self.vrouters = dict()
res = self.query_api('listVirtualRouterElements', )
for vrouter in res['virtualrouterelement']:
self.vrouters[vrouter['nspid']] = vrouter
if nspid not in self.vrouters:
self.module.fail_json(msg="Failed: No VirtualRouterElement found for nsp '%s'" % nsp_name)
return self.vrouters[nspid]
def get_loadbalancer_element(self, nsp_name='internallbvm'):
nsp = self.get_nsp(nsp_name)
nspid = nsp['id']
if self.loadbalancers is None:
self.loadbalancers = dict()
res = self.query_api('listInternalLoadBalancerElements', )
for loadbalancer in res['internalloadbalancerelement']:
self.loadbalancers[loadbalancer['nspid']] = loadbalancer
if nspid not in self.loadbalancers:
self.module.fail_json(msg="Failed: No Loadbalancer found for nsp '%s'" % nsp_name)
return self.loadbalancers[nspid]
def set_vrouter_element_state(self, enabled, nsp_name='virtualrouter'):
vrouter = self.get_vrouter_element(nsp_name)
if vrouter['enabled'] == enabled:
return vrouter
args = {
'id': vrouter['id'],
'enabled': enabled
}
if not self.module.check_mode:
res = self.query_api('configureVirtualRouterElement', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vrouter = self.poll_job(res, 'virtualrouterelement')
self.result['changed'] = True
return vrouter
def set_loadbalancer_element_state(self, enabled, nsp_name='internallbvm'):
loadbalancer = self.get_loadbalancer_element(nsp_name=nsp_name)
if loadbalancer['enabled'] == enabled:
return loadbalancer
args = {
'id': loadbalancer['id'],
'enabled': enabled
}
if not self.module.check_mode:
res = self.query_api('configureInternalLoadBalancerElement', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
loadbalancer = self.poll_job(res, 'internalloadbalancerelement')
self.result['changed'] = True
return loadbalancer
def present_network(self):
network = self.get_physical_network()
if network:
network = self._update_network()
else:
network = self._create_network()
return network
def _create_network(self):
self.result['changed'] = True
args = dict(zoneid=self.get_zone(key='id'))
args.update(self._get_common_args())
if self.get_domain(key='id'):
args['domainid'] = self.get_domain(key='id')
if not self.module.check_mode:
resource = self.query_api('createPhysicalNetwork', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.network = self.poll_job(resource, 'physicalnetwork')
return self.network
def _update_network(self):
network = self.get_physical_network()
args = dict(id=network['id'])
args.update(self._get_common_args())
if self.has_changed(args, network):
self.result['changed'] = True
if not self.module.check_mode:
resource = self.query_api('updatePhysicalNetwork', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.physical_network = self.poll_job(resource, 'physicalnetwork')
return self.physical_network
def absent_network(self):
physical_network = self.get_physical_network()
if physical_network:
self.result['changed'] = True
args = {
'id': physical_network['id'],
}
if not self.module.check_mode:
resource = self.query_api('deletePhysicalNetwork', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(resource, 'success')
return physical_network
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True, aliases=['physical_network']),
zone=dict(),
domain=dict(),
vlan=dict(),
nsps_disabled=dict(type='list'),
nsps_enabled=dict(type='list'),
network_speed=dict(choices=['1G', '10G']),
broadcast_domain_range=dict(choices=['POD', 'ZONE']),
isolation_method=dict(choices=['VLAN', 'GRE', 'L3']),
state=dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'),
tags=dict(aliases=['tag']),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_network = AnsibleCloudStackPhysicalNetwork(module)
state = module.params.get('state')
nsps_disabled = module.params.get('nsps_disabled', [])
nsps_enabled = module.params.get('nsps_enabled', [])
if state in ['absent']:
network = acs_network.absent_network()
else:
network = acs_network.present_network()
if nsps_disabled is not None:
for name in nsps_disabled:
acs_network.update_nsp(name=name, state='Disabled')
if nsps_enabled is not None:
for nsp_name in nsps_enabled:
if nsp_name.lower() in ['virtualrouter', 'vpcvirtualrouter']:
acs_network.set_vrouter_element_state(enabled=True, nsp_name=nsp_name)
elif nsp_name.lower() == 'internallbvm':
acs_network.set_loadbalancer_element_state(enabled=True, nsp_name=nsp_name)
acs_network.update_nsp(name=nsp_name, state='Enabled')
result = acs_network.get_result(network)
if nsps_enabled:
result['nsps_enabled'] = nsps_enabled
if nsps_disabled:
result['nsps_disabled'] = nsps_disabled
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,297 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_pod
short_description: Manages pods on Apache CloudStack based clouds.
description:
- Create, update, delete pods.
author: René Moser (@resmo)
options:
name:
description:
- Name of the pod.
type: str
required: true
id:
description:
- uuid of the existing pod.
type: str
start_ip:
description:
- Starting IP address for the Pod.
- Required on I(state=present)
type: str
end_ip:
description:
- Ending IP address for the Pod.
type: str
netmask:
description:
- Netmask for the Pod.
- Required on I(state=present)
type: str
gateway:
description:
- Gateway for the Pod.
- Required on I(state=present)
type: str
zone:
description:
- Name of the zone in which the pod belongs to.
- If not set, default zone is used.
type: str
state:
description:
- State of the pod.
type: str
default: present
choices: [ present, enabled, disabled, absent ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a pod is present
cs_pod:
name: pod1
zone: ch-zrh-ix-01
start_ip: 10.100.10.101
gateway: 10.100.10.1
netmask: 255.255.255.0
delegate_to: localhost
- name: Ensure a pod is disabled
cs_pod:
name: pod1
zone: ch-zrh-ix-01
state: disabled
delegate_to: localhost
- name: Ensure a pod is enabled
cs_pod:
name: pod1
zone: ch-zrh-ix-01
state: enabled
delegate_to: localhost
- name: Ensure a pod is absent
cs_pod:
name: pod1
zone: ch-zrh-ix-01
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the pod.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the pod.
returned: success
type: str
sample: pod01
start_ip:
description: Starting IP of the pod.
returned: success
type: str
sample: 10.100.1.101
end_ip:
description: Ending IP of the pod.
returned: success
type: str
sample: 10.100.1.254
netmask:
description: Netmask of the pod.
returned: success
type: str
sample: 255.255.255.0
gateway:
description: Gateway of the pod.
returned: success
type: str
sample: 10.100.1.1
allocation_state:
description: State of the pod.
returned: success
type: str
sample: Enabled
zone:
description: Name of zone the pod is in.
returned: success
type: str
sample: ch-gva-2
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackPod(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackPod, self).__init__(module)
self.returns = {
'endip': 'end_ip',
'startip': 'start_ip',
'gateway': 'gateway',
'netmask': 'netmask',
'allocationstate': 'allocation_state',
}
self.pod = None
def _get_common_pod_args(self):
args = {
'name': self.module.params.get('name'),
'zoneid': self.get_zone(key='id'),
'startip': self.module.params.get('start_ip'),
'endip': self.module.params.get('end_ip'),
'netmask': self.module.params.get('netmask'),
'gateway': self.module.params.get('gateway')
}
state = self.module.params.get('state')
if state in ['enabled', 'disabled']:
args['allocationstate'] = state.capitalize()
return args
def get_pod(self):
if not self.pod:
args = {
'zoneid': self.get_zone(key='id')
}
uuid = self.module.params.get('id')
if uuid:
args['id'] = uuid
else:
args['name'] = self.module.params.get('name')
pods = self.query_api('listPods', **args)
if pods:
for pod in pods['pod']:
if not args['name']:
self.pod = self._transform_ip_list(pod)
break
elif args['name'] == pod['name']:
self.pod = self._transform_ip_list(pod)
break
return self.pod
def present_pod(self):
pod = self.get_pod()
if pod:
pod = self._update_pod()
else:
pod = self._create_pod()
return pod
def _create_pod(self):
required_params = [
'start_ip',
'netmask',
'gateway',
]
self.module.fail_on_missing_params(required_params=required_params)
pod = None
self.result['changed'] = True
args = self._get_common_pod_args()
if not self.module.check_mode:
res = self.query_api('createPod', **args)
pod = res['pod']
return pod
def _update_pod(self):
pod = self.get_pod()
args = self._get_common_pod_args()
args['id'] = pod['id']
if self.has_changed(args, pod):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updatePod', **args)
pod = res['pod']
return pod
def absent_pod(self):
pod = self.get_pod()
if pod:
self.result['changed'] = True
args = {
'id': pod['id']
}
if not self.module.check_mode:
self.query_api('deletePod', **args)
return pod
def _transform_ip_list(self, resource):
""" Workaround for 4.11 return API break """
keys = ['endip', 'startip']
if resource:
for key in keys:
if key in resource and isinstance(resource[key], list):
resource[key] = resource[key][0]
return resource
def get_result(self, pod):
pod = self._transform_ip_list(pod)
super(AnsibleCloudStackPod, self).get_result(pod)
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
id=dict(),
name=dict(required=True),
gateway=dict(),
netmask=dict(),
start_ip=dict(),
end_ip=dict(),
zone=dict(),
state=dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_pod = AnsibleCloudStackPod(module)
state = module.params.get('state')
if state in ['absent']:
pod = acs_pod.absent_pod()
else:
pod = acs_pod.present_pod()
result = acs_pod.get_result(pod)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,396 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_portforward
short_description: Manages port forwarding rules on Apache CloudStack based clouds.
description:
- Create, update and remove port forwarding rules.
author: René Moser (@resmo)
options:
ip_address:
description:
- Public IP address the rule is assigned to.
type: str
required: true
vm:
description:
- Name of virtual machine which we make the port forwarding rule for.
- Required if I(state=present).
type: str
state:
description:
- State of the port forwarding rule.
type: str
default: present
choices: [ present, absent ]
protocol:
description:
- Protocol of the port forwarding rule.
type: str
default: tcp
choices: [ tcp, udp ]
public_port:
description:
- Start public port for this rule.
type: int
required: true
public_end_port:
description:
- End public port for this rule.
- If not specified equal I(public_port).
type: int
private_port:
description:
- Start private port for this rule.
type: int
required: true
private_end_port:
description:
- End private port for this rule.
- If not specified equal I(private_port).
type: int
open_firewall:
description:
- Whether the firewall rule for public port should be created, while creating the new rule.
- Use M(cs_firewall) for managing firewall rules.
default: no
type: bool
vm_guest_ip:
description:
- VM guest NIC secondary IP address for the port forwarding rule.
type: str
network:
description:
- Name of the network.
type: str
vpc:
description:
- Name of the VPC.
type: str
domain:
description:
- Domain the I(vm) is related to.
type: str
account:
description:
- Account the I(vm) is related to.
type: str
project:
description:
- Name of the project the I(vm) is located in.
type: str
zone:
description:
- Name of the zone in which the virtual machine is in.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: 1.2.3.4:80 -> web01:8080
cs_portforward:
ip_address: 1.2.3.4
vm: web01
public_port: 80
private_port: 8080
delegate_to: localhost
- name: forward SSH and open firewall
cs_portforward:
ip_address: '{{ public_ip }}'
vm: '{{ inventory_hostname }}'
public_port: '{{ ansible_ssh_port }}'
private_port: 22
open_firewall: true
delegate_to: localhost
- name: forward DNS traffic, but do not open firewall
cs_portforward:
ip_address: 1.2.3.4
vm: '{{ inventory_hostname }}'
public_port: 53
private_port: 53
protocol: udp
delegate_to: localhost
- name: remove ssh port forwarding
cs_portforward:
ip_address: 1.2.3.4
public_port: 22
private_port: 22
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the public IP address.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
ip_address:
description: Public IP address.
returned: success
type: str
sample: 1.2.3.4
protocol:
description: Protocol.
returned: success
type: str
sample: tcp
private_port:
description: Start port on the virtual machine's IP address.
returned: success
type: int
sample: 80
private_end_port:
description: End port on the virtual machine's IP address.
returned: success
type: int
sample: 80
public_port:
description: Start port on the public IP address.
returned: success
type: int
sample: 80
public_end_port:
description: End port on the public IP address.
returned: success
type: int
sample: 80
tags:
description: Tags related to the port forwarding.
returned: success
type: list
sample: []
vm_name:
description: Name of the virtual machine.
returned: success
type: str
sample: web-01
vm_display_name:
description: Display name of the virtual machine.
returned: success
type: str
sample: web-01
vm_guest_ip:
description: IP of the virtual machine.
returned: success
type: str
sample: 10.101.65.152
vpc:
description: Name of the VPC.
returned: success
type: str
sample: my_vpc
network:
description: Name of the network.
returned: success
type: str
sample: dmz
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together
class AnsibleCloudStackPortforwarding(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackPortforwarding, self).__init__(module)
self.returns = {
'virtualmachinedisplayname': 'vm_display_name',
'virtualmachinename': 'vm_name',
'ipaddress': 'ip_address',
'vmguestip': 'vm_guest_ip',
'publicip': 'public_ip',
'protocol': 'protocol',
}
# these values will be casted to int
self.returns_to_int = {
'publicport': 'public_port',
'publicendport': 'public_end_port',
'privateport': 'private_port',
'privateendport': 'private_end_port',
}
self.portforwarding_rule = None
def get_portforwarding_rule(self):
if not self.portforwarding_rule:
protocol = self.module.params.get('protocol')
public_port = self.module.params.get('public_port')
args = {
'ipaddressid': self.get_ip_address(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
}
portforwarding_rules = self.query_api('listPortForwardingRules', **args)
if portforwarding_rules and 'portforwardingrule' in portforwarding_rules:
for rule in portforwarding_rules['portforwardingrule']:
if (protocol == rule['protocol'] and
public_port == int(rule['publicport'])):
self.portforwarding_rule = rule
break
return self.portforwarding_rule
def present_portforwarding_rule(self):
portforwarding_rule = self.get_portforwarding_rule()
if portforwarding_rule:
portforwarding_rule = self.update_portforwarding_rule(portforwarding_rule)
else:
portforwarding_rule = self.create_portforwarding_rule()
if portforwarding_rule:
portforwarding_rule = self.ensure_tags(resource=portforwarding_rule, resource_type='PortForwardingRule')
self.portforwarding_rule = portforwarding_rule
return portforwarding_rule
def create_portforwarding_rule(self):
args = {
'protocol': self.module.params.get('protocol'),
'publicport': self.module.params.get('public_port'),
'publicendport': self.get_or_fallback('public_end_port', 'public_port'),
'privateport': self.module.params.get('private_port'),
'privateendport': self.get_or_fallback('private_end_port', 'private_port'),
'openfirewall': self.module.params.get('open_firewall'),
'vmguestip': self.get_vm_guest_ip(),
'ipaddressid': self.get_ip_address(key='id'),
'virtualmachineid': self.get_vm(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'networkid': self.get_network(key='id'),
}
portforwarding_rule = None
self.result['changed'] = True
if not self.module.check_mode:
portforwarding_rule = self.query_api('createPortForwardingRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
portforwarding_rule = self.poll_job(portforwarding_rule, 'portforwardingrule')
return portforwarding_rule
def update_portforwarding_rule(self, portforwarding_rule):
args = {
'protocol': self.module.params.get('protocol'),
'publicport': self.module.params.get('public_port'),
'publicendport': self.get_or_fallback('public_end_port', 'public_port'),
'privateport': self.module.params.get('private_port'),
'privateendport': self.get_or_fallback('private_end_port', 'private_port'),
'vmguestip': self.get_vm_guest_ip(),
'ipaddressid': self.get_ip_address(key='id'),
'virtualmachineid': self.get_vm(key='id'),
'networkid': self.get_network(key='id'),
}
if self.has_changed(args, portforwarding_rule):
self.result['changed'] = True
if not self.module.check_mode:
# API broken in 4.2.1?, workaround using remove/create instead of update
# portforwarding_rule = self.query_api('updatePortForwardingRule', **args)
self.absent_portforwarding_rule()
portforwarding_rule = self.query_api('createPortForwardingRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
portforwarding_rule = self.poll_job(portforwarding_rule, 'portforwardingrule')
return portforwarding_rule
def absent_portforwarding_rule(self):
portforwarding_rule = self.get_portforwarding_rule()
if portforwarding_rule:
self.result['changed'] = True
args = {
'id': portforwarding_rule['id'],
}
if not self.module.check_mode:
res = self.query_api('deletePortForwardingRule', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'portforwardingrule')
return portforwarding_rule
def get_result(self, portforwarding_rule):
super(AnsibleCloudStackPortforwarding, self).get_result(portforwarding_rule)
if portforwarding_rule:
for search_key, return_key in self.returns_to_int.items():
if search_key in portforwarding_rule:
self.result[return_key] = int(portforwarding_rule[search_key])
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
ip_address=dict(required=True),
protocol=dict(choices=['tcp', 'udp'], default='tcp'),
public_port=dict(type='int', required=True),
public_end_port=dict(type='int'),
private_port=dict(type='int', required=True),
private_end_port=dict(type='int'),
state=dict(choices=['present', 'absent'], default='present'),
open_firewall=dict(type='bool', default=False),
vm_guest_ip=dict(),
vm=dict(),
vpc=dict(),
network=dict(),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_pf = AnsibleCloudStackPortforwarding(module)
state = module.params.get('state')
if state in ['absent']:
pf_rule = acs_pf.absent_portforwarding_rule()
else:
pf_rule = acs_pf.present_portforwarding_rule()
result = acs_pf.get_result(pf_rule)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,279 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_project
short_description: Manages projects on Apache CloudStack based clouds.
description:
- Create, update, suspend, activate and remove projects.
author: René Moser (@resmo)
options:
name:
description:
- Name of the project.
type: str
required: true
display_text:
description:
- Display text of the project.
- If not specified, I(name) will be used as I(display_text).
type: str
state:
description:
- State of the project.
type: str
default: present
choices: [ present, absent, active, suspended ]
domain:
description:
- Domain the project is related to.
type: str
account:
description:
- Account the project is related to.
type: str
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "If you want to delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a project
cs_project:
name: web
tags:
- { key: admin, value: john }
- { key: foo, value: bar }
delegate_to: localhost
- name: Rename a project
cs_project:
name: web
display_text: my web project
delegate_to: localhost
- name: Suspend an existing project
cs_project:
name: web
state: suspended
delegate_to: localhost
- name: Activate an existing project
cs_project:
name: web
state: active
delegate_to: localhost
- name: Remove a project
cs_project:
name: web
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the project.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the project.
returned: success
type: str
sample: web project
display_text:
description: Display text of the project.
returned: success
type: str
sample: web project
state:
description: State of the project.
returned: success
type: str
sample: Active
domain:
description: Domain the project is related to.
returned: success
type: str
sample: example domain
account:
description: Account the project is related to.
returned: success
type: str
sample: example account
tags:
description: List of resource tags associated with the project.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackProject(AnsibleCloudStack):
def get_project(self):
if not self.project:
project = self.module.params.get('name')
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'fetch_list': True,
}
projects = self.query_api('listProjects', **args)
if projects:
for p in projects:
if project.lower() in [p['name'].lower(), p['id']]:
self.project = p
break
return self.project
def present_project(self):
project = self.get_project()
if not project:
project = self.create_project(project)
else:
project = self.update_project(project)
if project:
project = self.ensure_tags(resource=project, resource_type='project')
# refresh resource
self.project = project
return project
def update_project(self, project):
args = {
'id': project['id'],
'displaytext': self.get_or_fallback('display_text', 'name')
}
if self.has_changed(args, project):
self.result['changed'] = True
if not self.module.check_mode:
project = self.query_api('updateProject', **args)
poll_async = self.module.params.get('poll_async')
if project and poll_async:
project = self.poll_job(project, 'project')
return project
def create_project(self, project):
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'account': self.get_account('name'),
'domainid': self.get_domain('id')
}
if not self.module.check_mode:
project = self.query_api('createProject', **args)
poll_async = self.module.params.get('poll_async')
if project and poll_async:
project = self.poll_job(project, 'project')
return project
def state_project(self, state='active'):
project = self.present_project()
if project['state'].lower() != state:
self.result['changed'] = True
args = {
'id': project['id']
}
if not self.module.check_mode:
if state == 'suspended':
project = self.query_api('suspendProject', **args)
else:
project = self.query_api('activateProject', **args)
poll_async = self.module.params.get('poll_async')
if project and poll_async:
project = self.poll_job(project, 'project')
return project
def absent_project(self):
project = self.get_project()
if project:
self.result['changed'] = True
args = {
'id': project['id']
}
if not self.module.check_mode:
res = self.query_api('deleteProject', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
res = self.poll_job(res, 'project')
return project
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
state=dict(choices=['present', 'absent', 'active', 'suspended'], default='present'),
domain=dict(),
account=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_project = AnsibleCloudStackProject(module)
state = module.params.get('state')
if state in ['absent']:
project = acs_project.absent_project()
elif state in ['active', 'suspended']:
project = acs_project.state_project(state=state)
else:
project = acs_project.present_project()
result = acs_project.get_result(project)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,193 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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: cs_region
short_description: Manages regions on Apache CloudStack based clouds.
description:
- Add, update and remove regions.
author: René Moser (@resmo)
options:
id:
description:
- ID of the region.
- Must be an number (int).
type: int
required: true
name:
description:
- Name of the region.
- Required if I(state=present)
type: str
endpoint:
description:
- Endpoint URL of the region.
- Required if I(state=present)
type: str
state:
description:
- State of the region.
type: str
default: present
choices: [ present, absent ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create a region
cs_region:
id: 2
name: geneva
endpoint: https://cloud.gva.example.com
delegate_to: localhost
- name: remove a region with ID 2
cs_region:
id: 2
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: ID of the region.
returned: success
type: int
sample: 1
name:
description: Name of the region.
returned: success
type: str
sample: local
endpoint:
description: Endpoint of the region.
returned: success
type: str
sample: http://cloud.example.com
gslb_service_enabled:
description: Whether the GSLB service is enabled or not.
returned: success
type: bool
sample: true
portable_ip_service_enabled:
description: Whether the portable IP service is enabled or not.
returned: success
type: bool
sample: true
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackRegion(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackRegion, self).__init__(module)
self.returns = {
'endpoint': 'endpoint',
'gslbserviceenabled': 'gslb_service_enabled',
'portableipserviceenabled': 'portable_ip_service_enabled',
}
def get_region(self):
id = self.module.params.get('id')
regions = self.query_api('listRegions', id=id)
if regions:
return regions['region'][0]
return None
def present_region(self):
region = self.get_region()
if not region:
region = self._create_region(region=region)
else:
region = self._update_region(region=region)
return region
def _create_region(self, region):
self.result['changed'] = True
args = {
'id': self.module.params.get('id'),
'name': self.module.params.get('name'),
'endpoint': self.module.params.get('endpoint')
}
if not self.module.check_mode:
res = self.query_api('addRegion', **args)
region = res['region']
return region
def _update_region(self, region):
args = {
'id': self.module.params.get('id'),
'name': self.module.params.get('name'),
'endpoint': self.module.params.get('endpoint')
}
if self.has_changed(args, region):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateRegion', **args)
region = res['region']
return region
def absent_region(self):
region = self.get_region()
if region:
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('removeRegion', id=region['id'])
return region
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
id=dict(required=True, type='int'),
name=dict(),
endpoint=dict(),
state=dict(choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
required_if=[
('state', 'present', ['name', 'endpoint']),
],
supports_check_mode=True
)
acs_region = AnsibleCloudStackRegion(module)
state = module.params.get('state')
if state == 'absent':
region = acs_region.absent_region()
else:
region = acs_region.present_region()
result = acs_region.get_result(region)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,208 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_resourcelimit
short_description: Manages resource limits on Apache CloudStack based clouds.
description:
- Manage limits of resources for domains, accounts and projects.
author: René Moser (@resmo)
options:
resource_type:
description:
- Type of the resource.
type: str
required: true
choices:
- instance
- ip_address
- volume
- snapshot
- template
- network
- vpc
- cpu
- memory
- primary_storage
- secondary_storage
aliases: [ type ]
limit:
description:
- Maximum number of the resource.
- Default is unlimited C(-1).
type: int
default: -1
aliases: [ max ]
domain:
description:
- Domain the resource is related to.
type: str
account:
description:
- Account the resource is related to.
type: str
project:
description:
- Name of the project the resource is related to.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Update a resource limit for instances of a domain
cs_resourcelimit:
type: instance
limit: 10
domain: customers
delegate_to: localhost
- name: Update a resource limit for instances of an account
cs_resourcelimit:
type: instance
limit: 12
account: moserre
domain: customers
delegate_to: localhost
'''
RETURN = '''
---
recource_type:
description: Type of the resource
returned: success
type: str
sample: instance
limit:
description: Maximum number of the resource.
returned: success
type: int
sample: -1
domain:
description: Domain the resource is related to.
returned: success
type: str
sample: example domain
account:
description: Account the resource is related to.
returned: success
type: str
sample: example account
project:
description: Project the resource is related to.
returned: success
type: str
sample: example project
'''
# import cloudstack common
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_required_together,
cs_argument_spec
)
RESOURCE_TYPES = {
'instance': 0,
'ip_address': 1,
'volume': 2,
'snapshot': 3,
'template': 4,
'network': 6,
'vpc': 7,
'cpu': 8,
'memory': 9,
'primary_storage': 10,
'secondary_storage': 11,
}
class AnsibleCloudStackResourceLimit(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackResourceLimit, self).__init__(module)
self.returns = {
'max': 'limit',
}
def get_resource_type(self):
resource_type = self.module.params.get('resource_type')
return RESOURCE_TYPES.get(resource_type)
def get_resource_limit(self):
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'resourcetype': self.get_resource_type()
}
resource_limit = self.query_api('listResourceLimits', **args)
if resource_limit:
if 'limit' in resource_limit['resourcelimit'][0]:
resource_limit['resourcelimit'][0]['limit'] = int(resource_limit['resourcelimit'][0])
return resource_limit['resourcelimit'][0]
self.module.fail_json(msg="Resource limit type '%s' not found." % self.module.params.get('resource_type'))
def update_resource_limit(self):
resource_limit = self.get_resource_limit()
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'resourcetype': self.get_resource_type(),
'max': self.module.params.get('limit', -1)
}
if self.has_changed(args, resource_limit):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateResourceLimit', **args)
resource_limit = res['resourcelimit']
return resource_limit
def get_result(self, resource_limit):
self.result = super(AnsibleCloudStackResourceLimit, self).get_result(resource_limit)
self.result['resource_type'] = self.module.params.get('resource_type')
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
resource_type=dict(required=True, choices=RESOURCE_TYPES.keys(), aliases=['type']),
limit=dict(default=-1, aliases=['max'], type='int'),
domain=dict(),
account=dict(),
project=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_resource_limit = AnsibleCloudStackResourceLimit(module)
resource_limit = acs_resource_limit.update_resource_limit()
result = acs_resource_limit.get_result(resource_limit)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,212 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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: cs_role
short_description: Manages user roles on Apache CloudStack based clouds.
description:
- Create, update, delete user roles.
author: René Moser (@resmo)
options:
name:
description:
- Name of the role.
type: str
required: true
uuid:
description:
- ID of the role.
- If provided, I(uuid) is used as key.
type: str
aliases: [ id ]
role_type:
description:
- Type of the role.
- Only considered for creation.
type: str
default: User
choices: [ User, DomainAdmin, ResourceAdmin, Admin ]
description:
description:
- Description of the role.
type: str
state:
description:
- State of the role.
type: str
default: present
choices: [ present, absent ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure an user role is present
cs_role:
name: myrole_user
delegate_to: localhost
- name: Ensure a role having particular ID is named as myrole_user
cs_role:
name: myrole_user
id: 04589590-ac63-4ffc-93f5-b698b8ac38b6
delegate_to: localhost
- name: Ensure a role is absent
cs_role:
name: myrole_user
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the role.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the role.
returned: success
type: str
sample: myrole
description:
description: Description of the role.
returned: success
type: str
sample: "This is my role description"
role_type:
description: Type of the role.
returned: success
type: str
sample: User
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackRole(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackRole, self).__init__(module)
self.returns = {
'type': 'role_type',
}
def get_role(self):
uuid = self.module.params.get('uuid')
if uuid:
args = {
'id': uuid,
}
roles = self.query_api('listRoles', **args)
if roles:
return roles['role'][0]
else:
args = {
'name': self.module.params.get('name'),
}
roles = self.query_api('listRoles', **args)
if roles:
return roles['role'][0]
return None
def present_role(self):
role = self.get_role()
if role:
role = self._update_role(role)
else:
role = self._create_role(role)
return role
def _create_role(self, role):
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'type': self.module.params.get('role_type'),
'description': self.module.params.get('description'),
}
if not self.module.check_mode:
res = self.query_api('createRole', **args)
role = res['role']
return role
def _update_role(self, role):
args = {
'id': role['id'],
'name': self.module.params.get('name'),
'description': self.module.params.get('description'),
}
if self.has_changed(args, role):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateRole', **args)
# The API as in 4.9 does not return an updated role yet
if 'role' not in res:
role = self.get_role()
else:
role = res['role']
return role
def absent_role(self):
role = self.get_role()
if role:
self.result['changed'] = True
args = {
'id': role['id'],
}
if not self.module.check_mode:
self.query_api('deleteRole', **args)
return role
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
uuid=dict(aliases=['id']),
name=dict(required=True),
description=dict(),
role_type=dict(choices=['User', 'DomainAdmin', 'ResourceAdmin', 'Admin'], default='User'),
state=dict(choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_role = AnsibleCloudStackRole(module)
state = module.params.get('state')
if state == 'absent':
role = acs_role.absent_role()
else:
role = acs_role.present_role()
result = acs_role.get_result(role)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,352 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, David Passante (@dpassante)
# 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: cs_role_permission
short_description: Manages role permissions on Apache CloudStack based clouds.
description:
- Create, update and remove CloudStack role permissions.
- Managing role permissions only supported in CloudStack >= 4.9.
author: David Passante (@dpassante)
options:
name:
description:
- The API name of the permission.
type: str
required: true
role:
description:
- Name or ID of the role.
type: str
required: true
permission:
description:
- The rule permission, allow or deny. Defaulted to deny.
type: str
choices: [ allow, deny ]
default: deny
state:
description:
- State of the role permission.
type: str
choices: [ present, absent ]
default: present
description:
description:
- The description of the role permission.
type: str
parent:
description:
- The parent role permission uuid. use 0 to move this rule at the top of the list.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a role permission
cs_role_permission:
role: My_Custom_role
name: createVPC
permission: allow
description: My comments
delegate_to: localhost
- name: Remove a role permission
cs_role_permission:
state: absent
role: My_Custom_role
name: createVPC
delegate_to: localhost
- name: Update a system role permission
cs_role_permission:
role: Domain Admin
name: createVPC
permission: deny
delegate_to: localhost
- name: Update rules order. Move the rule at the top of list
cs_role_permission:
role: Domain Admin
name: createVPC
parent: 0
delegate_to: localhost
'''
RETURN = '''
---
id:
description: The ID of the role permission.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: The API name of the permission.
returned: success
type: str
sample: createVPC
permission:
description: The permission type of the api name.
returned: success
type: str
sample: allow
role_id:
description: The ID of the role to which the role permission belongs.
returned: success
type: str
sample: c6f7a5fc-43f8-11e5-a151-feff819cdc7f
description:
description: The description of the role permission
returned: success
type: str
sample: Deny createVPC for users
'''
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackRolePermission(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackRolePermission, self).__init__(module)
cloudstack_min_version = LooseVersion('4.9.2')
self.returns = {
'id': 'id',
'roleid': 'role_id',
'rule': 'name',
'permission': 'permission',
'description': 'description',
}
self.role_permission = None
self.cloudstack_version = self._cloudstack_ver()
if self.cloudstack_version < cloudstack_min_version:
self.fail_json(msg="This module requires CloudStack >= %s." % cloudstack_min_version)
def _cloudstack_ver(self):
capabilities = self.get_capabilities()
return LooseVersion(capabilities['cloudstackversion'])
def _get_role_id(self):
role = self.module.params.get('role')
if not role:
return None
res = self.query_api('listRoles')
roles = res['role']
if roles:
for r in roles:
if role in [r['name'], r['id']]:
return r['id']
self.fail_json(msg="Role '%s' not found" % role)
def _get_role_perm(self):
role_permission = self.role_permission
args = {
'roleid': self._get_role_id(),
}
rp = self.query_api('listRolePermissions', **args)
if rp:
role_permission = rp['rolepermission']
return role_permission
def _get_rule(self, rule=None):
if not rule:
rule = self.module.params.get('name')
if self._get_role_perm():
for _rule in self._get_role_perm():
if rule == _rule['rule'] or rule == _rule['id']:
return _rule
return None
def _get_rule_order(self):
perms = self._get_role_perm()
rules = []
if perms:
for i, rule in enumerate(perms):
rules.append(rule['id'])
return rules
def replace_rule(self):
old_rule = self._get_rule()
if old_rule:
rules_order = self._get_rule_order()
old_pos = rules_order.index(old_rule['id'])
self.remove_role_perm()
new_rule = self.create_role_perm()
if new_rule:
perm_order = self.order_permissions(int(old_pos - 1), new_rule['id'])
return perm_order
return None
def order_permissions(self, parent, rule_id):
rules = self._get_rule_order()
if isinstance(parent, int):
parent_pos = parent
elif parent == '0':
parent_pos = -1
else:
parent_rule = self._get_rule(parent)
if not parent_rule:
self.fail_json(msg="Parent rule '%s' not found" % parent)
parent_pos = rules.index(parent_rule['id'])
r_id = rules.pop(rules.index(rule_id))
rules.insert((parent_pos + 1), r_id)
rules = ','.join(map(str, rules))
return rules
def create_or_update_role_perm(self):
role_permission = self._get_rule()
if not role_permission:
role_permission = self.create_role_perm()
else:
role_permission = self.update_role_perm(role_permission)
return role_permission
def create_role_perm(self):
role_permission = None
self.result['changed'] = True
args = {
'rule': self.module.params.get('name'),
'description': self.module.params.get('description'),
'roleid': self._get_role_id(),
'permission': self.module.params.get('permission'),
}
if not self.module.check_mode:
res = self.query_api('createRolePermission', **args)
role_permission = res['rolepermission']
return role_permission
def update_role_perm(self, role_perm):
perm_order = None
if not self.module.params.get('parent'):
args = {
'ruleid': role_perm['id'],
'roleid': role_perm['roleid'],
'permission': self.module.params.get('permission'),
}
if self.has_changed(args, role_perm, only_keys=['permission']):
self.result['changed'] = True
if not self.module.check_mode:
if self.cloudstack_version >= LooseVersion('4.11.0'):
self.query_api('updateRolePermission', **args)
role_perm = self._get_rule()
else:
perm_order = self.replace_rule()
else:
perm_order = self.order_permissions(self.module.params.get('parent'), role_perm['id'])
if perm_order:
args = {
'roleid': role_perm['roleid'],
'ruleorder': perm_order,
}
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('updateRolePermission', **args)
role_perm = self._get_rule()
return role_perm
def remove_role_perm(self):
role_permission = self._get_rule()
if role_permission:
self.result['changed'] = True
args = {
'id': role_permission['id'],
}
if not self.module.check_mode:
self.query_api('deleteRolePermission', **args)
return role_permission
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
role=dict(required=True),
name=dict(required=True),
permission=dict(choices=['allow', 'deny'], default='deny'),
description=dict(),
state=dict(choices=['present', 'absent'], default='present'),
parent=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
mutually_exclusive=(
['permission', 'parent'],
),
supports_check_mode=True
)
acs_role_perm = AnsibleCloudStackRolePermission(module)
state = module.params.get('state')
if state in ['absent']:
role_permission = acs_role_perm.remove_role_perm()
else:
role_permission = acs_role_perm.create_or_update_role_perm()
result = acs_role_perm.get_result(role_permission)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,377 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_router
short_description: Manages routers on Apache CloudStack based clouds.
description:
- Start, restart, stop and destroy routers.
- I(state=present) is not able to create routers, use M(cs_network) instead.
author: René Moser (@resmo)
options:
name:
description:
- Name of the router.
type: str
required: true
service_offering:
description:
- Name or id of the service offering of the router.
type: str
domain:
description:
- Domain the router is related to.
type: str
account:
description:
- Account the router is related to.
type: str
project:
description:
- Name of the project the router is related to.
type: str
zone:
description:
- Name of the zone the router is deployed in.
- If not set, all zones are used.
type: str
state:
description:
- State of the router.
type: str
default: present
choices: [ present, absent, started, stopped, restarted ]
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
# Ensure the router has the desired service offering, no matter if
# the router is running or not.
- name: Present router
cs_router:
name: r-40-VM
service_offering: System Offering for Software Router
delegate_to: localhost
- name: Ensure started
cs_router:
name: r-40-VM
state: started
delegate_to: localhost
# Ensure started with desired service offering.
# If the service offerings changes, router will be rebooted.
- name: Ensure started with desired service offering
cs_router:
name: r-40-VM
service_offering: System Offering for Software Router
state: started
delegate_to: localhost
- name: Ensure stopped
cs_router:
name: r-40-VM
state: stopped
delegate_to: localhost
- name: Remove a router
cs_router:
name: r-40-VM
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the router.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the router.
returned: success
type: str
sample: r-40-VM
created:
description: Date of the router was created.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
template_version:
description: Version of the system VM template.
returned: success
type: str
sample: 4.5.1
requires_upgrade:
description: Whether the router needs to be upgraded to the new template.
returned: success
type: bool
sample: false
redundant_state:
description: Redundant state of the router.
returned: success
type: str
sample: UNKNOWN
role:
description: Role of the router.
returned: success
type: str
sample: VIRTUAL_ROUTER
zone:
description: Name of zone the router is in.
returned: success
type: str
sample: ch-gva-2
service_offering:
description: Name of the service offering the router has.
returned: success
type: str
sample: System Offering For Software Router
state:
description: State of the router.
returned: success
type: str
sample: Active
domain:
description: Domain the router is related to.
returned: success
type: str
sample: ROOT
account:
description: Account the router is related to.
returned: success
type: str
sample: admin
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackRouter(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackRouter, self).__init__(module)
self.returns = {
'serviceofferingname': 'service_offering',
'version': 'template_version',
'requiresupgrade': 'requires_upgrade',
'redundantstate': 'redundant_state',
'role': 'role'
}
self.router = None
def get_service_offering_id(self):
service_offering = self.module.params.get('service_offering')
if not service_offering:
return None
args = {
'issystem': True
}
service_offerings = self.query_api('listServiceOfferings', **args)
if service_offerings:
for s in service_offerings['serviceoffering']:
if service_offering in [s['name'], s['id']]:
return s['id']
self.module.fail_json(msg="Service offering '%s' not found" % service_offering)
def get_router(self):
if not self.router:
router = self.module.params.get('name')
args = {
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'listall': True,
'fetch_list': True,
}
if self.module.params.get('zone'):
args['zoneid'] = self.get_zone(key='id')
routers = self.query_api('listRouters', **args)
if routers:
for r in routers:
if router.lower() in [r['name'].lower(), r['id']]:
self.router = r
break
return self.router
def start_router(self):
router = self.get_router()
if not router:
self.module.fail_json(msg="Router not found")
if router['state'].lower() != "running":
self.result['changed'] = True
args = {
'id': router['id'],
}
if not self.module.check_mode:
res = self.query_api('startRouter', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
router = self.poll_job(res, 'router')
return router
def stop_router(self):
router = self.get_router()
if not router:
self.module.fail_json(msg="Router not found")
if router['state'].lower() != "stopped":
self.result['changed'] = True
args = {
'id': router['id'],
}
if not self.module.check_mode:
res = self.query_api('stopRouter', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
router = self.poll_job(res, 'router')
return router
def reboot_router(self):
router = self.get_router()
if not router:
self.module.fail_json(msg="Router not found")
self.result['changed'] = True
args = {
'id': router['id'],
}
if not self.module.check_mode:
res = self.query_api('rebootRouter', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
router = self.poll_job(res, 'router')
return router
def absent_router(self):
router = self.get_router()
if router:
self.result['changed'] = True
args = {
'id': router['id'],
}
if not self.module.check_mode:
res = self.query_api('destroyRouter', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'router')
return router
def present_router(self):
router = self.get_router()
if not router:
self.module.fail_json(msg="Router can not be created using the API, see cs_network.")
args = {
'id': router['id'],
'serviceofferingid': self.get_service_offering_id(),
}
state = self.module.params.get('state')
if self.has_changed(args, router):
self.result['changed'] = True
if not self.module.check_mode:
current_state = router['state'].lower()
self.stop_router()
router = self.query_api('changeServiceForRouter', **args)
if state in ['restarted', 'started']:
router = self.start_router()
# if state=present we get to the state before the service
# offering change.
elif state == "present" and current_state == "running":
router = self.start_router()
elif state == "started":
router = self.start_router()
elif state == "stopped":
router = self.stop_router()
elif state == "restarted":
router = self.reboot_router()
return router
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
service_offering=dict(),
state=dict(choices=['present', 'started', 'stopped', 'restarted', 'absent'], default="present"),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_router = AnsibleCloudStackRouter(module)
state = module.params.get('state')
if state in ['absent']:
router = acs_router.absent_router()
else:
router = acs_router.present_router()
result = acs_router.get_result(router)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,200 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_securitygroup
short_description: Manages security groups on Apache CloudStack based clouds.
description:
- Create and remove security groups.
author: René Moser (@resmo)
options:
name:
description:
- Name of the security group.
type: str
required: true
description:
description:
- Description of the security group.
type: str
state:
description:
- State of the security group.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the security group is related to.
type: str
account:
description:
- Account the security group is related to.
type: str
project:
description:
- Name of the project the security group to be created in.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create a security group
cs_securitygroup:
name: default
description: default security group
delegate_to: localhost
- name: remove a security group
cs_securitygroup:
name: default
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the security group.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: Name of security group.
returned: success
type: str
sample: app
description:
description: Description of security group.
returned: success
type: str
sample: application security group
tags:
description: List of resource tags associated with the security group.
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
project:
description: Name of project the security group is related to.
returned: success
type: str
sample: Production
domain:
description: Domain the security group is related to.
returned: success
type: str
sample: example domain
account:
description: Account the security group is related to.
returned: success
type: str
sample: example account
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together
class AnsibleCloudStackSecurityGroup(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackSecurityGroup, self).__init__(module)
self.security_group = None
def get_security_group(self):
if not self.security_group:
args = {
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'securitygroupname': self.module.params.get('name'),
}
sgs = self.query_api('listSecurityGroups', **args)
if sgs:
self.security_group = sgs['securitygroup'][0]
return self.security_group
def create_security_group(self):
security_group = self.get_security_group()
if not security_group:
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'description': self.module.params.get('description'),
}
if not self.module.check_mode:
res = self.query_api('createSecurityGroup', **args)
security_group = res['securitygroup']
return security_group
def remove_security_group(self):
security_group = self.get_security_group()
if security_group:
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
}
if not self.module.check_mode:
self.query_api('deleteSecurityGroup', **args)
return security_group
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
description=dict(),
state=dict(choices=['present', 'absent'], default='present'),
project=dict(),
account=dict(),
domain=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_sg = AnsibleCloudStackSecurityGroup(module)
state = module.params.get('state')
if state in ['absent']:
sg = acs_sg.remove_security_group()
else:
sg = acs_sg.create_security_group()
result = acs_sg.get_result(sg)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,389 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_securitygroup_rule
short_description: Manages security group rules on Apache CloudStack based clouds.
description:
- Add and remove security group rules.
author: René Moser (@resmo)
options:
security_group:
description:
- Name of the security group the rule is related to. The security group must be existing.
type: str
required: true
state:
description:
- State of the security group rule.
type: str
default: present
choices: [ present, absent ]
protocol:
description:
- Protocol of the security group rule.
type: str
default: tcp
choices: [ tcp, udp, icmp, ah, esp, gre ]
type:
description:
- Ingress or egress security group rule.
type: str
default: ingress
choices: [ ingress, egress ]
cidr:
description:
- CIDR (full notation) to be used for security group rule.
type: str
default: 0.0.0.0/0
user_security_group:
description:
- Security group this rule is based of.
type: str
start_port:
description:
- Start port for this rule. Required if I(protocol=tcp) or I(protocol=udp).
type: int
aliases: [ port ]
end_port:
description:
- End port for this rule. Required if I(protocol=tcp) or I(protocol=udp), but I(start_port) will be used if not set.
type: int
icmp_type:
description:
- Type of the icmp message being sent. Required if I(protocol=icmp).
type: int
icmp_code:
description:
- Error code for this icmp message. Required if I(protocol=icmp).
type: int
project:
description:
- Name of the project the security group to be created in.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
---
- name: allow inbound port 80/tcp from 1.2.3.4 added to security group 'default'
cs_securitygroup_rule:
security_group: default
port: 80
cidr: 1.2.3.4/32
delegate_to: localhost
- name: allow tcp/udp outbound added to security group 'default'
cs_securitygroup_rule:
security_group: default
type: egress
start_port: 1
end_port: 65535
protocol: '{{ item }}'
with_items:
- tcp
- udp
delegate_to: localhost
- name: allow inbound icmp from 0.0.0.0/0 added to security group 'default'
cs_securitygroup_rule:
security_group: default
protocol: icmp
icmp_code: -1
icmp_type: -1
delegate_to: localhost
- name: remove rule inbound port 80/tcp from 0.0.0.0/0 from security group 'default'
cs_securitygroup_rule:
security_group: default
port: 80
state: absent
delegate_to: localhost
- name: allow inbound port 80/tcp from security group web added to security group 'default'
cs_securitygroup_rule:
security_group: default
port: 80
user_security_group: web
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the of the rule.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
security_group:
description: security group of the rule.
returned: success
type: str
sample: default
type:
description: type of the rule.
returned: success
type: str
sample: ingress
cidr:
description: CIDR of the rule.
returned: success and cidr is defined
type: str
sample: 0.0.0.0/0
user_security_group:
description: user security group of the rule.
returned: success and user_security_group is defined
type: str
sample: default
protocol:
description: protocol of the rule.
returned: success
type: str
sample: tcp
start_port:
description: start port of the rule.
returned: success
type: int
sample: 80
end_port:
description: end port of the rule.
returned: success
type: int
sample: 80
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together
class AnsibleCloudStackSecurityGroupRule(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackSecurityGroupRule, self).__init__(module)
self.returns = {
'icmptype': 'icmp_type',
'icmpcode': 'icmp_code',
'endport': 'end_port',
'startport': 'start_port',
'protocol': 'protocol',
'cidr': 'cidr',
'securitygroupname': 'user_security_group',
}
def _tcp_udp_match(self, rule, protocol, start_port, end_port):
return (protocol in ['tcp', 'udp'] and
protocol == rule['protocol'] and
start_port == int(rule['startport']) and
end_port == int(rule['endport']))
def _icmp_match(self, rule, protocol, icmp_code, icmp_type):
return (protocol == 'icmp' and
protocol == rule['protocol'] and
icmp_code == int(rule['icmpcode']) and
icmp_type == int(rule['icmptype']))
def _ah_esp_gre_match(self, rule, protocol):
return (protocol in ['ah', 'esp', 'gre'] and
protocol == rule['protocol'])
def _type_security_group_match(self, rule, security_group_name):
return (security_group_name and
'securitygroupname' in rule and
security_group_name == rule['securitygroupname'])
def _type_cidr_match(self, rule, cidr):
return ('cidr' in rule and
cidr == rule['cidr'])
def _get_rule(self, rules):
user_security_group_name = self.module.params.get('user_security_group')
cidr = self.module.params.get('cidr')
protocol = self.module.params.get('protocol')
start_port = self.module.params.get('start_port')
end_port = self.get_or_fallback('end_port', 'start_port')
icmp_code = self.module.params.get('icmp_code')
icmp_type = self.module.params.get('icmp_type')
if protocol in ['tcp', 'udp'] and (start_port is None or end_port is None):
self.module.fail_json(msg="no start_port or end_port set for protocol '%s'" % protocol)
if protocol == 'icmp' and (icmp_type is None or icmp_code is None):
self.module.fail_json(msg="no icmp_type or icmp_code set for protocol '%s'" % protocol)
for rule in rules:
if user_security_group_name:
type_match = self._type_security_group_match(rule, user_security_group_name)
else:
type_match = self._type_cidr_match(rule, cidr)
protocol_match = (self._tcp_udp_match(rule, protocol, start_port, end_port) or
self._icmp_match(rule, protocol, icmp_code, icmp_type) or
self._ah_esp_gre_match(rule, protocol))
if type_match and protocol_match:
return rule
return None
def get_security_group(self, security_group_name=None):
if not security_group_name:
security_group_name = self.module.params.get('security_group')
args = {
'securitygroupname': security_group_name,
'projectid': self.get_project('id'),
}
sgs = self.query_api('listSecurityGroups', **args)
if not sgs or 'securitygroup' not in sgs:
self.module.fail_json(msg="security group '%s' not found" % security_group_name)
return sgs['securitygroup'][0]
def add_rule(self):
security_group = self.get_security_group()
args = {}
user_security_group_name = self.module.params.get('user_security_group')
# the user_security_group and cidr are mutually_exclusive, but cidr is defaulted to 0.0.0.0/0.
# that is why we ignore if we have a user_security_group.
if user_security_group_name:
args['usersecuritygrouplist'] = []
user_security_group = self.get_security_group(user_security_group_name)
args['usersecuritygrouplist'].append({
'group': user_security_group['name'],
'account': user_security_group['account'],
})
else:
args['cidrlist'] = self.module.params.get('cidr')
args['protocol'] = self.module.params.get('protocol')
args['startport'] = self.module.params.get('start_port')
args['endport'] = self.get_or_fallback('end_port', 'start_port')
args['icmptype'] = self.module.params.get('icmp_type')
args['icmpcode'] = self.module.params.get('icmp_code')
args['projectid'] = self.get_project('id')
args['securitygroupid'] = security_group['id']
rule = None
res = None
sg_type = self.module.params.get('type')
if sg_type == 'ingress':
if 'ingressrule' in security_group:
rule = self._get_rule(security_group['ingressrule'])
if not rule:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('authorizeSecurityGroupIngress', **args)
elif sg_type == 'egress':
if 'egressrule' in security_group:
rule = self._get_rule(security_group['egressrule'])
if not rule:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('authorizeSecurityGroupEgress', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
security_group = self.poll_job(res, 'securitygroup')
key = sg_type + "rule" # ingressrule / egressrule
if key in security_group:
rule = security_group[key][0]
return rule
def remove_rule(self):
security_group = self.get_security_group()
rule = None
res = None
sg_type = self.module.params.get('type')
if sg_type == 'ingress':
rule = self._get_rule(security_group['ingressrule'])
if rule:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('revokeSecurityGroupIngress', id=rule['ruleid'])
elif sg_type == 'egress':
rule = self._get_rule(security_group['egressrule'])
if rule:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('revokeSecurityGroupEgress', id=rule['ruleid'])
poll_async = self.module.params.get('poll_async')
if res and poll_async:
res = self.poll_job(res, 'securitygroup')
return rule
def get_result(self, security_group_rule):
super(AnsibleCloudStackSecurityGroupRule, self).get_result(security_group_rule)
self.result['type'] = self.module.params.get('type')
self.result['security_group'] = self.module.params.get('security_group')
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
security_group=dict(required=True),
type=dict(choices=['ingress', 'egress'], default='ingress'),
cidr=dict(default='0.0.0.0/0'),
user_security_group=dict(),
protocol=dict(choices=['tcp', 'udp', 'icmp', 'ah', 'esp', 'gre'], default='tcp'),
icmp_type=dict(type='int'),
icmp_code=dict(type='int'),
start_port=dict(type='int', aliases=['port']),
end_port=dict(type='int'),
state=dict(choices=['present', 'absent'], default='present'),
project=dict(),
poll_async=dict(type='bool', default=True),
))
required_together = cs_required_together()
required_together.extend([
['icmp_type', 'icmp_code'],
])
module = AnsibleModule(
argument_spec=argument_spec,
required_together=required_together,
mutually_exclusive=(
['icmp_type', 'start_port'],
['icmp_type', 'end_port'],
['icmp_code', 'start_port'],
['icmp_code', 'end_port'],
),
supports_check_mode=True
)
acs_sg_rule = AnsibleCloudStackSecurityGroupRule(module)
state = module.params.get('state')
if state in ['absent']:
sg_rule = acs_sg_rule.remove_rule()
else:
sg_rule = acs_sg_rule.add_rule()
result = acs_sg_rule.get_result(sg_rule)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,583 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_service_offering
description:
- Create and delete service offerings for guest and system VMs.
- Update display_text of existing service offering.
short_description: Manages service offerings on Apache CloudStack based clouds.
author: René Moser (@resmo)
options:
disk_bytes_read_rate:
description:
- Bytes read rate of the disk offering.
type: int
aliases: [ bytes_read_rate ]
disk_bytes_write_rate:
description:
- Bytes write rate of the disk offering.
type: int
aliases: [ bytes_write_rate ]
cpu_number:
description:
- The number of CPUs of the service offering.
type: int
cpu_speed:
description:
- The CPU speed of the service offering in MHz.
type: int
limit_cpu_usage:
description:
- Restrict the CPU usage to committed service offering.
type: bool
deployment_planner:
description:
- The deployment planner heuristics used to deploy a VM of this offering.
- If not set, the value of global config I(vm.deployment.planner) is used.
type: str
display_text:
description:
- Display text of the service offering.
- If not set, I(name) will be used as I(display_text) while creating.
type: str
domain:
description:
- Domain the service offering is related to.
- Public for all domains and subdomains if not set.
type: str
host_tags:
description:
- The host tags for this service offering.
type: list
aliases:
- host_tag
hypervisor_snapshot_reserve:
description:
- Hypervisor snapshot reserve space as a percent of a volume.
- Only for managed storage using Xen or VMware.
type: int
is_iops_customized:
description:
- Whether compute offering iops is custom or not.
type: bool
aliases: [ disk_iops_customized ]
disk_iops_read_rate:
description:
- IO requests read rate of the disk offering.
type: int
disk_iops_write_rate:
description:
- IO requests write rate of the disk offering.
type: int
disk_iops_max:
description:
- Max. iops of the compute offering.
type: int
disk_iops_min:
description:
- Min. iops of the compute offering.
type: int
is_system:
description:
- Whether it is a system VM offering or not.
type: bool
default: no
is_volatile:
description:
- Whether the virtual machine needs to be volatile or not.
- Every reboot of VM the root disk is detached then destroyed and a fresh root disk is created and attached to VM.
type: bool
memory:
description:
- The total memory of the service offering in MB.
type: int
name:
description:
- Name of the service offering.
type: str
required: true
network_rate:
description:
- Data transfer rate in Mb/s allowed.
- Supported only for non-system offering and system offerings having I(system_vm_type=domainrouter).
type: int
offer_ha:
description:
- Whether HA is set for the service offering.
type: bool
default: no
provisioning_type:
description:
- Provisioning type used to create volumes.
type: str
choices:
- thin
- sparse
- fat
service_offering_details:
description:
- Details for planner, used to store specific parameters.
- A list of dictionaries having keys C(key) and C(value).
type: list
state:
description:
- State of the service offering.
type: str
choices:
- present
- absent
default: present
storage_type:
description:
- The storage type of the service offering.
type: str
choices:
- local
- shared
system_vm_type:
description:
- The system VM type.
- Required if I(is_system=yes).
type: str
choices:
- domainrouter
- consoleproxy
- secondarystoragevm
storage_tags:
description:
- The storage tags for this service offering.
type: list
aliases:
- storage_tag
is_customized:
description:
- Whether the offering is customizable or not.
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a non-volatile compute service offering with local storage
cs_service_offering:
name: Micro
display_text: Micro 512mb 1cpu
cpu_number: 1
cpu_speed: 2198
memory: 512
host_tags: eco
storage_type: local
delegate_to: localhost
- name: Create a volatile compute service offering with shared storage
cs_service_offering:
name: Tiny
display_text: Tiny 1gb 1cpu
cpu_number: 1
cpu_speed: 2198
memory: 1024
storage_type: shared
is_volatile: yes
host_tags: eco
storage_tags: eco
delegate_to: localhost
- name: Create or update a volatile compute service offering with shared storage
cs_service_offering:
name: Tiny
display_text: Tiny 1gb 1cpu
cpu_number: 1
cpu_speed: 2198
memory: 1024
storage_type: shared
is_volatile: yes
host_tags: eco
storage_tags: eco
delegate_to: localhost
- name: Create or update a custom compute service offering
cs_service_offering:
name: custom
display_text: custom compute offer
is_customized: yes
storage_type: shared
host_tags: eco
storage_tags: eco
delegate_to: localhost
- name: Remove a compute service offering
cs_service_offering:
name: Tiny
state: absent
delegate_to: localhost
- name: Create or update a system offering for the console proxy
cs_service_offering:
name: System Offering for Console Proxy 2GB
display_text: System Offering for Console Proxy 2GB RAM
is_system: yes
system_vm_type: consoleproxy
cpu_number: 1
cpu_speed: 2198
memory: 2048
storage_type: shared
storage_tags: perf
delegate_to: localhost
- name: Remove a system offering
cs_service_offering:
name: System Offering for Console Proxy 2GB
is_system: yes
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the service offering
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
cpu_number:
description: Number of CPUs in the service offering
returned: success
type: int
sample: 4
cpu_speed:
description: Speed of CPUs in MHz in the service offering
returned: success
type: int
sample: 2198
disk_iops_max:
description: Max iops of the disk offering
returned: success
type: int
sample: 1000
disk_iops_min:
description: Min iops of the disk offering
returned: success
type: int
sample: 500
disk_bytes_read_rate:
description: Bytes read rate of the service offering
returned: success
type: int
sample: 1000
disk_bytes_write_rate:
description: Bytes write rate of the service offering
returned: success
type: int
sample: 1000
disk_iops_read_rate:
description: IO requests per second read rate of the service offering
returned: success
type: int
sample: 1000
disk_iops_write_rate:
description: IO requests per second write rate of the service offering
returned: success
type: int
sample: 1000
created:
description: Date the offering was created
returned: success
type: str
sample: 2017-11-19T10:48:59+0000
display_text:
description: Display text of the offering
returned: success
type: str
sample: Micro 512mb 1cpu
domain:
description: Domain the offering is into
returned: success
type: str
sample: ROOT
host_tags:
description: List of host tags
returned: success
type: list
sample: [ 'eco' ]
storage_tags:
description: List of storage tags
returned: success
type: list
sample: [ 'eco' ]
is_system:
description: Whether the offering is for system VMs or not
returned: success
type: bool
sample: false
is_iops_customized:
description: Whether the offering uses custom IOPS or not
returned: success
type: bool
sample: false
is_volatile:
description: Whether the offering is volatile or not
returned: success
type: bool
sample: false
limit_cpu_usage:
description: Whether the CPU usage is restricted to committed service offering
returned: success
type: bool
sample: false
memory:
description: Memory of the system offering
returned: success
type: int
sample: 512
name:
description: Name of the system offering
returned: success
type: str
sample: Micro
offer_ha:
description: Whether HA support is enabled in the offering or not
returned: success
type: bool
sample: false
provisioning_type:
description: Provisioning type used to create volumes
returned: success
type: str
sample: thin
storage_type:
description: Storage type used to create volumes
returned: success
type: str
sample: shared
system_vm_type:
description: System VM type of this offering
returned: success
type: str
sample: consoleproxy
service_offering_details:
description: Additioanl service offering details
returned: success
type: dict
sample: "{'vgpuType': 'GRID K180Q','pciDevice':'Group of NVIDIA Corporation GK107GL [GRID K1] GPUs'}"
network_rate:
description: Data transfer rate in megabits per second allowed
returned: success
type: int
sample: 1000
is_customized:
description: Whether the offering is customizable or not
returned: success
type: bool
sample: false
version_added: '2.8'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackServiceOffering(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackServiceOffering, self).__init__(module)
self.returns = {
'cpunumber': 'cpu_number',
'cpuspeed': 'cpu_speed',
'deploymentplanner': 'deployment_planner',
'diskBytesReadRate': 'disk_bytes_read_rate',
'diskBytesWriteRate': 'disk_bytes_write_rate',
'diskIopsReadRate': 'disk_iops_read_rate',
'diskIopsWriteRate': 'disk_iops_write_rate',
'maxiops': 'disk_iops_max',
'miniops': 'disk_iops_min',
'hypervisorsnapshotreserve': 'hypervisor_snapshot_reserve',
'iscustomized': 'is_customized',
'iscustomizediops': 'is_iops_customized',
'issystem': 'is_system',
'isvolatile': 'is_volatile',
'limitcpuuse': 'limit_cpu_usage',
'memory': 'memory',
'networkrate': 'network_rate',
'offerha': 'offer_ha',
'provisioningtype': 'provisioning_type',
'serviceofferingdetails': 'service_offering_details',
'storagetype': 'storage_type',
'systemvmtype': 'system_vm_type',
'tags': 'storage_tags',
}
def get_service_offering(self):
args = {
'name': self.module.params.get('name'),
'domainid': self.get_domain(key='id'),
'issystem': self.module.params.get('is_system'),
'systemvmtype': self.module.params.get('system_vm_type'),
}
service_offerings = self.query_api('listServiceOfferings', **args)
if service_offerings:
return service_offerings['serviceoffering'][0]
def present_service_offering(self):
service_offering = self.get_service_offering()
if not service_offering:
service_offering = self._create_offering(service_offering)
else:
service_offering = self._update_offering(service_offering)
return service_offering
def absent_service_offering(self):
service_offering = self.get_service_offering()
if service_offering:
self.result['changed'] = True
if not self.module.check_mode:
args = {
'id': service_offering['id'],
}
self.query_api('deleteServiceOffering', **args)
return service_offering
def _create_offering(self, service_offering):
self.result['changed'] = True
system_vm_type = self.module.params.get('system_vm_type')
is_system = self.module.params.get('is_system')
required_params = []
if is_system and not system_vm_type:
required_params.append('system_vm_type')
self.module.fail_on_missing_params(required_params=required_params)
args = {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'bytesreadrate': self.module.params.get('disk_bytes_read_rate'),
'byteswriterate': self.module.params.get('disk_bytes_write_rate'),
'cpunumber': self.module.params.get('cpu_number'),
'cpuspeed': self.module.params.get('cpu_speed'),
'customizediops': self.module.params.get('is_iops_customized'),
'deploymentplanner': self.module.params.get('deployment_planner'),
'domainid': self.get_domain(key='id'),
'hosttags': self.module.params.get('host_tags'),
'hypervisorsnapshotreserve': self.module.params.get('hypervisor_snapshot_reserve'),
'iopsreadrate': self.module.params.get('disk_iops_read_rate'),
'iopswriterate': self.module.params.get('disk_iops_write_rate'),
'maxiops': self.module.params.get('disk_iops_max'),
'miniops': self.module.params.get('disk_iops_min'),
'issystem': is_system,
'isvolatile': self.module.params.get('is_volatile'),
'memory': self.module.params.get('memory'),
'networkrate': self.module.params.get('network_rate'),
'offerha': self.module.params.get('offer_ha'),
'provisioningtype': self.module.params.get('provisioning_type'),
'serviceofferingdetails': self.module.params.get('service_offering_details'),
'storagetype': self.module.params.get('storage_type'),
'systemvmtype': system_vm_type,
'tags': self.module.params.get('storage_tags'),
'limitcpuuse': self.module.params.get('limit_cpu_usage'),
'customized': self.module.params.get('is_customized')
}
if not self.module.check_mode:
res = self.query_api('createServiceOffering', **args)
service_offering = res['serviceoffering']
return service_offering
def _update_offering(self, service_offering):
args = {
'id': service_offering['id'],
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
}
if self.has_changed(args, service_offering):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateServiceOffering', **args)
service_offering = res['serviceoffering']
return service_offering
def get_result(self, service_offering):
super(AnsibleCloudStackServiceOffering, self).get_result(service_offering)
if service_offering:
if 'hosttags' in service_offering:
self.result['host_tags'] = service_offering['hosttags'].split(',') or [service_offering['hosttags']]
# Prevent confusion, the api returns a tags key for storage tags.
if 'tags' in service_offering:
self.result['storage_tags'] = service_offering['tags'].split(',') or [service_offering['tags']]
if 'tags' in self.result:
del self.result['tags']
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
cpu_number=dict(type='int'),
cpu_speed=dict(type='int'),
limit_cpu_usage=dict(type='bool'),
deployment_planner=dict(),
domain=dict(),
host_tags=dict(type='list', aliases=['host_tag']),
hypervisor_snapshot_reserve=dict(type='int'),
disk_bytes_read_rate=dict(type='int', aliases=['bytes_read_rate']),
disk_bytes_write_rate=dict(type='int', aliases=['bytes_write_rate']),
disk_iops_read_rate=dict(type='int'),
disk_iops_write_rate=dict(type='int'),
disk_iops_max=dict(type='int'),
disk_iops_min=dict(type='int'),
is_system=dict(type='bool', default=False),
is_volatile=dict(type='bool'),
is_iops_customized=dict(type='bool', aliases=['disk_iops_customized']),
memory=dict(type='int'),
network_rate=dict(type='int'),
offer_ha=dict(type='bool'),
provisioning_type=dict(choices=['thin', 'sparse', 'fat']),
service_offering_details=dict(type='list'),
storage_type=dict(choices=['local', 'shared']),
system_vm_type=dict(choices=['domainrouter', 'consoleproxy', 'secondarystoragevm']),
storage_tags=dict(type='list', aliases=['storage_tag']),
state=dict(choices=['present', 'absent'], default='present'),
is_customized=dict(type='bool'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_so = AnsibleCloudStackServiceOffering(module)
state = module.params.get('state')
if state == "absent":
service_offering = acs_so.absent_service_offering()
else:
service_offering = acs_so.present_service_offering()
result = acs_so.get_result(service_offering)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,358 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_snapshot_policy
short_description: Manages volume snapshot policies on Apache CloudStack based clouds.
description:
- Create, update and delete volume snapshot policies.
author: René Moser (@resmo)
options:
volume:
description:
- Name of the volume.
- Either I(volume) or I(vm) is required.
type: str
volume_type:
description:
- Type of the volume.
type: str
choices:
- DATADISK
- ROOT
vm:
description:
- Name of the instance to select the volume from.
- Use I(volume_type) if VM has a DATADISK and ROOT volume.
- In case of I(volume_type=DATADISK), additionally use I(device_id) if VM has more than one DATADISK volume.
- Either I(volume) or I(vm) is required.
type: str
device_id:
description:
- ID of the device on a VM the volume is attached to.
- This will only be considered if VM has multiple DATADISK volumes.
type: int
vpc:
description:
- Name of the vpc the instance is deployed in.
type: str
interval_type:
description:
- Interval of the snapshot.
type: str
default: daily
choices: [ hourly, daily, weekly, monthly ]
aliases: [ interval ]
max_snaps:
description:
- Max number of snapshots.
type: int
default: 8
aliases: [ max ]
schedule:
description:
- Time the snapshot is scheduled. Required if I(state=present).
- 'Format for I(interval_type=HOURLY): C(MM)'
- 'Format for I(interval_type=DAILY): C(MM:HH)'
- 'Format for I(interval_type=WEEKLY): C(MM:HH:DD (1-7))'
- 'Format for I(interval_type=MONTHLY): C(MM:HH:DD (1-28))'
type: str
time_zone:
description:
- Specifies a timezone for this command.
type: str
default: UTC
aliases: [ timezone ]
state:
description:
- State of the snapshot policy.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the volume is related to.
type: str
account:
description:
- Account the volume is related to.
type: str
project:
description:
- Name of the project the volume is related to.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: ensure a snapshot policy daily at 1h00 UTC
cs_snapshot_policy:
volume: ROOT-478
schedule: '00:1'
max_snaps: 3
delegate_to: localhost
- name: ensure a snapshot policy daily at 1h00 UTC on the second DATADISK of VM web-01
cs_snapshot_policy:
vm: web-01
volume_type: DATADISK
device_id: 2
schedule: '00:1'
max_snaps: 3
delegate_to: localhost
- name: ensure a snapshot policy hourly at minute 5 UTC
cs_snapshot_policy:
volume: ROOT-478
schedule: '5'
interval_type: hourly
max_snaps: 1
delegate_to: localhost
- name: ensure a snapshot policy weekly on Sunday at 05h00, TZ Europe/Zurich
cs_snapshot_policy:
volume: ROOT-478
schedule: '00:5:1'
interval_type: weekly
max_snaps: 1
time_zone: 'Europe/Zurich'
delegate_to: localhost
- name: ensure a snapshot policy is absent
cs_snapshot_policy:
volume: ROOT-478
interval_type: hourly
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the snapshot policy.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
interval_type:
description: interval type of the snapshot policy.
returned: success
type: str
sample: daily
schedule:
description: schedule of the snapshot policy.
returned: success
type: str
sample:
max_snaps:
description: maximum number of snapshots retained.
returned: success
type: int
sample: 10
time_zone:
description: the time zone of the snapshot policy.
returned: success
type: str
sample: Etc/UTC
volume:
description: the volume of the snapshot policy.
returned: success
type: str
sample: Etc/UTC
zone:
description: Name of zone the volume is related to.
returned: success
type: str
sample: ch-gva-2
project:
description: Name of project the volume is related to.
returned: success
type: str
sample: Production
account:
description: Account the volume is related to.
returned: success
type: str
sample: example account
domain:
description: Domain the volume is related to.
returned: success
type: str
sample: example domain
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackSnapshotPolicy(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackSnapshotPolicy, self).__init__(module)
self.returns = {
'schedule': 'schedule',
'timezone': 'time_zone',
'maxsnaps': 'max_snaps',
}
self.interval_types = {
'hourly': 0,
'daily': 1,
'weekly': 2,
'monthly': 3,
}
self.volume = None
def get_interval_type(self):
interval_type = self.module.params.get('interval_type')
return self.interval_types[interval_type]
def get_volume(self, key=None):
if self.volume:
return self._get_by_key(key, self.volume)
args = {
'name': self.module.params.get('volume'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'virtualmachineid': self.get_vm(key='id', filter_zone=False),
'type': self.module.params.get('volume_type'),
}
volumes = self.query_api('listVolumes', **args)
if volumes:
if volumes['count'] > 1:
device_id = self.module.params.get('device_id')
if not device_id:
self.module.fail_json(msg="Found more then 1 volume: combine params 'vm', 'volume_type', 'device_id' and/or 'volume' to select the volume")
else:
for v in volumes['volume']:
if v.get('deviceid') == device_id:
self.volume = v
return self._get_by_key(key, self.volume)
self.module.fail_json(msg="No volume found with device id %s" % device_id)
self.volume = volumes['volume'][0]
return self._get_by_key(key, self.volume)
return None
def get_snapshot_policy(self):
args = {
'volumeid': self.get_volume(key='id')
}
policies = self.query_api('listSnapshotPolicies', **args)
if policies:
for policy in policies['snapshotpolicy']:
if policy['intervaltype'] == self.get_interval_type():
return policy
return None
def present_snapshot_policy(self):
required_params = [
'schedule',
]
self.module.fail_on_missing_params(required_params=required_params)
policy = self.get_snapshot_policy()
args = {
'id': policy.get('id') if policy else None,
'intervaltype': self.module.params.get('interval_type'),
'schedule': self.module.params.get('schedule'),
'maxsnaps': self.module.params.get('max_snaps'),
'timezone': self.module.params.get('time_zone'),
'volumeid': self.get_volume(key='id')
}
if not policy or (policy and self.has_changed(policy, args, only_keys=['schedule', 'maxsnaps', 'timezone'])):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('createSnapshotPolicy', **args)
policy = res['snapshotpolicy']
return policy
def absent_snapshot_policy(self):
policy = self.get_snapshot_policy()
if policy:
self.result['changed'] = True
args = {
'id': policy['id']
}
if not self.module.check_mode:
self.query_api('deleteSnapshotPolicies', **args)
return policy
def get_result(self, policy):
super(AnsibleCloudStackSnapshotPolicy, self).get_result(policy)
if policy and 'intervaltype' in policy:
for key, value in self.interval_types.items():
if value == policy['intervaltype']:
self.result['interval_type'] = key
break
volume = self.get_volume()
if volume:
volume_results = {
'volume': volume.get('name'),
'zone': volume.get('zonename'),
'project': volume.get('project'),
'account': volume.get('account'),
'domain': volume.get('domain'),
}
self.result.update(volume_results)
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
volume=dict(),
volume_type=dict(choices=['DATADISK', 'ROOT']),
vm=dict(),
device_id=dict(type='int'),
vpc=dict(),
interval_type=dict(default='daily', choices=['hourly', 'daily', 'weekly', 'monthly'], aliases=['interval']),
schedule=dict(),
time_zone=dict(default='UTC', aliases=['timezone']),
max_snaps=dict(type='int', default=8, aliases=['max']),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
required_one_of=(
['vm', 'volume'],
),
supports_check_mode=True
)
acs_snapshot_policy = AnsibleCloudStackSnapshotPolicy(module)
state = module.params.get('state')
if state in ['absent']:
policy = acs_snapshot_policy.absent_snapshot_policy()
else:
policy = acs_snapshot_policy.present_snapshot_policy()
result = acs_snapshot_policy.get_result(policy)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,267 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_sshkeypair
short_description: Manages SSH keys on Apache CloudStack based clouds.
description:
- Create, register and remove SSH keys.
- If no key was found and no public key was provided and a new SSH
private/public key pair will be created and the private key will be returned.
author: René Moser (@resmo)
options:
name:
description:
- Name of public key.
type: str
required: true
domain:
description:
- Domain the public key is related to.
type: str
account:
description:
- Account the public key is related to.
type: str
project:
description:
- Name of the project the public key to be registered in.
type: str
state:
description:
- State of the public key.
type: str
default: present
choices: [ present, absent ]
public_key:
description:
- String of the public key.
type: str
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create a new private / public key pair
cs_sshkeypair:
name: linus@example.com
delegate_to: localhost
register: key
- debug:
msg: 'Private key is {{ key.private_key }}'
- name: remove a public key by its name
cs_sshkeypair:
name: linus@example.com
state: absent
delegate_to: localhost
- name: register your existing local public key
cs_sshkeypair:
name: linus@example.com
public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the SSH public key.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: Name of the SSH public key.
returned: success
type: str
sample: linus@example.com
fingerprint:
description: Fingerprint of the SSH public key.
returned: success
type: str
sample: "86:5e:a3:e8:bd:95:7b:07:7c:c2:5c:f7:ad:8b:09:28"
private_key:
description: Private key of generated SSH keypair.
returned: changed
type: str
sample: "-----BEGIN RSA PRIVATE KEY-----\nMII...8tO\n-----END RSA PRIVATE KEY-----\n"
'''
import traceback
SSHPUBKEYS_IMP_ERR = None
try:
import sshpubkeys
HAS_LIB_SSHPUBKEYS = True
except ImportError:
SSHPUBKEYS_IMP_ERR = traceback.format_exc()
HAS_LIB_SSHPUBKEYS = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_required_together,
cs_argument_spec
)
class AnsibleCloudStackSshKey(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackSshKey, self).__init__(module)
self.returns = {
'privatekey': 'private_key',
'fingerprint': 'fingerprint',
}
self.ssh_key = None
def register_ssh_key(self, public_key):
ssh_key = self.get_ssh_key()
args = self._get_common_args()
name = self.module.params.get('name')
res = None
if not ssh_key:
self.result['changed'] = True
args['publickey'] = public_key
if not self.module.check_mode:
args['name'] = name
res = self.query_api('registerSSHKeyPair', **args)
else:
fingerprint = self._get_ssh_fingerprint(public_key)
if ssh_key['fingerprint'] != fingerprint:
self.result['changed'] = True
if not self.module.check_mode:
# delete the ssh key with matching name but wrong fingerprint
args['name'] = name
self.query_api('deleteSSHKeyPair', **args)
elif ssh_key['name'].lower() != name.lower():
self.result['changed'] = True
if not self.module.check_mode:
# delete the ssh key with matching fingerprint but wrong name
args['name'] = ssh_key['name']
self.query_api('deleteSSHKeyPair', **args)
# First match for key retrievement will be the fingerprint.
# We need to make another lookup if there is a key with identical name.
self.ssh_key = None
ssh_key = self.get_ssh_key()
if ssh_key and ssh_key['fingerprint'] != fingerprint:
args['name'] = name
self.query_api('deleteSSHKeyPair', **args)
if not self.module.check_mode and self.result['changed']:
args['publickey'] = public_key
args['name'] = name
res = self.query_api('registerSSHKeyPair', **args)
if res and 'keypair' in res:
ssh_key = res['keypair']
return ssh_key
def create_ssh_key(self):
ssh_key = self.get_ssh_key()
if not ssh_key:
self.result['changed'] = True
args = self._get_common_args()
args['name'] = self.module.params.get('name')
if not self.module.check_mode:
res = self.query_api('createSSHKeyPair', **args)
ssh_key = res['keypair']
return ssh_key
def remove_ssh_key(self, name=None):
ssh_key = self.get_ssh_key()
if ssh_key:
self.result['changed'] = True
args = self._get_common_args()
args['name'] = name or self.module.params.get('name')
if not self.module.check_mode:
self.query_api('deleteSSHKeyPair', **args)
return ssh_key
def _get_common_args(self):
return {
'domainid': self.get_domain('id'),
'account': self.get_account('name'),
'projectid': self.get_project('id')
}
def get_ssh_key(self):
if not self.ssh_key:
public_key = self.module.params.get('public_key')
if public_key:
# Query by fingerprint of the public key
args_fingerprint = self._get_common_args()
args_fingerprint['fingerprint'] = self._get_ssh_fingerprint(public_key)
ssh_keys = self.query_api('listSSHKeyPairs', **args_fingerprint)
if ssh_keys and 'sshkeypair' in ssh_keys:
self.ssh_key = ssh_keys['sshkeypair'][0]
# When key has not been found by fingerprint, use the name
if not self.ssh_key:
args_name = self._get_common_args()
args_name['name'] = self.module.params.get('name')
ssh_keys = self.query_api('listSSHKeyPairs', **args_name)
if ssh_keys and 'sshkeypair' in ssh_keys:
self.ssh_key = ssh_keys['sshkeypair'][0]
return self.ssh_key
def _get_ssh_fingerprint(self, public_key):
key = sshpubkeys.SSHKey(public_key)
if hasattr(key, 'hash_md5'):
return key.hash_md5().replace(to_native('MD5:'), to_native(''))
return key.hash()
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
public_key=dict(),
domain=dict(),
account=dict(),
project=dict(),
state=dict(choices=['present', 'absent'], default='present'),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
if not HAS_LIB_SSHPUBKEYS:
module.fail_json(msg=missing_required_lib("sshpubkeys"), exception=SSHPUBKEYS_IMP_ERR)
acs_sshkey = AnsibleCloudStackSshKey(module)
state = module.params.get('state')
if state in ['absent']:
ssh_key = acs_sshkey.remove_ssh_key()
else:
public_key = module.params.get('public_key')
if public_key:
ssh_key = acs_sshkey.register_ssh_key(public_key)
else:
ssh_key = acs_sshkey.create_ssh_key()
result = acs_sshkey.get_result(ssh_key)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,255 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_staticnat
short_description: Manages static NATs on Apache CloudStack based clouds.
description:
- Create, update and remove static NATs.
author: René Moser (@resmo)
options:
ip_address:
description:
- Public IP address the static NAT is assigned to.
type: str
required: true
vm:
description:
- Name of virtual machine which we make the static NAT for.
- Required if I(state=present).
type: str
vm_guest_ip:
description:
- VM guest NIC secondary IP address for the static NAT.
type: str
network:
description:
- Network the IP address is related to.
type: str
vpc:
description:
- VPC the network related to.
type: str
state:
description:
- State of the static NAT.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the static NAT is related to.
type: str
account:
description:
- Account the static NAT is related to.
type: str
project:
description:
- Name of the project the static NAT is related to.
type: str
zone:
description:
- Name of the zone in which the virtual machine is in.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a static NAT for IP 1.2.3.4 to web01
cs_staticnat:
ip_address: 1.2.3.4
vm: web01
delegate_to: localhost
- name: Remove a static NAT
cs_staticnat:
ip_address: 1.2.3.4
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the ip_address.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
ip_address:
description: Public IP address.
returned: success
type: str
sample: 1.2.3.4
vm_name:
description: Name of the virtual machine.
returned: success
type: str
sample: web-01
vm_display_name:
description: Display name of the virtual machine.
returned: success
type: str
sample: web-01
vm_guest_ip:
description: IP of the virtual machine.
returned: success
type: str
sample: 10.101.65.152
zone:
description: Name of zone the static NAT is related to.
returned: success
type: str
sample: ch-gva-2
project:
description: Name of project the static NAT is related to.
returned: success
type: str
sample: Production
account:
description: Account the static NAT is related to.
returned: success
type: str
sample: example account
domain:
description: Domain the static NAT is related to.
returned: success
type: str
sample: example domain
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackStaticNat(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackStaticNat, self).__init__(module)
self.returns = {
'virtualmachinedisplayname': 'vm_display_name',
'virtualmachinename': 'vm_name',
'ipaddress': 'ip_address',
'vmipaddress': 'vm_guest_ip',
}
def create_static_nat(self, ip_address):
self.result['changed'] = True
args = {
'virtualmachineid': self.get_vm(key='id'),
'ipaddressid': ip_address['id'],
'vmguestip': self.get_vm_guest_ip(),
'networkid': self.get_network(key='id')
}
if not self.module.check_mode:
self.query_api('enableStaticNat', **args)
# reset ip address and query new values
self.ip_address = None
ip_address = self.get_ip_address()
return ip_address
def update_static_nat(self, ip_address):
args = {
'virtualmachineid': self.get_vm(key='id'),
'ipaddressid': ip_address['id'],
'vmguestip': self.get_vm_guest_ip(),
'networkid': self.get_network(key='id')
}
# make an alias, so we can use has_changed()
ip_address['vmguestip'] = ip_address['vmipaddress']
if self.has_changed(args, ip_address, ['vmguestip', 'virtualmachineid']):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('disableStaticNat', ipaddressid=ip_address['id'])
self.poll_job(res, 'staticnat')
self.query_api('enableStaticNat', **args)
# reset ip address and query new values
self.ip_address = None
ip_address = self.get_ip_address()
return ip_address
def present_static_nat(self):
ip_address = self.get_ip_address()
if not ip_address['isstaticnat']:
ip_address = self.create_static_nat(ip_address)
else:
ip_address = self.update_static_nat(ip_address)
return ip_address
def absent_static_nat(self):
ip_address = self.get_ip_address()
if ip_address['isstaticnat']:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('disableStaticNat', ipaddressid=ip_address['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'staticnat')
return ip_address
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
ip_address=dict(required=True),
vm=dict(),
vm_guest_ip=dict(),
network=dict(),
vpc=dict(),
state=dict(choices=['present', 'absent'], default='present'),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_static_nat = AnsibleCloudStackStaticNat(module)
state = module.params.get('state')
if state in ['absent']:
ip_address = acs_static_nat.absent_static_nat()
else:
ip_address = acs_static_nat.present_static_nat()
result = acs_static_nat.get_result(ip_address)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,510 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, Netservers Ltd. <support@netservers.co.uk>
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_storage_pool
short_description: Manages Primary Storage Pools on Apache CloudStack based clouds.
description:
- Create, update, put into maintenance, disable, enable and remove storage pools.
author:
- Netservers Ltd. (@netservers)
- René Moser (@resmo)
options:
name:
description:
- Name of the storage pool.
type: str
required: true
zone:
description:
- Name of the zone in which the host should be deployed.
- If not set, default zone is used.
type: str
storage_url:
description:
- URL of the storage pool.
- Required if I(state=present).
type: str
pod:
description:
- Name of the pod.
type: str
cluster:
description:
- Name of the cluster.
type: str
scope:
description:
- The scope of the storage pool.
- Defaults to cluster when C(cluster) is provided, otherwise zone.
type: str
choices: [ cluster, zone ]
managed:
description:
- Whether the storage pool should be managed by CloudStack.
- Only considered on creation.
type: bool
hypervisor:
description:
- Required when creating a zone scoped pool.
- Possible values are C(KVM), C(VMware), C(BareMetal), C(XenServer), C(LXC), C(HyperV), C(UCS), C(OVM), C(Simulator).
type: str
storage_tags:
description:
- Tags associated with this storage pool.
type: list
aliases: [ storage_tag ]
provider:
description:
- Name of the storage provider e.g. SolidFire, SolidFireShared, DefaultPrimary, CloudByte.
type: str
default: DefaultPrimary
capacity_bytes:
description:
- Bytes CloudStack can provision from this storage pool.
type: int
capacity_iops:
description:
- Bytes CloudStack can provision from this storage pool.
type: int
allocation_state:
description:
- Allocation state of the storage pool.
type: str
choices: [ enabled, disabled, maintenance ]
state:
description:
- State of the storage pool.
type: str
default: present
choices: [ present, absent ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: ensure a zone scoped storage_pool is present
cs_storage_pool:
zone: zone01
storage_url: rbd://admin:SECRET@ceph-mons.domain/poolname
provider: DefaultPrimary
name: Ceph RBD
scope: zone
hypervisor: KVM
delegate_to: localhost
- name: ensure a cluster scoped storage_pool is disabled
cs_storage_pool:
name: Ceph RBD
zone: zone01
cluster: cluster01
pod: pod01
storage_url: rbd://admin:SECRET@ceph-the-mons.domain/poolname
provider: DefaultPrimary
scope: cluster
allocation_state: disabled
delegate_to: localhost
- name: ensure a cluster scoped storage_pool is in maintenance
cs_storage_pool:
name: Ceph RBD
zone: zone01
cluster: cluster01
pod: pod01
storage_url: rbd://admin:SECRET@ceph-the-mons.domain/poolname
provider: DefaultPrimary
scope: cluster
allocation_state: maintenance
delegate_to: localhost
- name: ensure a storage_pool is absent
cs_storage_pool:
name: Ceph RBD
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the pool.
returned: success
type: str
sample: a3fca65a-7db1-4891-b97c-48806a978a96
created:
description: Date of the pool was created.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
capacity_iops:
description: IOPS CloudStack can provision from this storage pool
returned: when available
type: int
sample: 60000
zone:
description: The name of the zone.
returned: success
type: str
sample: Zone01
cluster:
description: The name of the cluster.
returned: when scope is cluster
type: str
sample: Cluster01
pod:
description: The name of the pod.
returned: when scope is cluster
type: str
sample: Cluster01
disk_size_allocated:
description: The pool's currently allocated disk space.
returned: success
type: int
sample: 2443517624320
disk_size_total:
description: The total size of the pool.
returned: success
type: int
sample: 3915055693824
disk_size_used:
description: The pool's currently used disk size.
returned: success
type: int
sample: 1040862622180
scope:
description: The scope of the storage pool.
returned: success
type: str
sample: cluster
hypervisor:
description: Hypervisor related to this storage pool.
returned: when available
type: str
sample: KVM
state:
description: The state of the storage pool as returned by the API.
returned: success
type: str
sample: Up
allocation_state:
description: The state of the storage pool.
returned: success
type: str
sample: enabled
path:
description: The storage pool path used in the storage_url.
returned: success
type: str
sample: poolname
overprovision_factor:
description: The overprovision factor of the storage pool.
returned: success
type: str
sample: 2.0
suitable_for_migration:
description: Whether the storage pool is suitable to migrate a volume or not.
returned: success
type: bool
sample: false
storage_capabilities:
description: Capabilities of the storage pool.
returned: success
type: dict
sample: {"VOLUME_SNAPSHOT_QUIESCEVM": "false"}
storage_tags:
description: the tags for the storage pool.
returned: success
type: list
sample: ["perf", "ssd"]
'''
# import cloudstack common
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackStoragePool(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackStoragePool, self).__init__(module)
self.returns = {
'capacityiops': 'capacity_iops',
'podname': 'pod',
'clustername': 'cluster',
'disksizeallocated': 'disk_size_allocated',
'disksizetotal': 'disk_size_total',
'disksizeused': 'disk_size_used',
'scope': 'scope',
'hypervisor': 'hypervisor',
'type': 'type',
'ip_address': 'ipaddress',
'path': 'path',
'overprovisionfactor': 'overprovision_factor',
'storagecapabilities': 'storage_capabilities',
'suitableformigration': 'suitable_for_migration',
}
self.allocation_states = {
# Host state: param state
'Up': 'enabled',
'Disabled': 'disabled',
'Maintenance': 'maintenance',
}
self.storage_pool = None
def _get_common_args(self):
return {
'name': self.module.params.get('name'),
'url': self.module.params.get('storage_url'),
'zoneid': self.get_zone(key='id'),
'provider': self.get_storage_provider(),
'scope': self.module.params.get('scope'),
'hypervisor': self.module.params.get('hypervisor'),
'capacitybytes': self.module.params.get('capacity_bytes'),
'capacityiops': self.module.params.get('capacity_iops'),
}
def _allocation_state_enabled_disabled_changed(self, pool, allocation_state):
if allocation_state in ['enabled', 'disabled']:
for pool_state, param_state in self.allocation_states.items():
if pool_state == pool['state'] and allocation_state != param_state:
return True
return False
def _handle_allocation_state(self, pool, state=None):
allocation_state = state or self.module.params.get('allocation_state')
if not allocation_state:
return pool
if self.allocation_states.get(pool['state']) == allocation_state:
return pool
# Cancel maintenance if target state is enabled/disabled
elif allocation_state in ['enabled', 'disabled']:
pool = self._cancel_maintenance(pool)
pool = self._update_storage_pool(pool=pool, allocation_state=allocation_state)
# Only an enabled host can put in maintenance
elif allocation_state == 'maintenance':
pool = self._update_storage_pool(pool=pool, allocation_state='enabled')
pool = self._enable_maintenance(pool=pool)
return pool
def _create_storage_pool(self):
args = self._get_common_args()
args.update({
'clusterid': self.get_cluster(key='id'),
'podid': self.get_pod(key='id'),
'managed': self.module.params.get('managed'),
})
scope = self.module.params.get('scope')
if scope is None:
args['scope'] = 'cluster' if args['clusterid'] else 'zone'
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('createStoragePool', **args)
return res['storagepool']
def _update_storage_pool(self, pool, allocation_state=None):
args = {
'id': pool['id'],
'capacitybytes': self.module.params.get('capacity_bytes'),
'capacityiops': self.module.params.get('capacity_iops'),
'tags': self.get_storage_tags(),
}
if self.has_changed(args, pool) or self._allocation_state_enabled_disabled_changed(pool, allocation_state):
self.result['changed'] = True
args['enabled'] = allocation_state == 'enabled' if allocation_state in ['enabled', 'disabled'] else None
if not self.module.check_mode:
res = self.query_api('updateStoragePool', **args)
pool = res['storagepool']
return pool
def _enable_maintenance(self, pool):
if pool['state'].lower() != "maintenance":
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('enableStorageMaintenance', id=pool['id'])
pool = self.poll_job(res, 'storagepool')
return pool
def _cancel_maintenance(self, pool):
if pool['state'].lower() == "maintenance":
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('cancelStorageMaintenance', id=pool['id'])
pool = self.poll_job(res, 'storagepool')
return pool
def get_storage_tags(self):
storage_tags = self.module.params.get('storage_tags')
if storage_tags is None:
return None
return ','.join(storage_tags)
def get_storage_pool(self, key=None):
if self.storage_pool is None:
zoneid = self.get_zone(key='id')
clusterid = self.get_cluster(key='id')
podid = self.get_pod(key='id')
args = {
'zoneid': zoneid,
'podid': podid,
'clusterid': clusterid,
'name': self.module.params.get('name'),
}
res = self.query_api('listStoragePools', **args)
if 'storagepool' not in res:
return None
self.storage_pool = res['storagepool'][0]
return self.storage_pool
def present_storage_pool(self):
pool = self.get_storage_pool()
if pool:
pool = self._update_storage_pool(pool=pool)
else:
pool = self._create_storage_pool()
if pool:
pool = self._handle_allocation_state(pool=pool)
return pool
def absent_storage_pool(self):
pool = self.get_storage_pool()
if pool:
self.result['changed'] = True
args = {
'id': pool['id'],
}
if not self.module.check_mode:
# Only a pool in maintenance can be deleted
self._handle_allocation_state(pool=pool, state='maintenance')
self.query_api('deleteStoragePool', **args)
return pool
def get_storage_provider(self, type="primary"):
args = {
'type': type,
}
provider = self.module.params.get('provider')
storage_providers = self.query_api('listStorageProviders', **args)
for sp in storage_providers.get('dataStoreProvider') or []:
if sp['name'].lower() == provider.lower():
return provider
self.fail_json(msg="Storage provider %s not found" % provider)
def get_pod(self, key=None):
pod = self.module.params.get('pod')
if not pod:
return None
args = {
'name': pod,
'zoneid': self.get_zone(key='id'),
}
pods = self.query_api('listPods', **args)
if pods:
return self._get_by_key(key, pods['pod'][0])
self.fail_json(msg="Pod %s not found" % self.module.params.get('pod'))
def get_cluster(self, key=None):
cluster = self.module.params.get('cluster')
if not cluster:
return None
args = {
'name': cluster,
'zoneid': self.get_zone(key='id'),
}
clusters = self.query_api('listClusters', **args)
if clusters:
return self._get_by_key(key, clusters['cluster'][0])
self.fail_json(msg="Cluster %s not found" % cluster)
def get_result(self, pool):
super(AnsibleCloudStackStoragePool, self).get_result(pool)
if pool:
self.result['storage_url'] = "%s://%s/%s" % (pool['type'], pool['ipaddress'], pool['path'])
self.result['scope'] = pool['scope'].lower()
self.result['storage_tags'] = pool['tags'].split(',') if pool.get('tags') else []
self.result['allocation_state'] = self.allocation_states.get(pool['state'])
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
storage_url=dict(),
zone=dict(),
pod=dict(),
cluster=dict(),
scope=dict(choices=['zone', 'cluster']),
hypervisor=dict(),
provider=dict(default='DefaultPrimary'),
capacity_bytes=dict(type='int'),
capacity_iops=dict(type='int'),
managed=dict(type='bool'),
storage_tags=dict(type='list', aliases=['storage_tag']),
allocation_state=dict(choices=['enabled', 'disabled', 'maintenance']),
state=dict(choices=['present', 'absent'], default='present'),
))
required_together = cs_required_together()
required_together.extend([
['pod', 'cluster'],
])
module = AnsibleModule(
argument_spec=argument_spec,
required_together=required_together,
required_if=[
('state', 'present', ['storage_url']),
],
supports_check_mode=True
)
acs_storage_pool = AnsibleCloudStackStoragePool(module)
state = module.params.get('state')
if state in ['absent']:
pool = acs_storage_pool.absent_storage_pool()
else:
pool = acs_storage_pool.present_storage_pool()
result = acs_storage_pool.get_result(pool)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,744 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_template
short_description: Manages templates on Apache CloudStack based clouds.
description:
- Register templates from an URL.
- Create templates from a ROOT volume of a stopped VM or its snapshot.
- Update (since version 2.7), extract and delete templates.
author: René Moser (@resmo)
options:
name:
description:
- Name of the template.
type: str
required: true
url:
description:
- URL of where the template is hosted on I(state=present).
- URL to which the template would be extracted on I(state=extracted).
- Mutually exclusive with I(vm).
type: str
vm:
description:
- VM name the template will be created from its volume or alternatively from a snapshot.
- VM must be in stopped state if created from its volume.
- Mutually exclusive with I(url).
type: str
snapshot:
description:
- Name of the snapshot, created from the VM ROOT volume, the template will be created from.
- I(vm) is required together with this argument.
type: str
os_type:
description:
- OS type that best represents the OS of this template.
type: str
checksum:
description:
- The MD5 checksum value of this template.
- If set, we search by checksum instead of name.
type: str
is_ready:
description:
- "Note: this flag was not implemented and therefore marked as deprecated."
- Deprecated, will be removed in version 2.11.
type: bool
is_public:
description:
- Register the template to be publicly available to all users.
- Only used if I(state) is C(present).
type: bool
is_featured:
description:
- Register the template to be featured.
- Only used if I(state) is C(present).
type: bool
is_dynamically_scalable:
description:
- Register the template having XS/VMware tools installed in order to support dynamic scaling of VM CPU/memory.
- Only used if I(state) is C(present).
type: bool
cross_zones:
description:
- Whether the template should be synced or removed across zones.
- Only used if I(state) is C(present) or C(absent).
default: no
type: bool
mode:
description:
- Mode for the template extraction.
- Only used if I(state=extracted).
type: str
default: http_download
choices: [ http_download, ftp_upload ]
domain:
description:
- Domain the template, snapshot or VM is related to.
type: str
account:
description:
- Account the template, snapshot or VM is related to.
type: str
project:
description:
- Name of the project the template to be registered in.
type: str
zone:
description:
- Name of the zone you wish the template to be registered or deleted from.
- If not specified, first found zone will be used.
type: str
template_filter:
description:
- Name of the filter used to search for the template.
- The filter C(all) was added in 2.7.
type: str
default: self
choices: [ all, featured, self, selfexecutable, sharedexecutable, executable, community ]
template_find_options:
description:
- Options to find a template uniquely.
- More than one allowed.
type: list
choices: [ display_text, checksum, cross_zones ]
aliases: [ template_find_option ]
default: []
hypervisor:
description:
- Name the hypervisor to be used for creating the new template.
- Relevant when using I(state=present).
- Possible values are C(KVM), C(VMware), C(BareMetal), C(XenServer), C(LXC), C(HyperV), C(UCS), C(OVM), C(Simulator).
type: str
requires_hvm:
description:
- Whether the template requires HVM or not.
- Only considered while creating the template.
type: bool
password_enabled:
description:
- Enable template password reset support.
type: bool
template_tag:
description:
- The tag for this template.
type: str
sshkey_enabled:
description:
- True if the template supports the sshkey upload feature.
- Only considered if I(url) is used (API limitation).
type: bool
is_routing:
description:
- Sets the template type to routing, i.e. if template is used to deploy routers.
- Only considered if I(url) is used.
type: bool
format:
description:
- The format for the template.
- Only considered if I(state=present).
type: str
choices: [ QCOW2, RAW, VHD, OVA ]
is_extractable:
description:
- Allows the template or its derivatives to be extractable.
type: bool
details:
description:
- Template details in key/value pairs.
type: str
bits:
description:
- 32 or 64 bits support.
type: int
default: 64
choices: [ 32, 64 ]
display_text:
description:
- Display text of the template.
type: str
state:
description:
- State of the template.
type: str
default: present
choices: [ present, absent, extracted ]
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: register a systemvm template
cs_template:
name: systemvm-vmware-4.5
url: "http://packages.shapeblue.com/systemvmtemplate/4.5/systemvm64template-4.5-vmware.ova"
hypervisor: VMware
format: OVA
cross_zones: yes
os_type: Debian GNU/Linux 7(64-bit)
delegate_to: localhost
- name: Create a template from a stopped virtual machine's volume
cs_template:
name: Debian 9 (64-bit) 20GB ({{ ansible_date_time.date }})
vm: debian-9-base-vm
os_type: Debian GNU/Linux 9 (64-bit)
zone: tokio-ix
password_enabled: yes
is_public: yes
delegate_to: localhost
# Note: Use template_find_option(s) when a template name is not unique
- name: Create a template from a stopped virtual machine's volume
cs_template:
name: Debian 9 (64-bit)
display_text: Debian 9 (64-bit) 20GB ({{ ansible_date_time.date }})
template_find_option: display_text
vm: debian-9-base-vm
os_type: Debian GNU/Linux 9 (64-bit)
zone: tokio-ix
password_enabled: yes
is_public: yes
delegate_to: localhost
- name: create a template from a virtual machine's root volume snapshot
cs_template:
name: Debian 9 (64-bit) Snapshot ROOT-233_2015061509114
snapshot: ROOT-233_2015061509114
os_type: Debian GNU/Linux 9 (64-bit)
zone: tokio-ix
password_enabled: yes
is_public: yes
delegate_to: localhost
- name: Remove a template
cs_template:
name: systemvm-4.2
cross_zones: yes
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the template or extracted object.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: Name of the template or extracted object.
returned: success
type: str
sample: Debian 7 64-bit
display_text:
description: Display text of the template.
returned: if available
type: str
sample: Debian 7.7 64-bit minimal 2015-03-19
checksum:
description: MD5 checksum of the template.
returned: if available
type: str
sample: 0b31bccccb048d20b551f70830bb7ad0
status:
description: Status of the template or extracted object.
returned: success
type: str
sample: Download Complete
is_ready:
description: True if the template is ready to be deployed from.
returned: if available
type: bool
sample: true
is_public:
description: True if the template is public.
returned: if available
type: bool
sample: true
is_featured:
description: True if the template is featured.
returned: if available
type: bool
sample: true
is_extractable:
description: True if the template is extractable.
returned: if available
type: bool
sample: true
format:
description: Format of the template.
returned: if available
type: str
sample: OVA
os_type:
description: Type of the OS.
returned: if available
type: str
sample: CentOS 6.5 (64-bit)
password_enabled:
description: True if the reset password feature is enabled, false otherwise.
returned: if available
type: bool
sample: false
sshkey_enabled:
description: true if template is sshkey enabled, false otherwise.
returned: if available
type: bool
sample: false
cross_zones:
description: true if the template is managed across all zones, false otherwise.
returned: if available
type: bool
sample: false
template_type:
description: Type of the template.
returned: if available
type: str
sample: USER
created:
description: Date of registering.
returned: success
type: str
sample: 2015-03-29T14:57:06+0200
template_tag:
description: Template tag related to this template.
returned: if available
type: str
sample: special
hypervisor:
description: Hypervisor related to this template.
returned: if available
type: str
sample: VMware
mode:
description: Mode of extraction
returned: on state=extracted
type: str
sample: http_download
state:
description: State of the extracted template
returned: on state=extracted
type: str
sample: DOWNLOAD_URL_CREATED
url:
description: Url to which the template is extracted to
returned: on state=extracted
type: str
sample: "http://1.2.3.4/userdata/eb307f13-4aca-45e8-b157-a414a14e6b04.ova"
tags:
description: List of resource tags associated with the template.
returned: if available
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
zone:
description: Name of zone the template is registered in.
returned: success
type: str
sample: zuerich
domain:
description: Domain the template is related to.
returned: success
type: str
sample: example domain
account:
description: Account the template is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the template is related to.
returned: success
type: str
sample: Production
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackTemplate(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackTemplate, self).__init__(module)
self.returns = {
'checksum': 'checksum',
'status': 'status',
'isready': 'is_ready',
'templatetag': 'template_tag',
'sshkeyenabled': 'sshkey_enabled',
'passwordenabled': 'password_enabled',
'templatetype': 'template_type',
'ostypename': 'os_type',
'crossZones': 'cross_zones',
'format': 'format',
'hypervisor': 'hypervisor',
'url': 'url',
'extractMode': 'mode',
'state': 'state',
}
def _get_args(self):
args = {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'bits': self.module.params.get('bits'),
'isdynamicallyscalable': self.module.params.get('is_dynamically_scalable'),
'isextractable': self.module.params.get('is_extractable'),
'isfeatured': self.module.params.get('is_featured'),
'ispublic': self.module.params.get('is_public'),
'passwordenabled': self.module.params.get('password_enabled'),
'requireshvm': self.module.params.get('requires_hvm'),
'templatetag': self.module.params.get('template_tag'),
'ostypeid': self.get_os_type(key='id'),
}
if not args['ostypeid']:
self.module.fail_json(msg="Missing required arguments: os_type")
return args
def get_root_volume(self, key=None):
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'virtualmachineid': self.get_vm(key='id'),
'type': "ROOT"
}
volumes = self.query_api('listVolumes', **args)
if volumes:
return self._get_by_key(key, volumes['volume'][0])
self.module.fail_json(msg="Root volume for '%s' not found" % self.get_vm('name'))
def get_snapshot(self, key=None):
snapshot = self.module.params.get('snapshot')
if not snapshot:
return None
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'volumeid': self.get_root_volume('id'),
'fetch_list': True,
}
snapshots = self.query_api('listSnapshots', **args)
if snapshots:
for s in snapshots:
if snapshot in [s['name'], s['id']]:
return self._get_by_key(key, s)
self.module.fail_json(msg="Snapshot '%s' not found" % snapshot)
def present_template(self):
template = self.get_template()
if template:
template = self.update_template(template)
elif self.module.params.get('url'):
template = self.register_template()
elif self.module.params.get('vm'):
template = self.create_template()
else:
self.fail_json(msg="one of the following is required on state=present: url, vm")
return template
def create_template(self):
template = None
self.result['changed'] = True
args = self._get_args()
snapshot_id = self.get_snapshot(key='id')
if snapshot_id:
args['snapshotid'] = snapshot_id
else:
args['volumeid'] = self.get_root_volume('id')
if not self.module.check_mode:
template = self.query_api('createTemplate', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
template = self.poll_job(template, 'template')
if template:
template = self.ensure_tags(resource=template, resource_type='Template')
return template
def register_template(self):
required_params = [
'format',
'url',
'hypervisor',
]
self.module.fail_on_missing_params(required_params=required_params)
template = None
self.result['changed'] = True
args = self._get_args()
args.update({
'url': self.module.params.get('url'),
'format': self.module.params.get('format'),
'checksum': self.module.params.get('checksum'),
'isextractable': self.module.params.get('is_extractable'),
'isrouting': self.module.params.get('is_routing'),
'sshkeyenabled': self.module.params.get('sshkey_enabled'),
'hypervisor': self.get_hypervisor(),
'domainid': self.get_domain(key='id'),
'account': self.get_account(key='name'),
'projectid': self.get_project(key='id'),
})
if not self.module.params.get('cross_zones'):
args['zoneid'] = self.get_zone(key='id')
else:
args['zoneid'] = -1
if not self.module.check_mode:
self.query_api('registerTemplate', **args)
template = self.get_template()
return template
def update_template(self, template):
args = {
'id': template['id'],
'displaytext': self.get_or_fallback('display_text', 'name'),
'format': self.module.params.get('format'),
'isdynamicallyscalable': self.module.params.get('is_dynamically_scalable'),
'isrouting': self.module.params.get('is_routing'),
'ostypeid': self.get_os_type(key='id'),
'passwordenabled': self.module.params.get('password_enabled'),
}
if self.has_changed(args, template):
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('updateTemplate', **args)
template = self.get_template()
args = {
'id': template['id'],
'isextractable': self.module.params.get('is_extractable'),
'isfeatured': self.module.params.get('is_featured'),
'ispublic': self.module.params.get('is_public'),
}
if self.has_changed(args, template):
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('updateTemplatePermissions', **args)
# Refresh
template = self.get_template()
if template:
template = self.ensure_tags(resource=template, resource_type='Template')
return template
def _is_find_option(self, param_name):
return param_name in self.module.params.get('template_find_options')
def _find_option_match(self, template, param_name, internal_name=None):
if not internal_name:
internal_name = param_name
if param_name in self.module.params.get('template_find_options'):
param_value = self.module.params.get(param_name)
if not param_value:
self.fail_json(msg="The param template_find_options has %s but param was not provided." % param_name)
if template[internal_name] == param_value:
return True
return False
def get_template(self):
args = {
'name': self.module.params.get('name'),
'templatefilter': self.module.params.get('template_filter'),
'domainid': self.get_domain(key='id'),
'account': self.get_account(key='name'),
'projectid': self.get_project(key='id')
}
cross_zones = self.module.params.get('cross_zones')
if not cross_zones:
args['zoneid'] = self.get_zone(key='id')
template_found = None
templates = self.query_api('listTemplates', **args)
if templates:
for tmpl in templates['template']:
if self._is_find_option('cross_zones') and not self._find_option_match(
template=tmpl,
param_name='cross_zones',
internal_name='crossZones'):
continue
if self._is_find_option('checksum') and not self._find_option_match(
template=tmpl,
param_name='checksum'):
continue
if self._is_find_option('display_text') and not self._find_option_match(
template=tmpl,
param_name='display_text',
internal_name='displaytext'):
continue
if not template_found:
template_found = tmpl
# A cross zones template has one entry per zone but the same id
elif tmpl['id'] == template_found['id']:
continue
else:
self.fail_json(msg="Multiple templates found matching provided params. Please use template_find_options.")
return template_found
def extract_template(self):
template = self.get_template()
if not template:
self.module.fail_json(msg="Failed: template not found")
args = {
'id': template['id'],
'url': self.module.params.get('url'),
'mode': self.module.params.get('mode'),
'zoneid': self.get_zone(key='id')
}
self.result['changed'] = True
if not self.module.check_mode:
template = self.query_api('extractTemplate', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
template = self.poll_job(template, 'template')
return template
def remove_template(self):
template = self.get_template()
if template:
self.result['changed'] = True
args = {
'id': template['id']
}
if not self.module.params.get('cross_zones'):
args['zoneid'] = self.get_zone(key='id')
if not self.module.check_mode:
res = self.query_api('deleteTemplate', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
res = self.poll_job(res, 'template')
return template
def get_result(self, template):
super(AnsibleCloudStackTemplate, self).get_result(template)
if template:
if 'isextractable' in template:
self.result['is_extractable'] = True if template['isextractable'] else False
if 'isfeatured' in template:
self.result['is_featured'] = True if template['isfeatured'] else False
if 'ispublic' in template:
self.result['is_public'] = True if template['ispublic'] else False
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
url=dict(),
vm=dict(),
snapshot=dict(),
os_type=dict(),
is_ready=dict(type='bool', removed_in_version='2.11'),
is_public=dict(type='bool'),
is_featured=dict(type='bool'),
is_dynamically_scalable=dict(type='bool'),
is_extractable=dict(type='bool'),
is_routing=dict(type='bool'),
checksum=dict(),
template_filter=dict(default='self', choices=['all', 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']),
template_find_options=dict(type='list', choices=['display_text', 'checksum', 'cross_zones'], aliases=['template_find_option'], default=[]),
hypervisor=dict(),
requires_hvm=dict(type='bool'),
password_enabled=dict(type='bool'),
template_tag=dict(),
sshkey_enabled=dict(type='bool'),
format=dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA']),
details=dict(),
bits=dict(type='int', choices=[32, 64], default=64),
state=dict(choices=['present', 'absent', 'extracted'], default='present'),
cross_zones=dict(type='bool', default=False),
mode=dict(choices=['http_download', 'ftp_upload'], default='http_download'),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
mutually_exclusive=(
['url', 'vm'],
['zone', 'cross_zones'],
),
supports_check_mode=True
)
acs_tpl = AnsibleCloudStackTemplate(module)
state = module.params.get('state')
if state == 'absent':
tpl = acs_tpl.remove_template()
elif state == 'extracted':
tpl = acs_tpl.extract_template()
else:
tpl = acs_tpl.present_template()
result = acs_tpl.get_result(tpl)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,327 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2019, Patryk D. Cichy <patryk.d.cichy@gmail.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: cs_traffic_type
short_description: Manages traffic types on CloudStack Physical Networks
description:
- Add, remove, update Traffic Types associated with CloudStack Physical Networks.
extends_documentation_fragment:
- community.general.cloudstack
author:
- Patryk Cichy (@PatTheSilent)
options:
physical_network:
description:
- the name of the Physical Network
required: true
type: str
zone:
description:
- Name of the zone with the physical network.
- Default zone will be used if this is empty.
type: str
traffic_type:
description:
- the trafficType to be added to the physical network.
required: true
choices: [Management, Guest, Public, Storage]
type: str
state:
description:
- State of the traffic type
choices: [present, absent]
default: present
type: str
hyperv_networklabel:
description:
- The network name label of the physical device dedicated to this traffic on a HyperV host.
type: str
isolation_method:
description:
- Use if the physical network has multiple isolation types and traffic type is public.
choices: [vlan, vxlan]
type: str
kvm_networklabel:
description:
- The network name label of the physical device dedicated to this traffic on a KVM host.
type: str
ovm3_networklabel:
description:
- The network name of the physical device dedicated to this traffic on an OVM3 host.
type: str
vlan:
description:
- The VLAN id to be used for Management traffic by VMware host.
type: str
vmware_networklabel:
description:
- The network name label of the physical device dedicated to this traffic on a VMware host.
type: str
xen_networklabel:
description:
- The network name label of the physical device dedicated to this traffic on a XenServer host.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
'''
EXAMPLES = '''
- name: add a traffic type
cs_traffic_type:
physical_network: public-network
traffic_type: Guest
zone: test-zone
delegate_to: localhost
- name: update traffic type
cs_traffic_type:
physical_network: public-network
traffic_type: Guest
kvm_networklabel: cloudbr0
zone: test-zone
delegate_to: localhost
- name: remove traffic type
cs_traffic_type:
physical_network: public-network
traffic_type: Public
state: absent
zone: test-zone
delegate_to: localhost
'''
RETURN = '''
---
id:
description: ID of the network provider
returned: success
type: str
sample: 659c1840-9374-440d-a412-55ca360c9d3c
traffic_type:
description: the trafficType that was added to the physical network
returned: success
type: str
sample: Public
hyperv_networklabel:
description: The network name label of the physical device dedicated to this traffic on a HyperV host
returned: success
type: str
sample: HyperV Internal Switch
kvm_networklabel:
description: The network name label of the physical device dedicated to this traffic on a KVM host
returned: success
type: str
sample: cloudbr0
ovm3_networklabel:
description: The network name of the physical device dedicated to this traffic on an OVM3 host
returned: success
type: str
sample: cloudbr0
physical_network:
description: the physical network this belongs to
returned: success
type: str
sample: 28ed70b7-9a1f-41bf-94c3-53a9f22da8b6
vmware_networklabel:
description: The network name label of the physical device dedicated to this traffic on a VMware host
returned: success
type: str
sample: Management Network
xen_networklabel:
description: The network name label of the physical device dedicated to this traffic on a XenServer host
returned: success
type: str
sample: xenbr0
zone:
description: Name of zone the physical network is in.
returned: success
type: str
sample: ch-gva-2
'''
from ansible_collections.community.general.plugins.module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together
from ansible.module_utils.basic import AnsibleModule
class AnsibleCloudStackTrafficType(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackTrafficType, self).__init__(module)
self.returns = {
'traffictype': 'traffic_type',
'hypervnetworklabel': 'hyperv_networklabel',
'kvmnetworklabel': 'kvm_networklabel',
'ovm3networklabel': 'ovm3_networklabel',
'physicalnetworkid': 'physical_network',
'vmwarenetworklabel': 'vmware_networklabel',
'xennetworklabel': 'xen_networklabel'
}
self.traffic_type = None
def _get_label_args(self):
label_args = dict()
if self.module.params.get('hyperv_networklabel'):
label_args.update(dict(hypervnetworklabel=self.module.params.get('hyperv_networklabel')))
if self.module.params.get('kvm_networklabel'):
label_args.update(dict(kvmnetworklabel=self.module.params.get('kvm_networklabel')))
if self.module.params.get('ovm3_networklabel'):
label_args.update(dict(ovm3networklabel=self.module.params.get('ovm3_networklabel')))
if self.module.params.get('vmware_networklabel'):
label_args.update(dict(vmwarenetworklabel=self.module.params.get('vmware_networklabel')))
return label_args
def _get_additional_args(self):
additional_args = dict()
if self.module.params.get('isolation_method'):
additional_args.update(dict(isolationmethod=self.module.params.get('isolation_method')))
if self.module.params.get('vlan'):
additional_args.update(dict(vlan=self.module.params.get('vlan')))
additional_args.update(self._get_label_args())
return additional_args
def get_traffic_types(self):
args = {
'physicalnetworkid': self.get_physical_network(key='id')
}
traffic_types = self.query_api('listTrafficTypes', **args)
return traffic_types
def get_traffic_type(self):
if self.traffic_type:
return self.traffic_type
traffic_type = self.module.params.get('traffic_type')
traffic_types = self.get_traffic_types()
if traffic_types:
for t_type in traffic_types['traffictype']:
if traffic_type.lower() in [t_type['traffictype'].lower(), t_type['id']]:
self.traffic_type = t_type
break
return self.traffic_type
def present_traffic_type(self):
traffic_type = self.get_traffic_type()
if traffic_type:
self.traffic_type = self.update_traffic_type()
else:
self.result['changed'] = True
self.traffic_type = self.add_traffic_type()
return self.traffic_type
def add_traffic_type(self):
traffic_type = self.module.params.get('traffic_type')
args = {
'physicalnetworkid': self.get_physical_network(key='id'),
'traffictype': traffic_type
}
args.update(self._get_additional_args())
if not self.module.check_mode:
resource = self.query_api('addTrafficType', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.traffic_type = self.poll_job(resource, 'traffictype')
return self.traffic_type
def absent_traffic_type(self):
traffic_type = self.get_traffic_type()
if traffic_type:
args = {
'id': traffic_type['id']
}
self.result['changed'] = True
if not self.module.check_mode:
resource = self.query_api('deleteTrafficType', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(resource, 'traffictype')
return traffic_type
def update_traffic_type(self):
traffic_type = self.get_traffic_type()
args = {
'id': traffic_type['id']
}
args.update(self._get_label_args())
if self.has_changed(args, traffic_type):
self.result['changed'] = True
if not self.module.check_mode:
resource = self.query_api('updateTrafficType', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.traffic_type = self.poll_job(resource, 'traffictype')
return self.traffic_type
def setup_module_object():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
physical_network=dict(required=True),
zone=dict(),
state=dict(choices=['present', 'absent'], default='present'),
traffic_type=dict(required=True, choices=['Management', 'Guest', 'Public', 'Storage']),
hyperv_networklabel=dict(),
isolation_method=dict(choices=['vlan', 'vxlan']),
kvm_networklabel=dict(),
ovm3_networklabel=dict(),
vlan=dict(),
vmware_networklabel=dict(),
xen_networklabel=dict(),
poll_async=dict(type='bool', default=True)
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
return module
def execute_module(module):
actt = AnsibleCloudStackTrafficType(module)
state = module.params.get('state')
if state in ['present']:
result = actt.present_traffic_type()
else:
result = actt.absent_traffic_type()
return actt.get_result(result)
def main():
module = setup_module_object()
result = execute_module(module)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,445 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_user
short_description: Manages users on Apache CloudStack based clouds.
description:
- Create, update, disable, lock, enable and remove users.
author: René Moser (@resmo)
options:
username:
description:
- Username of the user.
type: str
required: true
account:
description:
- Account the user will be created under.
- Required on I(state=present).
type: str
password:
description:
- Password of the user to be created.
- Required on I(state=present).
- Only considered on creation and will not be updated if user exists.
type: str
first_name:
description:
- First name of the user.
- Required on I(state=present).
type: str
last_name:
description:
- Last name of the user.
- Required on I(state=present).
type: str
email:
description:
- Email of the user.
- Required on I(state=present).
type: str
timezone:
description:
- Timezone of the user.
type: str
keys_registered:
description:
- If API keys of the user should be generated.
- "Note: Keys can not be removed by the API again."
type: bool
default: no
domain:
description:
- Domain the user is related to.
type: str
default: ROOT
state:
description:
- State of the user.
- C(unlocked) is an alias for C(enabled).
type: str
default: present
choices: [ present, absent, enabled, disabled, locked, unlocked ]
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create an user in domain 'CUSTOMERS'
cs_user:
account: developers
username: johndoe
password: S3Cur3
last_name: Doe
first_name: John
email: john.doe@example.com
domain: CUSTOMERS
delegate_to: localhost
- name: Lock an existing user in domain 'CUSTOMERS'
cs_user:
username: johndoe
domain: CUSTOMERS
state: locked
delegate_to: localhost
- name: Disable an existing user in domain 'CUSTOMERS'
cs_user:
username: johndoe
domain: CUSTOMERS
state: disabled
delegate_to: localhost
- name: Enable/unlock an existing user in domain 'CUSTOMERS'
cs_user:
username: johndoe
domain: CUSTOMERS
state: enabled
delegate_to: localhost
- name: Remove an user in domain 'CUSTOMERS'
cs_user:
name: customer_xy
domain: CUSTOMERS
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the user.
returned: success
type: str
sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8
username:
description: Username of the user.
returned: success
type: str
sample: johndoe
fist_name:
description: First name of the user.
returned: success
type: str
sample: John
last_name:
description: Last name of the user.
returned: success
type: str
sample: Doe
email:
description: Emailof the user.
returned: success
type: str
sample: john.doe@example.com
user_api_key:
description: API key of the user.
returned: success
type: str
sample: JLhcg8VWi8DoFqL2sSLZMXmGojcLnFrOBTipvBHJjySODcV4mCOo29W2duzPv5cALaZnXj5QxDx3xQfaQt3DKg
user_api_secret:
description: API secret of the user.
returned: success
type: str
sample: FUELo3LB9fa1UopjTLPdqLv_6OXQMJZv9g9N4B_Ao3HFz8d6IGFCV9MbPFNM8mwz00wbMevja1DoUNDvI8C9-g
account:
description: Account name of the user.
returned: success
type: str
sample: developers
account_type:
description: Type of the account.
returned: success
type: str
sample: user
timezone:
description: Timezone of the user.
returned: success
type: str
sample: enabled
created:
description: Date the user was created.
returned: success
type: str
sample: Doe
state:
description: State of the user.
returned: success
type: str
sample: enabled
domain:
description: Domain the user is related.
returned: success
type: str
sample: ROOT
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackUser(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackUser, self).__init__(module)
self.returns = {
'username': 'username',
'firstname': 'first_name',
'lastname': 'last_name',
'email': 'email',
'secretkey': 'user_api_secret',
'apikey': 'user_api_key',
'timezone': 'timezone',
}
self.account_types = {
'user': 0,
'root_admin': 1,
'domain_admin': 2,
}
self.user = None
def get_account_type(self):
account_type = self.module.params.get('account_type')
return self.account_types[account_type]
def get_user(self):
if not self.user:
args = {
'domainid': self.get_domain('id'),
'fetch_list': True,
}
users = self.query_api('listUsers', **args)
if users:
user_name = self.module.params.get('username')
for u in users:
if user_name.lower() == u['username'].lower():
self.user = u
break
return self.user
def enable_user(self):
user = self.get_user()
if not user:
user = self.present_user()
if user['state'].lower() != 'enabled':
self.result['changed'] = True
args = {
'id': user['id'],
}
if not self.module.check_mode:
res = self.query_api('enableUser', **args)
user = res['user']
return user
def lock_user(self):
user = self.get_user()
if not user:
user = self.present_user()
# we need to enable the user to lock it.
if user['state'].lower() == 'disabled':
user = self.enable_user()
if user['state'].lower() != 'locked':
self.result['changed'] = True
args = {
'id': user['id'],
}
if not self.module.check_mode:
res = self.query_api('lockUser', **args)
user = res['user']
return user
def disable_user(self):
user = self.get_user()
if not user:
user = self.present_user()
if user['state'].lower() != 'disabled':
self.result['changed'] = True
args = {
'id': user['id'],
}
if not self.module.check_mode:
user = self.query_api('disableUser', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
user = self.poll_job(user, 'user')
return user
def present_user(self):
required_params = [
'account',
'email',
'password',
'first_name',
'last_name',
]
self.module.fail_on_missing_params(required_params=required_params)
user = self.get_user()
if user:
user = self._update_user(user)
else:
user = self._create_user(user)
return user
def _get_common_args(self):
return {
'firstname': self.module.params.get('first_name'),
'lastname': self.module.params.get('last_name'),
'email': self.module.params.get('email'),
'timezone': self.module.params.get('timezone'),
}
def _create_user(self, user):
self.result['changed'] = True
args = self._get_common_args()
args.update({
'account': self.get_account(key='name'),
'domainid': self.get_domain('id'),
'username': self.module.params.get('username'),
'password': self.module.params.get('password'),
})
if not self.module.check_mode:
res = self.query_api('createUser', **args)
user = res['user']
# register user api keys
if self.module.params.get('keys_registered'):
res = self.query_api('registerUserKeys', id=user['id'])
user.update(res['userkeys'])
return user
def _update_user(self, user):
args = self._get_common_args()
args.update({
'id': user['id'],
})
if self.has_changed(args, user):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateUser', **args)
user = res['user']
# register user api keys
if 'apikey' not in user and self.module.params.get('keys_registered'):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('registerUserKeys', id=user['id'])
user.update(res['userkeys'])
return user
def absent_user(self):
user = self.get_user()
if user:
self.result['changed'] = True
if not self.module.check_mode:
self.query_api('deleteUser', id=user['id'])
return user
def get_result(self, user):
super(AnsibleCloudStackUser, self).get_result(user)
if user:
if 'accounttype' in user:
for key, value in self.account_types.items():
if value == user['accounttype']:
self.result['account_type'] = key
break
# secretkey has been removed since CloudStack 4.10 from listUsers API
if self.module.params.get('keys_registered') and 'apikey' in user and 'secretkey' not in user:
user_keys = self.query_api('getUserKeys', id=user['id'])
if user_keys:
self.result['user_api_secret'] = user_keys['userkeys'].get('secretkey')
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
username=dict(required=True),
account=dict(),
state=dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'),
domain=dict(default='ROOT'),
email=dict(),
first_name=dict(),
last_name=dict(),
password=dict(no_log=True),
timezone=dict(),
keys_registered=dict(type='bool', default=False),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_acc = AnsibleCloudStackUser(module)
state = module.params.get('state')
if state == 'absent':
user = acs_acc.absent_user()
elif state in ['enabled', 'unlocked']:
user = acs_acc.enable_user()
elif state == 'disabled':
user = acs_acc.disable_user()
elif state == 'locked':
user = acs_acc.lock_user()
else:
user = acs_acc.present_user()
result = acs_acc.get_result(user)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,388 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2018, David Passante <@dpassante>
# 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: cs_vlan_ip_range
short_description: Manages VLAN IP ranges on Apache CloudStack based clouds.
description:
- Create and delete VLAN IP range.
author: David Passante (@dpassante)
options:
network:
description:
- The network name or id.
- Required if I(for_virtual_network) and I(physical_network) are not set.
type: str
physical_network:
description:
- The physical network name or id.
type: str
start_ip:
description:
- The beginning IPv4 address in the VLAN IP range.
- Only considered on create.
type: str
required: true
end_ip:
description:
- The ending IPv4 address in the VLAN IP range.
- If not specified, value of I(start_ip) is used.
- Only considered on create.
type: str
gateway:
description:
- The gateway of the VLAN IP range.
- Required if I(state=present).
type: str
netmask:
description:
- The netmask of the VLAN IP range.
- Required if I(state=present).
type: str
start_ipv6:
description:
- The beginning IPv6 address in the IPv6 network range.
- Only considered on create.
type: str
end_ipv6:
description:
- The ending IPv6 address in the IPv6 network range.
- If not specified, value of I(start_ipv6) is used.
- Only considered on create.
type: str
gateway_ipv6:
description:
- The gateway of the IPv6 network.
- Only considered on create.
type: str
cidr_ipv6:
description:
- The CIDR of IPv6 network, must be at least /64.
type: str
vlan:
description:
- The ID or VID of the network.
- If not specified, will be defaulted to the vlan of the network.
type: str
state:
description:
- State of the network ip range.
type: str
default: present
choices: [ present, absent ]
zone:
description:
- The Zone ID of the VLAN IP range.
- If not set, default zone is used.
type: str
domain:
description:
- Domain of the account owning the VLAN.
type: str
account:
description:
- Account who owns the VLAN.
- Mutually exclusive with I(project).
type: str
project:
description:
- Project who owns the VLAN.
- Mutually exclusive with I(account).
type: str
for_virtual_network:
description:
- C(yes) if VLAN is of Virtual type, C(no) if Direct.
- If set to C(yes) but neither I(physical_network) or I(network) is set CloudStack will try to add the
VLAN range to the Physical Network with a Public traffic type.
type: bool
default: no
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create a VLAN IP range for network test
cs_vlan_ip_range:
network: test
vlan: 98
start_ip: 10.2.4.10
end_ip: 10.2.4.100
gateway: 10.2.4.1
netmask: 255.255.255.0
zone: zone-02
delegate_to: localhost
- name: remove a VLAN IP range for network test
cs_vlan_ip_range:
state: absent
network: test
start_ip: 10.2.4.10
end_ip: 10.2.4.100
zone: zone-02
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the VLAN IP range.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
network:
description: The network of vlan range
returned: if available
type: str
sample: test
vlan:
description: The ID or VID of the VLAN.
returned: success
type: str
sample: vlan://98
gateway:
description: IPv4 gateway.
returned: success
type: str
sample: 10.2.4.1
netmask:
description: IPv4 netmask.
returned: success
type: str
sample: 255.255.255.0
gateway_ipv6:
description: IPv6 gateway.
returned: if available
type: str
sample: 2001:db8::1
cidr_ipv6:
description: The CIDR of IPv6 network.
returned: if available
type: str
sample: 2001:db8::/64
zone:
description: Name of zone.
returned: success
type: str
sample: zone-02
domain:
description: Domain name of the VLAN IP range.
returned: success
type: str
sample: ROOT
account:
description: Account who owns the network.
returned: if available
type: str
sample: example account
project:
description: Project who owns the network.
returned: if available
type: str
sample: example project
for_systemvms:
description: Whether VLAN IP range is dedicated to system vms or not.
returned: success
type: bool
sample: false
for_virtual_network:
description: Whether VLAN IP range is of Virtual type or not.
returned: success
type: bool
sample: false
physical_network:
description: The physical network VLAN IP range belongs to.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
start_ip:
description: The start ip of the VLAN IP range.
returned: success
type: str
sample: 10.2.4.10
end_ip:
description: The end ip of the VLAN IP range.
returned: success
type: str
sample: 10.2.4.100
start_ipv6:
description: The start ipv6 of the VLAN IP range.
returned: if available
type: str
sample: 2001:db8::10
end_ipv6:
description: The end ipv6 of the VLAN IP range.
returned: if available
type: str
sample: 2001:db8::50
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackVlanIpRange(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVlanIpRange, self).__init__(module)
self.returns = {
'startip': 'start_ip',
'endip': 'end_ip',
'physicalnetworkid': 'physical_network',
'vlan': 'vlan',
'forsystemvms': 'for_systemvms',
'forvirtualnetwork': 'for_virtual_network',
'gateway': 'gateway',
'netmask': 'netmask',
'ip6gateway': 'gateway_ipv6',
'ip6cidr': 'cidr_ipv6',
'startipv6': 'start_ipv6',
'endipv6': 'end_ipv6',
}
self.ip_range = None
def get_vlan_ip_range(self):
if not self.ip_range:
args = {
'zoneid': self.get_zone(key='id'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'networkid': self.get_network(key='id'),
}
res = self.query_api('listVlanIpRanges', **args)
if res:
ip_range_list = res['vlaniprange']
params = {
'startip': self.module.params.get('start_ip'),
'endip': self.get_or_fallback('end_ip', 'start_ip'),
}
for ipr in ip_range_list:
if params['startip'] == ipr['startip'] and params['endip'] == ipr['endip']:
self.ip_range = ipr
break
return self.ip_range
def present_vlan_ip_range(self):
ip_range = self.get_vlan_ip_range()
if not ip_range:
ip_range = self.create_vlan_ip_range()
return ip_range
def create_vlan_ip_range(self):
self.result['changed'] = True
vlan = self.module.params.get('vlan')
args = {
'zoneid': self.get_zone(key='id'),
'projectid': self.get_project(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'startip': self.module.params.get('start_ip'),
'endip': self.get_or_fallback('end_ip', 'start_ip'),
'netmask': self.module.params.get('netmask'),
'gateway': self.module.params.get('gateway'),
'startipv6': self.module.params.get('start_ipv6'),
'endipv6': self.get_or_fallback('end_ipv6', 'start_ipv6'),
'ip6gateway': self.module.params.get('gateway_ipv6'),
'ip6cidr': self.module.params.get('cidr_ipv6'),
'vlan': self.get_network(key='vlan') if not vlan else vlan,
'networkid': self.get_network(key='id'),
'forvirtualnetwork': self.module.params.get('for_virtual_network'),
}
if self.module.params.get('physical_network'):
args['physicalnetworkid'] = self.get_physical_network(key='id')
if not self.module.check_mode:
res = self.query_api('createVlanIpRange', **args)
self.ip_range = res['vlan']
return self.ip_range
def absent_vlan_ip_range(self):
ip_range = self.get_vlan_ip_range()
if ip_range:
self.result['changed'] = True
args = {
'id': ip_range['id'],
}
if not self.module.check_mode:
self.query_api('deleteVlanIpRange', **args)
return ip_range
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
network=dict(type='str'),
physical_network=dict(type='str'),
zone=dict(type='str'),
start_ip=dict(type='str', required=True),
end_ip=dict(type='str'),
gateway=dict(type='str'),
netmask=dict(type='str'),
start_ipv6=dict(type='str'),
end_ipv6=dict(type='str'),
gateway_ipv6=dict(type='str'),
cidr_ipv6=dict(type='str'),
vlan=dict(type='str'),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(type='str'),
account=dict(type='str'),
project=dict(type='str'),
for_virtual_network=dict(type='bool', default=False),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
mutually_exclusive=(
['account', 'project'],
),
required_if=(("state", "present", ("gateway", "netmask")),),
supports_check_mode=True,
)
acs_vlan_ip_range = AnsibleCloudStackVlanIpRange(module)
state = module.params.get('state')
if state == 'absent':
ipr = acs_vlan_ip_range.absent_vlan_ip_range()
else:
ipr = acs_vlan_ip_range.present_vlan_ip_range()
result = acs_vlan_ip_range.get_result(ipr)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,284 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_vmsnapshot
short_description: Manages VM snapshots on Apache CloudStack based clouds.
description:
- Create, remove and revert VM from snapshots.
author: René Moser (@resmo)
options:
name:
description:
- Unique Name of the snapshot. In CloudStack terms display name.
type: str
required: true
aliases: [ display_name ]
vm:
description:
- Name of the virtual machine.
type: str
required: true
description:
description:
- Description of the snapshot.
type: str
snapshot_memory:
description:
- Snapshot memory if set to true.
default: no
type: bool
zone:
description:
- Name of the zone in which the VM is in. If not set, default zone is used.
type: str
project:
description:
- Name of the project the VM is assigned to.
type: str
state:
description:
- State of the snapshot.
type: str
default: present
choices: [ present, absent, revert ]
domain:
description:
- Domain the VM snapshot is related to.
type: str
account:
description:
- Account the VM snapshot is related to.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a VM snapshot of disk and memory before an upgrade
cs_vmsnapshot:
name: Snapshot before upgrade
vm: web-01
snapshot_memory: yes
delegate_to: localhost
- name: Revert a VM to a snapshot after a failed upgrade
cs_vmsnapshot:
name: Snapshot before upgrade
vm: web-01
state: revert
delegate_to: localhost
- name: Remove a VM snapshot after successful upgrade
cs_vmsnapshot:
name: Snapshot before upgrade
vm: web-01
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the snapshot.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: Name of the snapshot.
returned: success
type: str
sample: snapshot before update
display_name:
description: Display name of the snapshot.
returned: success
type: str
sample: snapshot before update
created:
description: date of the snapshot.
returned: success
type: str
sample: 2015-03-29T14:57:06+0200
current:
description: true if the snapshot is current
returned: success
type: bool
sample: True
state:
description: state of the vm snapshot
returned: success
type: str
sample: Allocated
type:
description: type of vm snapshot
returned: success
type: str
sample: DiskAndMemory
description:
description: description of vm snapshot
returned: success
type: str
sample: snapshot brought to you by Ansible
domain:
description: Domain the vm snapshot is related to.
returned: success
type: str
sample: example domain
account:
description: Account the vm snapshot is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the vm snapshot is related to.
returned: success
type: str
sample: Production
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackVmSnapshot(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVmSnapshot, self).__init__(module)
self.returns = {
'type': 'type',
'current': 'current',
}
def get_snapshot(self):
args = {
'virtualmachineid': self.get_vm('id'),
'account': self.get_account('name'),
'domainid': self.get_domain('id'),
'projectid': self.get_project('id'),
'name': self.module.params.get('name'),
}
snapshots = self.query_api('listVMSnapshot', **args)
if snapshots:
return snapshots['vmSnapshot'][0]
return None
def create_snapshot(self):
snapshot = self.get_snapshot()
if not snapshot:
self.result['changed'] = True
args = {
'virtualmachineid': self.get_vm('id'),
'name': self.module.params.get('name'),
'description': self.module.params.get('description'),
'snapshotmemory': self.module.params.get('snapshot_memory'),
}
if not self.module.check_mode:
res = self.query_api('createVMSnapshot', **args)
poll_async = self.module.params.get('poll_async')
if res and poll_async:
snapshot = self.poll_job(res, 'vmsnapshot')
if snapshot:
snapshot = self.ensure_tags(resource=snapshot, resource_type='Snapshot')
return snapshot
def remove_snapshot(self):
snapshot = self.get_snapshot()
if snapshot:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('deleteVMSnapshot', vmsnapshotid=snapshot['id'])
poll_async = self.module.params.get('poll_async')
if res and poll_async:
res = self.poll_job(res, 'vmsnapshot')
return snapshot
def revert_vm_to_snapshot(self):
snapshot = self.get_snapshot()
if snapshot:
self.result['changed'] = True
if snapshot['state'] != "Ready":
self.module.fail_json(msg="snapshot state is '%s', not ready, could not revert VM" % snapshot['state'])
if not self.module.check_mode:
res = self.query_api('revertToVMSnapshot', vmsnapshotid=snapshot['id'])
poll_async = self.module.params.get('poll_async')
if res and poll_async:
res = self.poll_job(res, 'vmsnapshot')
return snapshot
self.module.fail_json(msg="snapshot not found, could not revert VM")
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True, aliases=['display_name']),
vm=dict(required=True),
description=dict(),
zone=dict(),
snapshot_memory=dict(type='bool', default=False),
state=dict(choices=['present', 'absent', 'revert'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_vmsnapshot = AnsibleCloudStackVmSnapshot(module)
state = module.params.get('state')
if state in ['revert']:
snapshot = acs_vmsnapshot.revert_vm_to_snapshot()
elif state in ['absent']:
snapshot = acs_vmsnapshot.remove_snapshot()
else:
snapshot = acs_vmsnapshot.create_snapshot()
result = acs_vmsnapshot.get_result(snapshot)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,573 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, Jefferson Girão <jefferson@girao.net>
# (c) 2015, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_volume
short_description: Manages volumes on Apache CloudStack based clouds.
description:
- Create, destroy, attach, detach, extract or upload volumes.
author:
- Jefferson Girão (@jeffersongirao)
- René Moser (@resmo)
options:
name:
description:
- Name of the volume.
- I(name) can only contain ASCII letters.
type: str
required: true
account:
description:
- Account the volume is related to.
type: str
device_id:
description:
- ID of the device on a VM the volume is attached to.
- Only considered if I(state) is C(attached).
type: int
custom_id:
description:
- Custom id to the resource.
- Allowed to Root Admins only.
type: str
disk_offering:
description:
- Name of the disk offering to be used.
- Required one of I(disk_offering), I(snapshot) if volume is not already I(state=present).
type: str
display_volume:
description:
- Whether to display the volume to the end user or not.
- Allowed to Root Admins only.
type: bool
domain:
description:
- Name of the domain the volume to be deployed in.
type: str
max_iops:
description:
- Max iops
type: int
min_iops:
description:
- Min iops
type: int
project:
description:
- Name of the project the volume to be deployed in.
type: str
size:
description:
- Size of disk in GB
type: int
snapshot:
description:
- The snapshot name for the disk volume.
- Required one of I(disk_offering), I(snapshot) if volume is not already I(state=present).
type: str
force:
description:
- Force removal of volume even it is attached to a VM.
- Considered on I(state=absent) only.
default: no
type: bool
shrink_ok:
description:
- Whether to allow to shrink the volume.
default: no
type: bool
vm:
description:
- Name of the virtual machine to attach the volume to.
type: str
zone:
description:
- Name of the zone in which the volume should be deployed.
- If not set, default zone is used.
type: str
state:
description:
- State of the volume.
- The choices C(extracted) and C(uploaded) were added in version 2.8.
type: str
default: present
choices: [ present, absent, attached, detached, extracted, uploaded ]
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "To delete all tags, set a empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
url:
description:
- URL to which the volume would be extracted on I(state=extracted)
- or the URL where to download the volume on I(state=uploaded).
- Only considered if I(state) is C(extracted) or C(uploaded).
type: str
mode:
description:
- Mode for the volume extraction.
- Only considered if I(state=extracted).
type: str
choices: [ http_download, ftp_upload ]
default: http_download
format:
description:
- The format for the volume.
- Only considered if I(state=uploaded).
type: str
choices: [ QCOW2, RAW, VHD, VHDX, OVA ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: create volume within project and zone with specified storage options
cs_volume:
name: web-vm-1-volume
project: Integration
zone: ch-zrh-ix-01
disk_offering: PerfPlus Storage
size: 20
delegate_to: localhost
- name: create/attach volume to instance
cs_volume:
name: web-vm-1-volume
disk_offering: PerfPlus Storage
size: 20
vm: web-vm-1
state: attached
delegate_to: localhost
- name: detach volume
cs_volume:
name: web-vm-1-volume
state: detached
delegate_to: localhost
- name: remove volume
cs_volume:
name: web-vm-1-volume
state: absent
delegate_to: localhost
# New in version 2.8
- name: Extract DATA volume to make it downloadable
cs_volume:
state: extracted
name: web-vm-1-volume
register: data_vol_out
delegate_to: localhost
- name: Create new volume by downloading source volume
cs_volume:
state: uploaded
name: web-vm-1-volume-2
format: VHD
url: "{{ data_vol_out.url }}"
delegate_to: localhost
'''
RETURN = '''
id:
description: ID of the volume.
returned: success
type: str
sample:
name:
description: Name of the volume.
returned: success
type: str
sample: web-volume-01
display_name:
description: Display name of the volume.
returned: success
type: str
sample: web-volume-01
group:
description: Group the volume belongs to
returned: success
type: str
sample: web
domain:
description: Domain the volume belongs to
returned: success
type: str
sample: example domain
project:
description: Project the volume belongs to
returned: success
type: str
sample: Production
zone:
description: Name of zone the volume is in.
returned: success
type: str
sample: ch-gva-2
created:
description: Date of the volume was created.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
attached:
description: Date of the volume was attached.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
type:
description: Disk volume type.
returned: success
type: str
sample: DATADISK
size:
description: Size of disk volume.
returned: success
type: int
sample: 20
vm:
description: Name of the vm the volume is attached to (not returned when detached)
returned: success
type: str
sample: web-01
state:
description: State of the volume
returned: success
type: str
sample: Attached
device_id:
description: Id of the device on user vm the volume is attached to (not returned when detached)
returned: success
type: int
sample: 1
url:
description: The url of the uploaded volume or the download url depending extraction mode.
returned: success when I(state=extracted)
type: str
sample: http://1.12.3.4/userdata/387e2c7c-7c42-4ecc-b4ed-84e8367a1965.vhd
version_added: '2.8'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_required_together,
cs_argument_spec
)
class AnsibleCloudStackVolume(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVolume, self).__init__(module)
self.returns = {
'group': 'group',
'attached': 'attached',
'vmname': 'vm',
'deviceid': 'device_id',
'type': 'type',
'size': 'size',
'url': 'url',
}
self.volume = None
def get_volume(self):
if not self.volume:
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'zoneid': self.get_zone(key='id'),
'displayvolume': self.module.params.get('display_volume'),
'type': 'DATADISK',
'fetch_list': True,
}
# Do not filter on DATADISK when state=extracted
if self.module.params.get('state') == 'extracted':
del args['type']
volumes = self.query_api('listVolumes', **args)
if volumes:
volume_name = self.module.params.get('name')
for v in volumes:
if volume_name.lower() == v['name'].lower():
self.volume = v
break
return self.volume
def get_snapshot(self, key=None):
snapshot = self.module.params.get('snapshot')
if not snapshot:
return None
args = {
'name': snapshot,
'account': self.get_account('name'),
'domainid': self.get_domain('id'),
'projectid': self.get_project('id'),
}
snapshots = self.query_api('listSnapshots', **args)
if snapshots:
return self._get_by_key(key, snapshots['snapshot'][0])
self.module.fail_json(msg="Snapshot with name %s not found" % snapshot)
def present_volume(self):
volume = self.get_volume()
if volume:
volume = self.update_volume(volume)
else:
disk_offering_id = self.get_disk_offering(key='id')
snapshot_id = self.get_snapshot(key='id')
if not disk_offering_id and not snapshot_id:
self.module.fail_json(msg="Required one of: disk_offering,snapshot")
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'diskofferingid': disk_offering_id,
'displayvolume': self.module.params.get('display_volume'),
'maxiops': self.module.params.get('max_iops'),
'miniops': self.module.params.get('min_iops'),
'projectid': self.get_project(key='id'),
'size': self.module.params.get('size'),
'snapshotid': snapshot_id,
'zoneid': self.get_zone(key='id')
}
if not self.module.check_mode:
res = self.query_api('createVolume', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
volume = self.poll_job(res, 'volume')
if volume:
volume = self.ensure_tags(resource=volume, resource_type='Volume')
self.volume = volume
return volume
def attached_volume(self):
volume = self.present_volume()
if volume:
if volume.get('virtualmachineid') != self.get_vm(key='id'):
self.result['changed'] = True
if not self.module.check_mode:
volume = self.detached_volume()
if 'attached' not in volume:
self.result['changed'] = True
args = {
'id': volume['id'],
'virtualmachineid': self.get_vm(key='id'),
'deviceid': self.module.params.get('device_id'),
}
if not self.module.check_mode:
res = self.query_api('attachVolume', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
volume = self.poll_job(res, 'volume')
return volume
def detached_volume(self):
volume = self.present_volume()
if volume:
if 'attached' not in volume:
return volume
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('detachVolume', id=volume['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
volume = self.poll_job(res, 'volume')
return volume
def absent_volume(self):
volume = self.get_volume()
if volume:
if 'attached' in volume and not self.module.params.get('force'):
self.module.fail_json(msg="Volume '%s' is attached, use force=true for detaching and removing the volume." % volume.get('name'))
self.result['changed'] = True
if not self.module.check_mode:
volume = self.detached_volume()
res = self.query_api('deleteVolume', id=volume['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'volume')
return volume
def update_volume(self, volume):
args_resize = {
'id': volume['id'],
'diskofferingid': self.get_disk_offering(key='id'),
'maxiops': self.module.params.get('max_iops'),
'miniops': self.module.params.get('min_iops'),
'size': self.module.params.get('size')
}
# change unit from bytes to giga bytes to compare with args
volume_copy = volume.copy()
volume_copy['size'] = volume_copy['size'] / (2**30)
if self.has_changed(args_resize, volume_copy):
self.result['changed'] = True
if not self.module.check_mode:
args_resize['shrinkok'] = self.module.params.get('shrink_ok')
res = self.query_api('resizeVolume', **args_resize)
poll_async = self.module.params.get('poll_async')
if poll_async:
volume = self.poll_job(res, 'volume')
self.volume = volume
return volume
def extract_volume(self):
volume = self.get_volume()
if not volume:
self.module.fail_json(msg="Failed: volume not found")
args = {
'id': volume['id'],
'url': self.module.params.get('url'),
'mode': self.module.params.get('mode').upper(),
'zoneid': self.get_zone(key='id')
}
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('extractVolume', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
volume = self.poll_job(res, 'volume')
self.volume = volume
return volume
def upload_volume(self):
volume = self.get_volume()
if not volume:
disk_offering_id = self.get_disk_offering(key='id')
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'zoneid': self.get_zone(key='id'),
'format': self.module.params.get('format'),
'url': self.module.params.get('url'),
'diskofferingid': disk_offering_id,
}
if not self.module.check_mode:
res = self.query_api('uploadVolume', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
volume = self.poll_job(res, 'volume')
if volume:
volume = self.ensure_tags(resource=volume, resource_type='Volume')
self.volume = volume
return volume
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
disk_offering=dict(),
display_volume=dict(type='bool'),
max_iops=dict(type='int'),
min_iops=dict(type='int'),
size=dict(type='int'),
snapshot=dict(),
vm=dict(),
device_id=dict(type='int'),
custom_id=dict(),
force=dict(type='bool', default=False),
shrink_ok=dict(type='bool', default=False),
state=dict(default='present', choices=[
'present',
'absent',
'attached',
'detached',
'extracted',
'uploaded',
]),
zone=dict(),
domain=dict(),
account=dict(),
project=dict(),
poll_async=dict(type='bool', default=True),
tags=dict(type='list', aliases=['tag']),
url=dict(),
mode=dict(choices=['http_download', 'ftp_upload'], default='http_download'),
format=dict(choices=['QCOW2', 'RAW', 'VHD', 'VHDX', 'OVA']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
mutually_exclusive=(
['snapshot', 'disk_offering'],
),
required_if=[
('state', 'uploaded', ['url', 'format']),
],
supports_check_mode=True
)
acs_vol = AnsibleCloudStackVolume(module)
state = module.params.get('state')
if state in ['absent']:
volume = acs_vol.absent_volume()
elif state in ['attached']:
volume = acs_vol.attached_volume()
elif state in ['detached']:
volume = acs_vol.detached_volume()
elif state == 'extracted':
volume = acs_vol.extract_volume()
elif state == 'uploaded':
volume = acs_vol.upload_volume()
else:
volume = acs_vol.present_volume()
result = acs_vol.get_result(volume)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,398 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_vpc
short_description: "Manages VPCs on Apache CloudStack based clouds."
description:
- Create, update and delete VPCs.
author: René Moser (@resmo)
options:
name:
description:
- Name of the VPC.
type: str
required: true
display_text:
description:
- Display text of the VPC.
- If not set, I(name) will be used for creating.
type: str
cidr:
description:
- CIDR of the VPC, e.g. 10.1.0.0/16
- All VPC guest networks' CIDRs must be within this CIDR.
- Required on I(state=present).
type: str
network_domain:
description:
- Network domain for the VPC.
- All networks inside the VPC will belong to this domain.
- Only considered while creating the VPC, can not be changed.
type: str
vpc_offering:
description:
- Name of the VPC offering.
- If not set, default VPC offering is used.
type: str
clean_up:
description:
- Whether to redeploy a VPC router or not when I(state=restarted)
type: bool
state:
description:
- State of the VPC.
- The state C(present) creates a started VPC.
- The state C(stopped) is only considered while creating the VPC, added in version 2.6.
type: str
default: present
choices:
- present
- absent
- stopped
- restarted
domain:
description:
- Domain the VPC is related to.
type: str
account:
description:
- Account the VPC is related to.
type: str
project:
description:
- Name of the project the VPC is related to.
type: str
zone:
description:
- Name of the zone.
- If not set, default zone is used.
type: str
tags:
description:
- List of tags. Tags are a list of dictionaries having keys I(key) and I(value).
- "For deleting all tags, set an empty list e.g. I(tags: [])."
type: list
aliases: [ tag ]
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a VPC is present but not started after creating
cs_vpc:
name: my_vpc
display_text: My example VPC
cidr: 10.10.0.0/16
state: stopped
delegate_to: localhost
- name: Ensure a VPC is present and started after creating
cs_vpc:
name: my_vpc
display_text: My example VPC
cidr: 10.10.0.0/16
delegate_to: localhost
- name: Ensure a VPC is absent
cs_vpc:
name: my_vpc
state: absent
delegate_to: localhost
- name: Ensure a VPC is restarted with clean up
cs_vpc:
name: my_vpc
clean_up: yes
state: restarted
delegate_to: localhost
'''
RETURN = '''
---
id:
description: "UUID of the VPC."
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: "Name of the VPC."
returned: success
type: str
sample: my_vpc
display_text:
description: "Display text of the VPC."
returned: success
type: str
sample: My example VPC
cidr:
description: "CIDR of the VPC."
returned: success
type: str
sample: 10.10.0.0/16
network_domain:
description: "Network domain of the VPC."
returned: success
type: str
sample: example.com
region_level_vpc:
description: "Whether the VPC is region level or not."
returned: success
type: bool
sample: true
restart_required:
description: "Whether the VPC router needs a restart or not."
returned: success
type: bool
sample: true
distributed_vpc_router:
description: "Whether the VPC uses distributed router or not."
returned: success
type: bool
sample: true
redundant_vpc_router:
description: "Whether the VPC has redundant routers or not."
returned: success
type: bool
sample: true
domain:
description: "Domain the VPC is related to."
returned: success
type: str
sample: example domain
account:
description: "Account the VPC is related to."
returned: success
type: str
sample: example account
project:
description: "Name of project the VPC is related to."
returned: success
type: str
sample: Production
zone:
description: "Name of zone the VPC is in."
returned: success
type: str
sample: ch-gva-2
state:
description: "State of the VPC."
returned: success
type: str
sample: Enabled
tags:
description: "List of resource tags associated with the VPC."
returned: success
type: list
sample: '[ { "key": "foo", "value": "bar" } ]'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackVpc(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVpc, self).__init__(module)
self.returns = {
'cidr': 'cidr',
'networkdomain': 'network_domain',
'redundantvpcrouter': 'redundant_vpc_router',
'distributedvpcrouter': 'distributed_vpc_router',
'regionlevelvpc': 'region_level_vpc',
'restartrequired': 'restart_required',
}
self.vpc = None
def get_vpc_offering(self, key=None):
vpc_offering = self.module.params.get('vpc_offering')
args = {
'state': 'Enabled',
}
if vpc_offering:
args['name'] = vpc_offering
fail_msg = "VPC offering not found or not enabled: %s" % vpc_offering
else:
args['isdefault'] = True
fail_msg = "No enabled default VPC offering found"
vpc_offerings = self.query_api('listVPCOfferings', **args)
if vpc_offerings:
# The API name argument filter also matches substrings, we have to
# iterate over the results to get an exact match
for vo in vpc_offerings['vpcoffering']:
if 'name' in args:
if args['name'] == vo['name']:
return self._get_by_key(key, vo)
# Return the first offering found, if not queried for the name
else:
return self._get_by_key(key, vo)
self.module.fail_json(msg=fail_msg)
def get_vpc(self):
if self.vpc:
return self.vpc
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'zoneid': self.get_zone(key='id'),
'fetch_list': True,
}
vpcs = self.query_api('listVPCs', **args)
if vpcs:
vpc_name = self.module.params.get('name')
for v in vpcs:
if vpc_name in [v['name'], v['displaytext'], v['id']]:
# Fail if the identifier matches more than one VPC
if self.vpc:
self.module.fail_json(msg="More than one VPC found with the provided identifyer: %s" % vpc_name)
else:
self.vpc = v
return self.vpc
def restart_vpc(self):
self.result['changed'] = True
vpc = self.get_vpc()
if vpc and not self.module.check_mode:
args = {
'id': vpc['id'],
'cleanup': self.module.params.get('clean_up'),
}
res = self.query_api('restartVPC', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'vpc')
return vpc
def present_vpc(self):
vpc = self.get_vpc()
if not vpc:
vpc = self._create_vpc(vpc)
else:
vpc = self._update_vpc(vpc)
if vpc:
vpc = self.ensure_tags(resource=vpc, resource_type='Vpc')
return vpc
def _create_vpc(self, vpc):
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'displaytext': self.get_or_fallback('display_text', 'name'),
'networkdomain': self.module.params.get('network_domain'),
'vpcofferingid': self.get_vpc_offering(key='id'),
'cidr': self.module.params.get('cidr'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'zoneid': self.get_zone(key='id'),
'start': self.module.params.get('state') != 'stopped'
}
self.result['diff']['after'] = args
if not self.module.check_mode:
res = self.query_api('createVPC', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpc = self.poll_job(res, 'vpc')
return vpc
def _update_vpc(self, vpc):
args = {
'id': vpc['id'],
'displaytext': self.module.params.get('display_text'),
}
if self.has_changed(args, vpc):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateVPC', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpc = self.poll_job(res, 'vpc')
return vpc
def absent_vpc(self):
vpc = self.get_vpc()
if vpc:
self.result['changed'] = True
self.result['diff']['before'] = vpc
if not self.module.check_mode:
res = self.query_api('deleteVPC', id=vpc['id'])
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'vpc')
return vpc
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
cidr=dict(),
display_text=dict(),
vpc_offering=dict(),
network_domain=dict(),
clean_up=dict(type='bool'),
state=dict(choices=['present', 'absent', 'stopped', 'restarted'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
tags=dict(type='list', aliases=['tag']),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
required_if=[
('state', 'present', ['cidr']),
],
supports_check_mode=True,
)
acs_vpc = AnsibleCloudStackVpc(module)
state = module.params.get('state')
if state == 'absent':
vpc = acs_vpc.absent_vpc()
elif state == 'restarted':
vpc = acs_vpc.restart_vpc()
else:
vpc = acs_vpc.present_vpc()
result = acs_vpc.get_result(vpc)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,323 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, David Passante (@dpassante)
# 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: cs_vpc_offering
short_description: Manages vpc offerings on Apache CloudStack based clouds.
description:
- Create, update, enable, disable and remove CloudStack VPC offerings.
author: David Passante (@dpassante)
options:
name:
description:
- The name of the vpc offering
type: str
required: true
state:
description:
- State of the vpc offering.
type: str
choices: [ enabled, present, disabled, absent ]
default: present
display_text:
description:
- Display text of the vpc offerings
type: str
service_capabilities:
description:
- Desired service capabilities as part of vpc offering.
type: list
aliases: [ service_capability ]
service_offering:
description:
- The name or ID of the service offering for the VPC router appliance.
type: str
supported_services:
description:
- Services supported by the vpc offering
type: list
aliases: [ supported_service ]
service_providers:
description:
- provider to service mapping. If not specified, the provider for the service will be mapped to the default provider on the physical network
type: list
aliases: [ service_provider ]
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Create a vpc offering and enable it
cs_vpc_offering:
name: my_vpc_offering
display_text: vpc offering description
state: enabled
supported_services: [ Dns, Dhcp ]
service_providers:
- {service: 'dns', provider: 'VpcVirtualRouter'}
- {service: 'dhcp', provider: 'VpcVirtualRouter'}
delegate_to: localhost
- name: Create a vpc offering with redundant router
cs_vpc_offering:
name: my_vpc_offering
display_text: vpc offering description
supported_services: [ Dns, Dhcp, SourceNat ]
service_providers:
- {service: 'dns', provider: 'VpcVirtualRouter'}
- {service: 'dhcp', provider: 'VpcVirtualRouter'}
- {service: 'SourceNat', provider: 'VpcVirtualRouter'}
service_capabilities:
- {service: 'SourceNat', capabilitytype: 'RedundantRouter', capabilityvalue: true}
delegate_to: localhost
- name: Create a region level vpc offering with distributed router
cs_vpc_offering:
name: my_vpc_offering
display_text: vpc offering description
state: present
supported_services: [ Dns, Dhcp, SourceNat ]
service_providers:
- {service: 'dns', provider: 'VpcVirtualRouter'}
- {service: 'dhcp', provider: 'VpcVirtualRouter'}
- {service: 'SourceNat', provider: 'VpcVirtualRouter'}
service_capabilities:
- {service: 'Connectivity', capabilitytype: 'DistributedRouter', capabilityvalue: true}
- {service: 'Connectivity', capabilitytype: 'RegionLevelVPC', capabilityvalue: true}
delegate_to: localhost
- name: Remove a vpc offering
cs_vpc_offering:
name: my_vpc_offering
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the vpc offering.
returned: success
type: str
sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
name:
description: The name of the vpc offering
returned: success
type: str
sample: MyCustomVPCOffering
display_text:
description: The display text of the vpc offering
returned: success
type: str
sample: My vpc offering
state:
description: The state of the vpc offering
returned: success
type: str
sample: Enabled
service_offering_id:
description: The service offering ID.
returned: success
type: str
sample: c5f7a5fc-43f8-11e5-a151-feff819cdc9f
is_default:
description: Whether VPC offering is the default offering or not.
returned: success
type: bool
sample: false
region_level:
description: Indicated if the offering can support region level vpc.
returned: success
type: bool
sample: false
distributed:
description: Indicates if the vpc offering supports distributed router for one-hop forwarding.
returned: success
type: bool
sample: false
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackVPCOffering(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVPCOffering, self).__init__(module)
self.returns = {
'serviceofferingid': 'service_offering_id',
'isdefault': 'is_default',
'distributedvpcrouter': 'distributed',
'supportsregionLevelvpc': 'region_level',
}
self.vpc_offering = None
def get_vpc_offering(self):
if self.vpc_offering:
return self.vpc_offering
args = {
'name': self.module.params.get('name'),
}
vo = self.query_api('listVPCOfferings', **args)
if vo:
for vpc_offer in vo['vpcoffering']:
if args['name'] == vpc_offer['name']:
self.vpc_offering = vpc_offer
return self.vpc_offering
def get_service_offering_id(self):
service_offering = self.module.params.get('service_offering')
if not service_offering:
return None
args = {
'issystem': True
}
service_offerings = self.query_api('listServiceOfferings', **args)
if service_offerings:
for s in service_offerings['serviceoffering']:
if service_offering in [s['name'], s['id']]:
return s['id']
self.fail_json(msg="Service offering '%s' not found" % service_offering)
def create_or_update(self):
vpc_offering = self.get_vpc_offering()
if not vpc_offering:
vpc_offering = self.create_vpc_offering()
return self.update_vpc_offering(vpc_offering)
def create_vpc_offering(self):
vpc_offering = None
self.result['changed'] = True
args = {
'name': self.module.params.get('name'),
'state': self.module.params.get('state'),
'displaytext': self.module.params.get('display_text'),
'supportedservices': self.module.params.get('supported_services'),
'serviceproviderlist': self.module.params.get('service_providers'),
'serviceofferingid': self.get_service_offering_id(),
'servicecapabilitylist': self.module.params.get('service_capabilities'),
}
required_params = [
'display_text',
'supported_services',
]
self.module.fail_on_missing_params(required_params=required_params)
if not self.module.check_mode:
res = self.query_api('createVPCOffering', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpc_offering = self.poll_job(res, 'vpcoffering')
return vpc_offering
def delete_vpc_offering(self):
vpc_offering = self.get_vpc_offering()
if vpc_offering:
self.result['changed'] = True
args = {
'id': vpc_offering['id'],
}
if not self.module.check_mode:
res = self.query_api('deleteVPCOffering', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpc_offering = self.poll_job(res, 'vpcoffering')
return vpc_offering
def update_vpc_offering(self, vpc_offering):
if not vpc_offering:
return vpc_offering
args = {
'id': vpc_offering['id'],
'state': self.module.params.get('state'),
'name': self.module.params.get('name'),
'displaytext': self.module.params.get('display_text'),
}
if args['state'] in ['enabled', 'disabled']:
args['state'] = args['state'].title()
else:
del args['state']
if self.has_changed(args, vpc_offering):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateVPCOffering', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpc_offering = self.poll_job(res, 'vpcoffering')
return vpc_offering
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
display_text=dict(),
state=dict(choices=['enabled', 'present', 'disabled', 'absent'], default='present'),
service_capabilities=dict(type='list', aliases=['service_capability']),
service_offering=dict(),
supported_services=dict(type='list', aliases=['supported_service']),
service_providers=dict(type='list', aliases=['service_provider']),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_vpc_offering = AnsibleCloudStackVPCOffering(module)
state = module.params.get('state')
if state in ['absent']:
vpc_offering = acs_vpc_offering.delete_vpc_offering()
else:
vpc_offering = acs_vpc_offering.create_or_update()
result = acs_vpc_offering.get_result(vpc_offering)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,355 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017, René Moser <mail@renemoser.net>
# 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 = r'''
---
module: cs_vpn_connection
short_description: Manages site-to-site VPN connections on Apache CloudStack based clouds.
description:
- Create and remove VPN connections.
author: René Moser (@resmo)
options:
vpc:
description:
- Name of the VPC the VPN connection is related to.
type: str
required: true
vpn_customer_gateway:
description:
- Name of the VPN customer gateway.
type: str
required: true
passive:
description:
- State of the VPN connection.
- Only considered when I(state=present).
default: no
type: bool
force:
description:
- Activate the VPN gateway if not already activated on I(state=present).
- Also see M(cs_vpn_gateway).
default: no
type: bool
state:
description:
- State of the VPN connection.
type: str
default: present
choices: [ present, absent ]
zone:
description:
- Name of the zone the VPC is related to.
- If not set, default zone is used.
type: str
domain:
description:
- Domain the VPN connection is related to.
type: str
account:
description:
- Account the VPN connection is related to.
type: str
project:
description:
- Name of the project the VPN connection is related to.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = r'''
- name: Create a VPN connection with activated VPN gateway
cs_vpn_connection:
vpn_customer_gateway: my vpn connection
vpc: my vpc
delegate_to: localhost
- name: Create a VPN connection and force VPN gateway activation
cs_vpn_connection:
vpn_customer_gateway: my vpn connection
vpc: my vpc
force: yes
delegate_to: localhost
- name: Remove a vpn connection
cs_vpn_connection:
vpn_customer_gateway: my vpn connection
vpc: my vpc
state: absent
delegate_to: localhost
'''
RETURN = r'''
---
id:
description: UUID of the VPN connection.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
vpn_gateway_id:
description: UUID of the VPN gateway.
returned: success
type: str
sample: 04589590-ac63-93f5-4ffc-b698b8ac38b6
domain:
description: Domain the VPN connection is related to.
returned: success
type: str
sample: example domain
account:
description: Account the VPN connection is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the VPN connection is related to.
returned: success
type: str
sample: Production
created:
description: Date the connection was created.
returned: success
type: str
sample: 2014-12-01T14:57:57+0100
dpd:
description: Whether dead pear detection is enabled or not.
returned: success
type: bool
sample: true
esp_lifetime:
description: Lifetime in seconds of phase 2 VPN connection.
returned: success
type: int
sample: 86400
esp_policy:
description: IKE policy of the VPN connection.
returned: success
type: str
sample: aes256-sha1;modp1536
force_encap:
description: Whether encapsulation for NAT traversal is enforced or not.
returned: success
type: bool
sample: true
ike_lifetime:
description: Lifetime in seconds of phase 1 VPN connection.
returned: success
type: int
sample: 86400
ike_policy:
description: ESP policy of the VPN connection.
returned: success
type: str
sample: aes256-sha1;modp1536
cidrs:
description: List of CIDRs of the customer gateway.
returned: success
type: list
sample: [ 10.10.10.0/24 ]
passive:
description: Whether the connection is passive or not.
returned: success
type: bool
sample: false
public_ip:
description: IP address of the VPN gateway.
returned: success
type: str
sample: 10.100.212.10
gateway:
description: IP address of the VPN customer gateway.
returned: success
type: str
sample: 10.101.214.10
state:
description: State of the VPN connection.
returned: success
type: str
sample: Connected
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackVpnConnection(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVpnConnection, self).__init__(module)
self.returns = {
'dpd': 'dpd',
'esplifetime': 'esp_lifetime',
'esppolicy': 'esp_policy',
'gateway': 'gateway',
'ikepolicy': 'ike_policy',
'ikelifetime': 'ike_lifetime',
'publicip': 'public_ip',
'passive': 'passive',
's2svpngatewayid': 'vpn_gateway_id',
}
self.vpn_customer_gateway = None
def get_vpn_customer_gateway(self, key=None, identifier=None, refresh=False):
if not refresh and self.vpn_customer_gateway:
return self._get_by_key(key, self.vpn_customer_gateway)
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'fetch_list': True,
}
vpn_customer_gateway = identifier or self.module.params.get('vpn_customer_gateway')
vcgws = self.query_api('listVpnCustomerGateways', **args)
if vcgws:
for vcgw in vcgws:
if vpn_customer_gateway.lower() in [vcgw['id'], vcgw['name'].lower()]:
self.vpn_customer_gateway = vcgw
return self._get_by_key(key, self.vpn_customer_gateway)
self.fail_json(msg="VPN customer gateway not found: %s" % vpn_customer_gateway)
def get_vpn_gateway(self, key=None):
args = {
'vpcid': self.get_vpc(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
}
vpn_gateways = self.query_api('listVpnGateways', **args)
if vpn_gateways:
return self._get_by_key(key, vpn_gateways['vpngateway'][0])
elif self.module.params.get('force'):
if self.module.check_mode:
return {}
res = self.query_api('createVpnGateway', **args)
vpn_gateway = self.poll_job(res, 'vpngateway')
return self._get_by_key(key, vpn_gateway)
self.fail_json(msg="VPN gateway not found and not forced to create one")
def get_vpn_connection(self):
args = {
'vpcid': self.get_vpc(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
}
vpn_conns = self.query_api('listVpnConnections', **args)
if vpn_conns:
for vpn_conn in vpn_conns['vpnconnection']:
if self.get_vpn_customer_gateway(key='id') == vpn_conn['s2scustomergatewayid']:
return vpn_conn
def present_vpn_connection(self):
vpn_conn = self.get_vpn_connection()
args = {
's2scustomergatewayid': self.get_vpn_customer_gateway(key='id'),
's2svpngatewayid': self.get_vpn_gateway(key='id'),
'passive': self.module.params.get('passive'),
}
if not vpn_conn:
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('createVpnConnection', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpn_conn = self.poll_job(res, 'vpnconnection')
return vpn_conn
def absent_vpn_connection(self):
vpn_conn = self.get_vpn_connection()
if vpn_conn:
self.result['changed'] = True
args = {
'id': vpn_conn['id']
}
if not self.module.check_mode:
res = self.query_api('deleteVpnConnection', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'vpnconnection')
return vpn_conn
def get_result(self, vpn_conn):
super(AnsibleCloudStackVpnConnection, self).get_result(vpn_conn)
if vpn_conn:
if 'cidrlist' in vpn_conn:
self.result['cidrs'] = vpn_conn['cidrlist'].split(',') or [vpn_conn['cidrlist']]
# Ensure we return a bool
self.result['force_encap'] = True if vpn_conn.get('forceencap') else False
args = {
'key': 'name',
'identifier': vpn_conn['s2scustomergatewayid'],
'refresh': True,
}
self.result['vpn_customer_gateway'] = self.get_vpn_customer_gateway(**args)
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
vpn_customer_gateway=dict(required=True),
vpc=dict(required=True),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
passive=dict(type='bool', default=False),
force=dict(type='bool', default=False),
state=dict(choices=['present', 'absent'], default='present'),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_vpn_conn = AnsibleCloudStackVpnConnection(module)
state = module.params.get('state')
if state == "absent":
vpn_conn = acs_vpn_conn.absent_vpn_connection()
else:
vpn_conn = acs_vpn_conn.present_vpn_connection()
result = acs_vpn_conn.get_result(vpn_conn)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,348 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, René Moser <mail@renemoser.net>
# 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 = r'''
---
module: cs_vpn_customer_gateway
short_description: Manages site-to-site VPN customer gateway configurations on Apache CloudStack based clouds.
description:
- Create, update and remove VPN customer gateways.
author: René Moser (@resmo)
options:
name:
description:
- Name of the gateway.
type: str
required: true
cidrs:
description:
- List of guest CIDRs behind the gateway.
- Required if I(state=present).
type: list
aliases: [ cidr ]
gateway:
description:
- Public IP address of the gateway.
- Required if I(state=present).
type: str
esp_policy:
description:
- ESP policy in the format e.g. C(aes256-sha1;modp1536).
- Required if I(state=present).
type: str
ike_policy:
description:
- IKE policy in the format e.g. C(aes256-sha1;modp1536).
- Required if I(state=present).
type: str
ipsec_psk:
description:
- IPsec Preshared-Key.
- Cannot contain newline or double quotes.
- Required if I(state=present).
type: str
ike_lifetime:
description:
- Lifetime in seconds of phase 1 VPN connection.
- Defaulted to 86400 by the API on creation if not set.
type: int
esp_lifetime:
description:
- Lifetime in seconds of phase 2 VPN connection.
- Defaulted to 3600 by the API on creation if not set.
type: int
dpd:
description:
- Enable Dead Peer Detection.
- Disabled per default by the API on creation if not set.
type: bool
force_encap:
description:
- Force encapsulation for NAT traversal.
- Disabled per default by the API on creation if not set.
type: bool
state:
description:
- State of the VPN customer gateway.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the VPN customer gateway is related to.
type: str
account:
description:
- Account the VPN customer gateway is related to.
type: str
project:
description:
- Name of the project the VPN gateway is related to.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
default: yes
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = r'''
- name: Create a vpn customer gateway
cs_vpn_customer_gateway:
name: my vpn customer gateway
cidrs:
- 192.168.123.0/24
- 192.168.124.0/24
esp_policy: aes256-sha1;modp1536
gateway: 10.10.1.1
ike_policy: aes256-sha1;modp1536
ipsec_psk: "S3cr3Tk3Y"
delegate_to: localhost
- name: Remove a vpn customer gateway
cs_vpn_customer_gateway:
name: my vpn customer gateway
state: absent
delegate_to: localhost
'''
RETURN = r'''
---
id:
description: UUID of the VPN customer gateway.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
gateway:
description: IP address of the VPN customer gateway.
returned: success
type: str
sample: 10.100.212.10
domain:
description: Domain the VPN customer gateway is related to.
returned: success
type: str
sample: example domain
account:
description: Account the VPN customer gateway is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the VPN customer gateway is related to.
returned: success
type: str
sample: Production
dpd:
description: Whether dead pear detection is enabled or not.
returned: success
type: bool
sample: true
esp_lifetime:
description: Lifetime in seconds of phase 2 VPN connection.
returned: success
type: int
sample: 86400
esp_policy:
description: IKE policy of the VPN customer gateway.
returned: success
type: str
sample: aes256-sha1;modp1536
force_encap:
description: Whether encapsulation for NAT traversal is enforced or not.
returned: success
type: bool
sample: true
ike_lifetime:
description: Lifetime in seconds of phase 1 VPN connection.
returned: success
type: int
sample: 86400
ike_policy:
description: ESP policy of the VPN customer gateway.
returned: success
type: str
sample: aes256-sha1;modp1536
name:
description: Name of this customer gateway.
returned: success
type: str
sample: my vpn customer gateway
cidrs:
description: List of CIDRs of this customer gateway.
returned: success
type: list
sample: [ 10.10.10.0/24 ]
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackVpnCustomerGateway(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVpnCustomerGateway, self).__init__(module)
self.returns = {
'dpd': 'dpd',
'esplifetime': 'esp_lifetime',
'esppolicy': 'esp_policy',
'gateway': 'gateway',
'ikepolicy': 'ike_policy',
'ikelifetime': 'ike_lifetime',
'ipaddress': 'ip_address',
}
def _common_args(self):
return {
'name': self.module.params.get('name'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'cidrlist': ','.join(self.module.params.get('cidrs')) if self.module.params.get('cidrs') is not None else None,
'esppolicy': self.module.params.get('esp_policy'),
'esplifetime': self.module.params.get('esp_lifetime'),
'ikepolicy': self.module.params.get('ike_policy'),
'ikelifetime': self.module.params.get('ike_lifetime'),
'ipsecpsk': self.module.params.get('ipsec_psk'),
'dpd': self.module.params.get('dpd'),
'forceencap': self.module.params.get('force_encap'),
'gateway': self.module.params.get('gateway'),
}
def get_vpn_customer_gateway(self):
args = {
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id'),
'fetch_list': True,
}
vpn_customer_gateway = self.module.params.get('name')
vpn_customer_gateways = self.query_api('listVpnCustomerGateways', **args)
if vpn_customer_gateways:
for vgw in vpn_customer_gateways:
if vpn_customer_gateway.lower() in [vgw['id'], vgw['name'].lower()]:
return vgw
def present_vpn_customer_gateway(self):
vpn_customer_gateway = self.get_vpn_customer_gateway()
required_params = [
'cidrs',
'esp_policy',
'gateway',
'ike_policy',
'ipsec_psk',
]
self.module.fail_on_missing_params(required_params=required_params)
if not vpn_customer_gateway:
vpn_customer_gateway = self._create_vpn_customer_gateway(vpn_customer_gateway)
else:
vpn_customer_gateway = self._update_vpn_customer_gateway(vpn_customer_gateway)
return vpn_customer_gateway
def _create_vpn_customer_gateway(self, vpn_customer_gateway):
self.result['changed'] = True
args = self._common_args()
if not self.module.check_mode:
res = self.query_api('createVpnCustomerGateway', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpn_customer_gateway = self.poll_job(res, 'vpncustomergateway')
return vpn_customer_gateway
def _update_vpn_customer_gateway(self, vpn_customer_gateway):
args = self._common_args()
args.update({'id': vpn_customer_gateway['id']})
if self.has_changed(args, vpn_customer_gateway, skip_diff_for_keys=['ipsecpsk']):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateVpnCustomerGateway', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpn_customer_gateway = self.poll_job(res, 'vpncustomergateway')
return vpn_customer_gateway
def absent_vpn_customer_gateway(self):
vpn_customer_gateway = self.get_vpn_customer_gateway()
if vpn_customer_gateway:
self.result['changed'] = True
args = {
'id': vpn_customer_gateway['id']
}
if not self.module.check_mode:
res = self.query_api('deleteVpnCustomerGateway', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'vpncustomergateway')
return vpn_customer_gateway
def get_result(self, vpn_customer_gateway):
super(AnsibleCloudStackVpnCustomerGateway, self).get_result(vpn_customer_gateway)
if vpn_customer_gateway:
if 'cidrlist' in vpn_customer_gateway:
self.result['cidrs'] = vpn_customer_gateway['cidrlist'].split(',') or [vpn_customer_gateway['cidrlist']]
# Ensure we return a bool
self.result['force_encap'] = True if vpn_customer_gateway.get('forceencap') else False
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
cidrs=dict(type='list', aliases=['cidr']),
esp_policy=dict(),
esp_lifetime=dict(type='int'),
gateway=dict(),
ike_policy=dict(),
ike_lifetime=dict(type='int'),
ipsec_psk=dict(no_log=True),
dpd=dict(type='bool'),
force_encap=dict(type='bool'),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_vpn_cgw = AnsibleCloudStackVpnCustomerGateway(module)
state = module.params.get('state')
if state == "absent":
vpn_customer_gateway = acs_vpn_cgw.absent_vpn_customer_gateway()
else:
vpn_customer_gateway = acs_vpn_cgw.present_vpn_customer_gateway()
result = acs_vpn_cgw.get_result(vpn_customer_gateway)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,210 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, René Moser <mail@renemoser.net>
# 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: cs_vpn_gateway
short_description: Manages site-to-site VPN gateways on Apache CloudStack based clouds.
description:
- Creates and removes VPN site-to-site gateways.
author: René Moser (@resmo)
options:
vpc:
description:
- Name of the VPC.
type: str
required: true
state:
description:
- State of the VPN gateway.
type: str
default: present
choices: [ present, absent ]
domain:
description:
- Domain the VPN gateway is related to.
type: str
account:
description:
- Account the VPN gateway is related to.
type: str
project:
description:
- Name of the project the VPN gateway is related to.
type: str
zone:
description:
- Name of the zone the VPC is related to.
- If not set, default zone is used.
type: str
poll_async:
description:
- Poll async jobs until job has finished.
type: bool
default: yes
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a vpn gateway is present
cs_vpn_gateway:
vpc: my VPC
delegate_to: localhost
- name: Ensure a vpn gateway is absent
cs_vpn_gateway:
vpc: my VPC
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the VPN site-to-site gateway.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
public_ip:
description: IP address of the VPN site-to-site gateway.
returned: success
type: str
sample: 10.100.212.10
vpc:
description: Name of the VPC.
returned: success
type: str
sample: My VPC
domain:
description: Domain the VPN site-to-site gateway is related to.
returned: success
type: str
sample: example domain
account:
description: Account the VPN site-to-site gateway is related to.
returned: success
type: str
sample: example account
project:
description: Name of project the VPN site-to-site gateway is related to.
returned: success
type: str
sample: Production
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together
)
class AnsibleCloudStackVpnGateway(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackVpnGateway, self).__init__(module)
self.returns = {
'publicip': 'public_ip'
}
def get_vpn_gateway(self):
args = {
'vpcid': self.get_vpc(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id')
}
vpn_gateways = self.query_api('listVpnGateways', **args)
if vpn_gateways:
return vpn_gateways['vpngateway'][0]
return None
def present_vpn_gateway(self):
vpn_gateway = self.get_vpn_gateway()
if not vpn_gateway:
self.result['changed'] = True
args = {
'vpcid': self.get_vpc(key='id'),
'account': self.get_account(key='name'),
'domainid': self.get_domain(key='id'),
'projectid': self.get_project(key='id')
}
if not self.module.check_mode:
res = self.query_api('createVpnGateway', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
vpn_gateway = self.poll_job(res, 'vpngateway')
return vpn_gateway
def absent_vpn_gateway(self):
vpn_gateway = self.get_vpn_gateway()
if vpn_gateway:
self.result['changed'] = True
args = {
'id': vpn_gateway['id']
}
if not self.module.check_mode:
res = self.query_api('deleteVpnGateway', **args)
poll_async = self.module.params.get('poll_async')
if poll_async:
self.poll_job(res, 'vpngateway')
return vpn_gateway
def get_result(self, vpn_gateway):
super(AnsibleCloudStackVpnGateway, self).get_result(vpn_gateway)
if vpn_gateway:
self.result['vpc'] = self.get_vpc(key='name')
return self.result
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
vpc=dict(required=True),
state=dict(choices=['present', 'absent'], default='present'),
domain=dict(),
account=dict(),
project=dict(),
zone=dict(),
poll_async=dict(type='bool', default=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_vpn_gw = AnsibleCloudStackVpnGateway(module)
state = module.params.get('state')
if state == "absent":
vpn_gateway = acs_vpn_gw.absent_vpn_gateway()
else:
vpn_gateway = acs_vpn_gw.present_vpn_gateway()
result = acs_vpn_gw.get_result(vpn_gateway)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,385 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_zone
short_description: Manages zones on Apache CloudStack based clouds.
description:
- Create, update and remove zones.
author: René Moser (@resmo)
options:
name:
description:
- Name of the zone.
type: str
required: true
id:
description:
- uuid of the existing zone.
type: str
state:
description:
- State of the zone.
type: str
default: present
choices: [ present, enabled, disabled, absent ]
domain:
description:
- Domain the zone is related to.
- Zone is a public zone if not set.
type: str
network_domain:
description:
- Network domain for the zone.
type: str
network_type:
description:
- Network type of the zone.
type: str
default: Basic
choices: [ Basic, Advanced ]
dns1:
description:
- First DNS for the zone.
- Required if I(state=present)
type: str
dns2:
description:
- Second DNS for the zone.
type: str
internal_dns1:
description:
- First internal DNS for the zone.
- If not set I(dns1) will be used on I(state=present).
type: str
internal_dns2:
description:
- Second internal DNS for the zone.
type: str
dns1_ipv6:
description:
- First DNS for IPv6 for the zone.
type: str
dns2_ipv6:
description:
- Second DNS for IPv6 for the zone.
type: str
guest_cidr_address:
description:
- Guest CIDR address for the zone.
type: str
dhcp_provider:
description:
- DHCP provider for the Zone.
type: str
local_storage_enabled:
description:
- Whether to enable local storage for the zone or not..
type: bool
securitygroups_enabled:
description:
- Whether the zone is security group enabled or not.
type: bool
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Ensure a zone is present
cs_zone:
name: ch-zrh-ix-01
dns1: 8.8.8.8
dns2: 8.8.4.4
network_type: basic
delegate_to: localhost
- name: Ensure a zone is disabled
cs_zone:
name: ch-zrh-ix-01
state: disabled
delegate_to: localhost
- name: Ensure a zone is enabled
cs_zone:
name: ch-zrh-ix-01
state: enabled
delegate_to: localhost
- name: Ensure a zone is absent
cs_zone:
name: ch-zrh-ix-01
state: absent
delegate_to: localhost
'''
RETURN = '''
---
id:
description: UUID of the zone.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the zone.
returned: success
type: str
sample: zone01
dns1:
description: First DNS for the zone.
returned: success
type: str
sample: 8.8.8.8
dns2:
description: Second DNS for the zone.
returned: success
type: str
sample: 8.8.4.4
internal_dns1:
description: First internal DNS for the zone.
returned: success
type: str
sample: 8.8.8.8
internal_dns2:
description: Second internal DNS for the zone.
returned: success
type: str
sample: 8.8.4.4
dns1_ipv6:
description: First IPv6 DNS for the zone.
returned: success
type: str
sample: "2001:4860:4860::8888"
dns2_ipv6:
description: Second IPv6 DNS for the zone.
returned: success
type: str
sample: "2001:4860:4860::8844"
allocation_state:
description: State of the zone.
returned: success
type: str
sample: Enabled
domain:
description: Domain the zone is related to.
returned: success
type: str
sample: ROOT
network_domain:
description: Network domain for the zone.
returned: success
type: str
sample: example.com
network_type:
description: Network type for the zone.
returned: success
type: str
sample: basic
local_storage_enabled:
description: Local storage offering enabled.
returned: success
type: bool
sample: false
securitygroups_enabled:
description: Security groups support is enabled.
returned: success
type: bool
sample: false
guest_cidr_address:
description: Guest CIDR address for the zone
returned: success
type: str
sample: 10.1.1.0/24
dhcp_provider:
description: DHCP provider for the zone
returned: success
type: str
sample: VirtualRouter
zone_token:
description: Zone token
returned: success
type: str
sample: ccb0a60c-79c8-3230-ab8b-8bdbe8c45bb7
tags:
description: List of resource tags associated with the zone.
returned: success
type: dict
sample: [ { "key": "foo", "value": "bar" } ]
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
cs_required_together,
)
class AnsibleCloudStackZone(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackZone, self).__init__(module)
self.returns = {
'dns1': 'dns1',
'dns2': 'dns2',
'internaldns1': 'internal_dns1',
'internaldns2': 'internal_dns2',
'ipv6dns1': 'dns1_ipv6',
'ipv6dns2': 'dns2_ipv6',
'domain': 'network_domain',
'networktype': 'network_type',
'securitygroupsenabled': 'securitygroups_enabled',
'localstorageenabled': 'local_storage_enabled',
'guestcidraddress': 'guest_cidr_address',
'dhcpprovider': 'dhcp_provider',
'allocationstate': 'allocation_state',
'zonetoken': 'zone_token',
}
self.zone = None
def _get_common_zone_args(self):
args = {
'name': self.module.params.get('name'),
'dns1': self.module.params.get('dns1'),
'dns2': self.module.params.get('dns2'),
'internaldns1': self.get_or_fallback('internal_dns1', 'dns1'),
'internaldns2': self.get_or_fallback('internal_dns2', 'dns2'),
'ipv6dns1': self.module.params.get('dns1_ipv6'),
'ipv6dns2': self.module.params.get('dns2_ipv6'),
'networktype': self.module.params.get('network_type'),
'domain': self.module.params.get('network_domain'),
'localstorageenabled': self.module.params.get('local_storage_enabled'),
'guestcidraddress': self.module.params.get('guest_cidr_address'),
'dhcpprovider': self.module.params.get('dhcp_provider'),
}
state = self.module.params.get('state')
if state in ['enabled', 'disabled']:
args['allocationstate'] = state.capitalize()
return args
def get_zone(self):
if not self.zone:
args = {}
uuid = self.module.params.get('id')
if uuid:
args['id'] = uuid
zones = self.query_api('listZones', **args)
if zones:
self.zone = zones['zone'][0]
return self.zone
args['name'] = self.module.params.get('name')
zones = self.query_api('listZones', **args)
if zones:
self.zone = zones['zone'][0]
return self.zone
def present_zone(self):
zone = self.get_zone()
if zone:
zone = self._update_zone()
else:
zone = self._create_zone()
return zone
def _create_zone(self):
required_params = [
'dns1',
]
self.module.fail_on_missing_params(required_params=required_params)
self.result['changed'] = True
args = self._get_common_zone_args()
args['domainid'] = self.get_domain(key='id')
args['securitygroupenabled'] = self.module.params.get('securitygroups_enabled')
zone = None
if not self.module.check_mode:
res = self.query_api('createZone', **args)
zone = res['zone']
return zone
def _update_zone(self):
zone = self.get_zone()
args = self._get_common_zone_args()
args['id'] = zone['id']
if self.has_changed(args, zone):
self.result['changed'] = True
if not self.module.check_mode:
res = self.query_api('updateZone', **args)
zone = res['zone']
return zone
def absent_zone(self):
zone = self.get_zone()
if zone:
self.result['changed'] = True
args = {
'id': zone['id']
}
if not self.module.check_mode:
self.query_api('deleteZone', **args)
return zone
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
id=dict(),
name=dict(required=True),
dns1=dict(),
dns2=dict(),
internal_dns1=dict(),
internal_dns2=dict(),
dns1_ipv6=dict(),
dns2_ipv6=dict(),
network_type=dict(default='Basic', choices=['Basic', 'Advanced']),
network_domain=dict(),
guest_cidr_address=dict(),
dhcp_provider=dict(),
local_storage_enabled=dict(type='bool'),
securitygroups_enabled=dict(type='bool'),
state=dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'),
domain=dict(),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_together=cs_required_together(),
supports_check_mode=True
)
acs_zone = AnsibleCloudStackZone(module)
state = module.params.get('state')
if state in ['absent']:
zone = acs_zone.absent_zone()
else:
zone = acs_zone.present_zone()
result = acs_zone.get_result(zone)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,201 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, René Moser <mail@renemoser.net>
# 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': ['deprecated'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_zone_facts
short_description: Gathering facts of zones from Apache CloudStack based clouds.
description:
- Gathering facts from the API of a zone.
- Sets Ansible facts accessible by the key C(cloudstack_zone) and since version 2.6 also returns results.
deprecated:
removed_in: "2.13"
why: Transformed into an info module.
alternative: Use M(cs_zone_info) instead.
author: René Moser (@resmo)
options:
zone:
description:
- Name of the zone.
type: str
required: true
aliases: [ name ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Gather facts from a zone
cs_zone_facts:
name: ch-gva-1
register: zone
delegate_to: localhost
- name: Show the returned results of the registered variable
debug:
var: zone
- name: Show the facts by the ansible_facts key cloudstack_zone
debug:
var: cloudstack_zone
'''
RETURN = '''
---
id:
description: UUID of the zone.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the zone.
returned: success
type: str
sample: zone01
dns1:
description: First DNS for the zone.
returned: success
type: str
sample: 8.8.8.8
dns2:
description: Second DNS for the zone.
returned: success
type: str
sample: 8.8.4.4
internal_dns1:
description: First internal DNS for the zone.
returned: success
type: str
sample: 8.8.8.8
internal_dns2:
description: Second internal DNS for the zone.
returned: success
type: str
sample: 8.8.4.4
dns1_ipv6:
description: First IPv6 DNS for the zone.
returned: success
type: str
sample: "2001:4860:4860::8888"
dns2_ipv6:
description: Second IPv6 DNS for the zone.
returned: success
type: str
sample: "2001:4860:4860::8844"
allocation_state:
description: State of the zone.
returned: success
type: str
sample: Enabled
domain:
description: Domain the zone is related to.
returned: success
type: str
sample: ROOT
network_domain:
description: Network domain for the zone.
returned: success
type: str
sample: example.com
network_type:
description: Network type for the zone.
returned: success
type: str
sample: basic
local_storage_enabled:
description: Local storage offering enabled.
returned: success
type: bool
sample: false
securitygroups_enabled:
description: Security groups support is enabled.
returned: success
type: bool
sample: false
guest_cidr_address:
description: Guest CIDR address for the zone
returned: success
type: str
sample: 10.1.1.0/24
dhcp_provider:
description: DHCP provider for the zone
returned: success
type: str
sample: VirtualRouter
zone_token:
description: Zone token
returned: success
type: str
sample: ccb0a60c-79c8-3230-ab8b-8bdbe8c45bb7
tags:
description: List of resource tags associated with the zone.
returned: success
type: dict
sample: [ { "key": "foo", "value": "bar" } ]
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
)
class AnsibleCloudStackZoneFacts(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackZoneFacts, self).__init__(module)
self.returns = {
'dns1': 'dns1',
'dns2': 'dns2',
'internaldns1': 'internal_dns1',
'internaldns2': 'internal_dns2',
'ipv6dns1': 'dns1_ipv6',
'ipv6dns2': 'dns2_ipv6',
'domain': 'network_domain',
'networktype': 'network_type',
'securitygroupsenabled': 'securitygroups_enabled',
'localstorageenabled': 'local_storage_enabled',
'guestcidraddress': 'guest_cidr_address',
'dhcpprovider': 'dhcp_provider',
'allocationstate': 'allocation_state',
'zonetoken': 'zone_token',
}
def get_zone(self):
return super(AnsibleCloudStackZoneFacts, self).get_zone()
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
zone=dict(required=True, aliases=['name']),
))
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
acs_zone_facts = AnsibleCloudStackZoneFacts(module=module)
result = acs_zone_facts.get_result_and_facts(
facts_name='cloudstack_zone',
resource=acs_zone_facts.get_zone()
)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,213 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, René Moser <mail@renemoser.net>
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: cs_zone_info
short_description: Gathering information about zones from Apache CloudStack based clouds.
description:
- Gathering information from the API of a zone.
author: René Moser (@resmo)
options:
zone:
description:
- Name of the zone.
- If not specified, all zones are returned
type: str
aliases: [ name ]
extends_documentation_fragment:
- community.general.cloudstack
'''
EXAMPLES = '''
- name: Gather information from a zone
cs_zone_info:
zone: ch-gva-1
register: zone
delegate_to: localhost
- name: Show the returned results of the registered variable
debug:
msg: "{{ zone }}"
- name: Gather information from all zones
cs_zone_info:
register: zones
delegate_to: localhost
- name: Show information on all zones
debug:
msg: "{{ zones }}"
'''
RETURN = '''
---
zones:
description: A list of matching zones.
type: list
returned: success
contains:
id:
description: UUID of the zone.
returned: success
type: str
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
name:
description: Name of the zone.
returned: success
type: str
sample: zone01
dns1:
description: First DNS for the zone.
returned: success
type: str
sample: 8.8.8.8
dns2:
description: Second DNS for the zone.
returned: success
type: str
sample: 8.8.4.4
internal_dns1:
description: First internal DNS for the zone.
returned: success
type: str
sample: 8.8.8.8
internal_dns2:
description: Second internal DNS for the zone.
returned: success
type: str
sample: 8.8.4.4
dns1_ipv6:
description: First IPv6 DNS for the zone.
returned: success
type: str
sample: "2001:4860:4860::8888"
dns2_ipv6:
description: Second IPv6 DNS for the zone.
returned: success
type: str
sample: "2001:4860:4860::8844"
allocation_state:
description: State of the zone.
returned: success
type: str
sample: Enabled
domain:
description: Domain the zone is related to.
returned: success
type: str
sample: ROOT
network_domain:
description: Network domain for the zone.
returned: success
type: str
sample: example.com
network_type:
description: Network type for the zone.
returned: success
type: str
sample: basic
local_storage_enabled:
description: Local storage offering enabled.
returned: success
type: bool
sample: false
securitygroups_enabled:
description: Security groups support is enabled.
returned: success
type: bool
sample: false
guest_cidr_address:
description: Guest CIDR address for the zone
returned: success
type: str
sample: 10.1.1.0/24
dhcp_provider:
description: DHCP provider for the zone
returned: success
type: str
sample: VirtualRouter
zone_token:
description: Zone token
returned: success
type: str
sample: ccb0a60c-79c8-3230-ab8b-8bdbe8c45bb7
tags:
description: List of resource tags associated with the zone.
returned: success
type: dict
sample: [ { "key": "foo", "value": "bar" } ]
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cloudstack import (
AnsibleCloudStack,
cs_argument_spec,
)
class AnsibleCloudStackZoneInfo(AnsibleCloudStack):
def __init__(self, module):
super(AnsibleCloudStackZoneInfo, self).__init__(module)
self.returns = {
'dns1': 'dns1',
'dns2': 'dns2',
'internaldns1': 'internal_dns1',
'internaldns2': 'internal_dns2',
'ipv6dns1': 'dns1_ipv6',
'ipv6dns2': 'dns2_ipv6',
'domain': 'network_domain',
'networktype': 'network_type',
'securitygroupsenabled': 'securitygroups_enabled',
'localstorageenabled': 'local_storage_enabled',
'guestcidraddress': 'guest_cidr_address',
'dhcpprovider': 'dhcp_provider',
'allocationstate': 'allocation_state',
'zonetoken': 'zone_token',
}
def get_zone(self):
if self.module.params['zone']:
zones = [super(AnsibleCloudStackZoneInfo, self).get_zone()]
else:
zones = self.query_api('listZones')
if zones:
zones = zones['zone']
else:
zones = []
return {
'zones': [self.update_result(resource) for resource in zones]
}
def main():
argument_spec = cs_argument_spec()
argument_spec.update(dict(
zone=dict(type='str', aliases=['name']),
))
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
acs_zone_info = AnsibleCloudStackZoneInfo(module=module)
result = acs_zone_info.get_zone()
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,479 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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': ['deprecated'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: digital_ocean
short_description: Create/delete a droplet/SSH_key in DigitalOcean
deprecated:
removed_in: '2.12'
why: Updated module to remove external dependency with increased functionality.
alternative: Use M(digital_ocean_droplet) instead.
description:
- Create/delete a droplet in DigitalOcean and optionally wait for it to be 'running', or deploy an SSH key.
author: "Vincent Viallet (@zbal)"
options:
command:
description:
- Which target you want to operate on.
default: droplet
choices: ['droplet', 'ssh']
state:
description:
- Indicate desired state of the target.
default: present
choices: ['present', 'active', 'absent', 'deleted']
api_token:
description:
- DigitalOcean api token.
id:
description:
- Numeric, the droplet id you want to operate on.
aliases: ['droplet_id']
name:
description:
- String, this is the name of the droplet - must be formatted by hostname rules, or the name of a SSH key.
unique_name:
description:
- Bool, require unique hostnames. By default, DigitalOcean allows multiple hosts with the same name. Setting this to "yes" allows only one host
per name. Useful for idempotence.
type: bool
default: 'no'
size_id:
description:
- This is the slug of the size you would like the droplet created with.
image_id:
description:
- This is the slug of the image you would like the droplet created with.
region_id:
description:
- This is the slug of the region you would like your server to be created in.
ssh_key_ids:
description:
- Optional, array of SSH key (numeric) ID that you would like to be added to the server.
virtio:
description:
- "Bool, turn on virtio driver in droplet for improved network and storage I/O."
type: bool
default: 'yes'
private_networking:
description:
- "Bool, add an additional, private network interface to droplet for inter-droplet communication."
type: bool
default: 'no'
backups_enabled:
description:
- Optional, Boolean, enables backups for your droplet.
type: bool
default: 'no'
user_data:
description:
- opaque blob of data which is made available to the droplet
ipv6:
description:
- Optional, Boolean, enable IPv6 for your droplet.
type: bool
default: 'no'
wait:
description:
- Wait for the droplet to be in state 'running' before returning. If wait is "no" an ip_address may not be returned.
type: bool
default: 'yes'
wait_timeout:
description:
- How long before wait gives up, in seconds.
default: 300
ssh_pub_key:
description:
- The public SSH key you want to add to your account.
notes:
- Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. They both refer to the v2 token.
- As of Ansible 1.9.5 and 2.0, Version 2 of the DigitalOcean API is used, this removes C(client_id) and C(api_key) options in favor of C(api_token).
- If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired.
Upgrade Ansible or, if unable to, try downloading the latest version of this module from github and putting it into a 'library' directory.
requirements:
- "python >= 2.6"
- dopy
'''
EXAMPLES = '''
# Ensure a SSH key is present
# If a key matches this name, will return the ssh key id and changed = False
# If no existing key matches this name, a new key is created, the ssh key id is returned and changed = False
- digital_ocean:
state: present
command: ssh
name: my_ssh_key
ssh_pub_key: 'ssh-rsa AAAA...'
api_token: XXX
# Create a new Droplet
# Will return the droplet details including the droplet id (used for idempotence)
- digital_ocean:
state: present
command: droplet
name: mydroplet
api_token: XXX
size_id: 2gb
region_id: ams2
image_id: fedora-19-x64
wait_timeout: 500
register: my_droplet
- debug:
msg: "ID is {{ my_droplet.droplet.id }}"
- debug:
msg: "IP is {{ my_droplet.droplet.ip_address }}"
# Ensure a droplet is present
# If droplet id already exist, will return the droplet details and changed = False
# If no droplet matches the id, a new droplet will be created and the droplet details (including the new id) are returned, changed = True.
- digital_ocean:
state: present
command: droplet
id: 123
name: mydroplet
api_token: XXX
size_id: 2gb
region_id: ams2
image_id: fedora-19-x64
wait_timeout: 500
# Create a droplet with ssh key
# The ssh key id can be passed as argument at the creation of a droplet (see ssh_key_ids).
# Several keys can be added to ssh_key_ids as id1,id2,id3
# The keys are used to connect as root to the droplet.
- digital_ocean:
state: present
ssh_key_ids: 123,456
name: mydroplet
api_token: XXX
size_id: 2gb
region_id: ams2
image_id: fedora-19-x64
'''
import os
import time
import traceback
from distutils.version import LooseVersion
try:
# Imported as a dependency for dopy
import ansible.module_utils.six
HAS_SIX = True
except ImportError:
HAS_SIX = False
HAS_DOPY = False
try:
import dopy
from dopy.manager import DoError, DoManager
if LooseVersion(dopy.__version__) >= LooseVersion('0.3.2'):
HAS_DOPY = True
except ImportError:
pass
from ansible.module_utils.basic import AnsibleModule, env_fallback
class TimeoutError(Exception):
def __init__(self, msg, id_):
super(TimeoutError, self).__init__(msg)
self.id = id_
class JsonfyMixIn(object):
def to_json(self):
return self.__dict__
class Droplet(JsonfyMixIn):
manager = None
def __init__(self, droplet_json):
self.status = 'new'
self.__dict__.update(droplet_json)
def is_powered_on(self):
return self.status == 'active'
def update_attr(self, attrs=None):
if attrs:
for k, v in attrs.items():
setattr(self, k, v)
networks = attrs.get('networks', {})
for network in networks.get('v6', []):
if network['type'] == 'public':
setattr(self, 'public_ipv6_address', network['ip_address'])
else:
setattr(self, 'private_ipv6_address', network['ip_address'])
else:
json = self.manager.show_droplet(self.id)
if json['ip_address']:
self.update_attr(json)
def power_on(self):
if self.status != 'off':
raise AssertionError('Can only power on a closed one.')
json = self.manager.power_on_droplet(self.id)
self.update_attr(json)
def ensure_powered_on(self, wait=True, wait_timeout=300):
if self.is_powered_on():
return
if self.status == 'off': # powered off
self.power_on()
if wait:
end_time = time.time() + wait_timeout
while time.time() < end_time:
time.sleep(min(20, end_time - time.time()))
self.update_attr()
if self.is_powered_on():
if not self.ip_address:
raise TimeoutError('No ip is found.', self.id)
return
raise TimeoutError('Wait for droplet running timeout', self.id)
def destroy(self):
return self.manager.destroy_droplet(self.id, scrub_data=True)
@classmethod
def setup(cls, api_token):
cls.manager = DoManager(None, api_token, api_version=2)
@classmethod
def add(cls, name, size_id, image_id, region_id, ssh_key_ids=None, virtio=True, private_networking=False, backups_enabled=False, user_data=None,
ipv6=False):
private_networking_lower = str(private_networking).lower()
backups_enabled_lower = str(backups_enabled).lower()
ipv6_lower = str(ipv6).lower()
json = cls.manager.new_droplet(name, size_id, image_id, region_id,
ssh_key_ids=ssh_key_ids, virtio=virtio, private_networking=private_networking_lower,
backups_enabled=backups_enabled_lower, user_data=user_data, ipv6=ipv6_lower)
droplet = cls(json)
return droplet
@classmethod
def find(cls, id=None, name=None):
if not id and not name:
return False
droplets = cls.list_all()
# Check first by id. digital ocean requires that it be unique
for droplet in droplets:
if droplet.id == id:
return droplet
# Failing that, check by hostname.
for droplet in droplets:
if droplet.name == name:
return droplet
return False
@classmethod
def list_all(cls):
json = cls.manager.all_active_droplets()
return list(map(cls, json))
class SSH(JsonfyMixIn):
manager = None
def __init__(self, ssh_key_json):
self.__dict__.update(ssh_key_json)
update_attr = __init__
def destroy(self):
self.manager.destroy_ssh_key(self.id)
return True
@classmethod
def setup(cls, api_token):
cls.manager = DoManager(None, api_token, api_version=2)
@classmethod
def find(cls, name):
if not name:
return False
keys = cls.list_all()
for key in keys:
if key.name == name:
return key
return False
@classmethod
def list_all(cls):
json = cls.manager.all_ssh_keys()
return list(map(cls, json))
@classmethod
def add(cls, name, key_pub):
json = cls.manager.new_ssh_key(name, key_pub)
return cls(json)
def core(module):
def getkeyordie(k):
v = module.params[k]
if v is None:
module.fail_json(msg='Unable to load %s' % k)
return v
api_token = module.params['api_token']
changed = True
command = module.params['command']
state = module.params['state']
if command == 'droplet':
Droplet.setup(api_token)
if state in ('active', 'present'):
# First, try to find a droplet by id.
droplet = Droplet.find(id=module.params['id'])
# If we couldn't find the droplet and the user is allowing unique
# hostnames, then check to see if a droplet with the specified
# hostname already exists.
if not droplet and module.params['unique_name']:
droplet = Droplet.find(name=getkeyordie('name'))
# If both of those attempts failed, then create a new droplet.
if not droplet:
droplet = Droplet.add(
name=getkeyordie('name'),
size_id=getkeyordie('size_id'),
image_id=getkeyordie('image_id'),
region_id=getkeyordie('region_id'),
ssh_key_ids=module.params['ssh_key_ids'],
virtio=module.params['virtio'],
private_networking=module.params['private_networking'],
backups_enabled=module.params['backups_enabled'],
user_data=module.params.get('user_data'),
ipv6=module.params['ipv6'],
)
if droplet.is_powered_on():
changed = False
droplet.ensure_powered_on(
wait=getkeyordie('wait'),
wait_timeout=getkeyordie('wait_timeout')
)
module.exit_json(changed=changed, droplet=droplet.to_json())
elif state in ('absent', 'deleted'):
# First, try to find a droplet by id.
droplet = Droplet.find(module.params['id'])
# If we couldn't find the droplet and the user is allowing unique
# hostnames, then check to see if a droplet with the specified
# hostname already exists.
if not droplet and module.params['unique_name']:
droplet = Droplet.find(name=getkeyordie('name'))
if not droplet:
module.exit_json(changed=False, msg='The droplet is not found.')
droplet.destroy()
module.exit_json(changed=True)
elif command == 'ssh':
SSH.setup(api_token)
name = getkeyordie('name')
if state in ('active', 'present'):
key = SSH.find(name)
if key:
module.exit_json(changed=False, ssh_key=key.to_json())
key = SSH.add(name, getkeyordie('ssh_pub_key'))
module.exit_json(changed=True, ssh_key=key.to_json())
elif state in ('absent', 'deleted'):
key = SSH.find(name)
if not key:
module.exit_json(changed=False, msg='SSH key with the name of %s is not found.' % name)
key.destroy()
module.exit_json(changed=True)
def main():
module = AnsibleModule(
argument_spec=dict(
command=dict(choices=['droplet', 'ssh'], default='droplet'),
state=dict(choices=['active', 'present', 'absent', 'deleted'], default='present'),
api_token=dict(
aliases=['API_TOKEN'],
no_log=True,
fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY'])
),
name=dict(type='str'),
size_id=dict(),
image_id=dict(),
region_id=dict(),
ssh_key_ids=dict(type='list'),
virtio=dict(type='bool', default='yes'),
private_networking=dict(type='bool', default='no'),
backups_enabled=dict(type='bool', default='no'),
id=dict(aliases=['droplet_id'], type='int'),
unique_name=dict(type='bool', default='no'),
user_data=dict(default=None),
ipv6=dict(type='bool', default='no'),
wait=dict(type='bool', default=True),
wait_timeout=dict(default=300, type='int'),
ssh_pub_key=dict(type='str'),
),
required_together=(
['size_id', 'image_id', 'region_id'],
),
mutually_exclusive=(
['size_id', 'ssh_pub_key'],
['image_id', 'ssh_pub_key'],
['region_id', 'ssh_pub_key'],
),
required_one_of=(
['id', 'name'],
),
)
if not HAS_DOPY and not HAS_SIX:
module.fail_json(msg='dopy >= 0.3.2 is required for this module. dopy requires six but six is not installed. '
'Make sure both dopy and six are installed.')
if not HAS_DOPY:
module.fail_json(msg='dopy >= 0.3.2 required for this module')
try:
core(module)
except TimeoutError as e:
module.fail_json(msg=str(e), id=e.id)
except (DoError, Exception) as e:
module.fail_json(msg=str(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_account_info.py

View file

@ -0,0 +1,87 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_account_info
short_description: Gather information about DigitalOcean User account
description:
- This module can be used to gather information about User account.
- This module was called C(digital_ocean_account_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about user account
digital_ocean_account_info:
oauth_token: "{{ oauth_token }}"
'''
RETURN = '''
data:
description: DigitalOcean account information
returned: success
type: dict
sample: {
"droplet_limit": 10,
"email": "testuser1@gmail.com",
"email_verified": true,
"floating_ip_limit": 3,
"status": "active",
"status_message": "",
"uuid": "aaaaaaaaaaaaaa"
}
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
rest = DigitalOceanHelper(module)
response = rest.get("account")
if response.status_code != 200:
module.fail_json(msg="Failed to fetch 'account' information due to error : %s" % response.json['message'])
module.exit_json(changed=False, data=response.json["account"])
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_account_facts':
module.deprecate("The 'digital_ocean_account_facts' module has been renamed to 'digital_ocean_account_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,285 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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: digital_ocean_block_storage
short_description: Create/destroy or attach/detach Block Storage volumes in DigitalOcean
description:
- Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet.
options:
command:
description:
- Which operation do you want to perform.
choices: ['create', 'attach']
required: true
state:
description:
- Indicate desired state of the target.
choices: ['present', 'absent']
required: true
block_size:
description:
- The size of the Block Storage volume in gigabytes. Required when command=create and state=present. If snapshot_id is included, this will be ignored.
volume_name:
description:
- The name of the Block Storage volume.
required: true
description:
description:
- Description of the Block Storage volume.
region:
description:
- The slug of the region where your Block Storage volume should be located in. If snapshot_id is included, this will be ignored.
required: true
snapshot_id:
description:
- The snapshot id you would like the Block Storage volume created with. If included, region and block_size will be ignored and changed to null.
droplet_id:
description:
- The droplet id you want to operate on. Required when command=attach.
extends_documentation_fragment:
- community.general.digital_ocean.documentation
notes:
- Two environment variables can be used, DO_API_KEY and DO_API_TOKEN.
They both refer to the v2 token.
- If snapshot_id is used, region and block_size will be ignored and changed to null.
author:
- "Harnek Sidhu (@harneksidhu)"
'''
EXAMPLES = '''
# Create new Block Storage
- digital_ocean_block_storage:
state: present
command: create
api_token: <TOKEN>
region: nyc1
block_size: 10
volume_name: nyc1-block-storage
# Delete Block Storage
- digital_ocean_block_storage:
state: absent
command: create
api_token: <TOKEN>
region: nyc1
volume_name: nyc1-block-storage
# Attach Block Storage to a Droplet
- digital_ocean_block_storage:
state: present
command: attach
api_token: <TOKEN>
volume_name: nyc1-block-storage
region: nyc1
droplet_id: <ID>
# Detach Block Storage from a Droplet
- digital_ocean_block_storage:
state: absent
command: attach
api_token: <TOKEN>
volume_name: nyc1-block-storage
region: nyc1
droplet_id: <ID>
'''
RETURN = '''
id:
description: Unique identifier of a Block Storage volume returned during creation.
returned: changed
type: str
sample: "69b25d9a-494c-12e6-a5af-001f53126b44"
'''
import time
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
class DOBlockStorageException(Exception):
pass
class DOBlockStorage(object):
def __init__(self, module):
self.module = module
self.rest = DigitalOceanHelper(module)
def get_key_or_fail(self, k):
v = self.module.params[k]
if v is None:
self.module.fail_json(msg='Unable to load %s' % k)
return v
def poll_action_for_complete_status(self, action_id):
url = 'actions/{0}'.format(action_id)
end_time = time.time() + self.module.params['timeout']
while time.time() < end_time:
time.sleep(2)
response = self.rest.get(url)
status = response.status_code
json = response.json
if status == 200:
if json['action']['status'] == 'completed':
return True
elif json['action']['status'] == 'errored':
raise DOBlockStorageException(json['message'])
raise DOBlockStorageException('Unable to reach api.digitalocean.com')
def get_attached_droplet_ID(self, volume_name, region):
url = 'volumes?name={0}&region={1}'.format(volume_name, region)
response = self.rest.get(url)
status = response.status_code
json = response.json
if status == 200:
volumes = json['volumes']
if len(volumes) > 0:
droplet_ids = volumes[0]['droplet_ids']
if len(droplet_ids) > 0:
return droplet_ids[0]
return None
else:
raise DOBlockStorageException(json['message'])
def attach_detach_block_storage(self, method, volume_name, region, droplet_id):
data = {
'type': method,
'volume_name': volume_name,
'region': region,
'droplet_id': droplet_id
}
response = self.rest.post('volumes/actions', data=data)
status = response.status_code
json = response.json
if status == 202:
return self.poll_action_for_complete_status(json['action']['id'])
elif status == 200:
return True
elif status == 422:
return False
else:
raise DOBlockStorageException(json['message'])
def create_block_storage(self):
volume_name = self.get_key_or_fail('volume_name')
snapshot_id = self.module.params['snapshot_id']
if snapshot_id:
self.module.params['block_size'] = None
self.module.params['region'] = None
block_size = None
region = None
else:
block_size = self.get_key_or_fail('block_size')
region = self.get_key_or_fail('region')
description = self.module.params['description']
data = {
'size_gigabytes': block_size,
'name': volume_name,
'description': description,
'region': region,
'snapshot_id': snapshot_id,
}
response = self.rest.post("volumes", data=data)
status = response.status_code
json = response.json
if status == 201:
self.module.exit_json(changed=True, id=json['volume']['id'])
elif status == 409 and json['id'] == 'conflict':
self.module.exit_json(changed=False)
else:
raise DOBlockStorageException(json['message'])
def delete_block_storage(self):
volume_name = self.get_key_or_fail('volume_name')
region = self.get_key_or_fail('region')
url = 'volumes?name={0}&region={1}'.format(volume_name, region)
attached_droplet_id = self.get_attached_droplet_ID(volume_name, region)
if attached_droplet_id is not None:
self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id)
response = self.rest.delete(url)
status = response.status_code
json = response.json
if status == 204:
self.module.exit_json(changed=True)
elif status == 404:
self.module.exit_json(changed=False)
else:
raise DOBlockStorageException(json['message'])
def attach_block_storage(self):
volume_name = self.get_key_or_fail('volume_name')
region = self.get_key_or_fail('region')
droplet_id = self.get_key_or_fail('droplet_id')
attached_droplet_id = self.get_attached_droplet_ID(volume_name, region)
if attached_droplet_id is not None:
if attached_droplet_id == droplet_id:
self.module.exit_json(changed=False)
else:
self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id)
changed_status = self.attach_detach_block_storage('attach', volume_name, region, droplet_id)
self.module.exit_json(changed=changed_status)
def detach_block_storage(self):
volume_name = self.get_key_or_fail('volume_name')
region = self.get_key_or_fail('region')
droplet_id = self.get_key_or_fail('droplet_id')
changed_status = self.attach_detach_block_storage('detach', volume_name, region, droplet_id)
self.module.exit_json(changed=changed_status)
def handle_request(module):
block_storage = DOBlockStorage(module)
command = module.params['command']
state = module.params['state']
if command == 'create':
if state == 'present':
block_storage.create_block_storage()
elif state == 'absent':
block_storage.delete_block_storage()
elif command == 'attach':
if state == 'present':
block_storage.attach_block_storage()
elif state == 'absent':
block_storage.detach_block_storage()
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
state=dict(choices=['present', 'absent'], required=True),
command=dict(choices=['create', 'attach'], required=True),
block_size=dict(type='int', required=False),
volume_name=dict(type='str', required=True),
description=dict(type='str'),
region=dict(type='str', required=False),
snapshot_id=dict(type='str', required=False),
droplet_id=dict(type='int')
)
module = AnsibleModule(argument_spec=argument_spec)
try:
handle_request(module)
except DOBlockStorageException as e:
module.fail_json(msg=e.message, exception=traceback.format_exc())
except KeyError as e:
module.fail_json(msg='Unable to load %s' % e.message, exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,174 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_certificate
short_description: Manage certificates in DigitalOcean.
description:
- Create, Retrieve and remove certificates DigitalOcean.
author: "Abhijeet Kasurde (@Akasurde)"
options:
name:
description:
- The name of the certificate.
required: true
private_key:
description:
- A PEM-formatted private key content of SSL Certificate.
leaf_certificate:
description:
- A PEM-formatted public SSL Certificate.
certificate_chain:
description:
- The full PEM-formatted trust chain between the certificate authority's certificate and your domain's SSL certificate.
state:
description:
- Whether the certificate should be present or absent.
default: present
choices: ['present', 'absent']
extends_documentation_fragment:
- community.general.digital_ocean.documentation
notes:
- Two environment variables can be used, DO_API_KEY, DO_OAUTH_TOKEN and DO_API_TOKEN.
They both refer to the v2 token.
'''
EXAMPLES = '''
- name: create a certificate
digital_ocean_certificate:
name: production
state: present
private_key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkM8OI7pRpgyj1I\n-----END PRIVATE KEY-----"
leaf_certificate: "-----BEGIN CERTIFICATE-----\nMIIFDmg2Iaw==\n-----END CERTIFICATE-----"
oauth_token: b7d03a6947b217efb6f3ec3bd365652
- name: create a certificate using file lookup plugin
digital_ocean_certificate:
name: production
state: present
private_key: "{{ lookup('file', 'test.key') }}"
leaf_certificate: "{{ lookup('file', 'test.cert') }}"
oauth_token: "{{ oauth_token }}"
- name: create a certificate with trust chain
digital_ocean_certificate:
name: production
state: present
private_key: "{{ lookup('file', 'test.key') }}"
leaf_certificate: "{{ lookup('file', 'test.cert') }}"
certificate_chain: "{{ lookup('file', 'chain.cert') }}"
oauth_token: "{{ oauth_token }}"
- name: remove a certificate
digital_ocean_certificate:
name: production
state: absent
oauth_token: "{{ oauth_token }}"
'''
RETURN = ''' # '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
state = module.params['state']
name = module.params['name']
rest = DigitalOceanHelper(module)
results = dict(changed=False)
response = rest.get('certificates')
status_code = response.status_code
resp_json = response.json
if status_code != 200:
module.fail_json(msg="Failed to retrieve certificates for DigitalOcean")
if state == 'present':
for cert in resp_json['certificates']:
if cert['name'] == name:
module.fail_json(msg="Certificate name %s already exists" % name)
# Certificate does not exist, let us create it
cert_data = dict(name=name,
private_key=module.params['private_key'],
leaf_certificate=module.params['leaf_certificate'])
if module.params['certificate_chain'] is not None:
cert_data.update(certificate_chain=module.params['certificate_chain'])
response = rest.post("certificates", data=cert_data)
status_code = response.status_code
if status_code == 500:
module.fail_json(msg="Failed to upload certificates as the certificates are malformed.")
resp_json = response.json
if status_code == 201:
results.update(changed=True, response=resp_json)
elif status_code == 422:
results.update(changed=False, response=resp_json)
elif state == 'absent':
cert_id_del = None
for cert in resp_json['certificates']:
if cert['name'] == name:
cert_id_del = cert['id']
if cert_id_del is not None:
url = "certificates/{0}".format(cert_id_del)
response = rest.delete(url)
if response.status_code == 204:
results.update(changed=True)
else:
results.update(changed=False)
else:
module.fail_json(msg="Failed to find certificate %s" % name)
module.exit_json(**results)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
name=dict(type='str'),
leaf_certificate=dict(type='str'),
private_key=dict(type='str', no_log=True),
state=dict(choices=['present', 'absent'], default='present'),
certificate_chain=dict(type='str')
)
module = AnsibleModule(
argument_spec=argument_spec,
required_if=[('state', 'present', ['name', 'leaf_certificate', 'private_key']),
('state', 'absent', ['name'])
],
)
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e))
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_certificate_info.py

View file

@ -0,0 +1,119 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_certificate_info
short_description: Gather information about DigitalOcean certificates
description:
- This module can be used to gather information about DigitalOcean provided certificates.
- This module was called C(digital_ocean_certificate_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
options:
certificate_id:
description:
- Certificate ID that can be used to identify and reference a certificate.
required: false
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about all certificates
digital_ocean_certificate_info:
oauth_token: "{{ oauth_token }}"
- name: Gather information about certificate with given id
digital_ocean_certificate_info:
oauth_token: "{{ oauth_token }}"
certificate_id: "892071a0-bb95-49bc-8021-3afd67a210bf"
- name: Get not after information about certificate
digital_ocean_certificate_info:
register: resp_out
- set_fact:
not_after_date: "{{ item.not_after }}"
loop: "{{ resp_out.data|json_query(name) }}"
vars:
name: "[?name=='web-cert-01']"
- debug: var=not_after_date
'''
RETURN = '''
data:
description: DigitalOcean certificate information
returned: success
type: list
sample: [
{
"id": "892071a0-bb95-49bc-8021-3afd67a210bf",
"name": "web-cert-01",
"not_after": "2017-02-22T00:23:00Z",
"sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
"created_at": "2017-02-08T16:02:37Z"
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
certificate_id = module.params.get('certificate_id', None)
rest = DigitalOceanHelper(module)
base_url = 'certificates?'
if certificate_id is not None:
response = rest.get("%s/%s" % (base_url, certificate_id))
status_code = response.status_code
if status_code != 200:
module.fail_json(msg="Failed to retrieve certificates for DigitalOcean")
resp_json = response.json
certificate = resp_json['certificate']
else:
certificate = rest.get_paginated_data(base_url=base_url, data_key_name='certificates')
module.exit_json(changed=False, data=certificate)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
certificate_id=dict(type='str', required=False),
)
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_certificate_facts':
module.deprecate("The 'digital_ocean_certificate_facts' module has been renamed to 'digital_ocean_certificate_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,221 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: digital_ocean_domain
short_description: Create/delete a DNS domain in DigitalOcean
description:
- Create/delete a DNS domain in DigitalOcean.
author: "Michael Gregson (@mgregson)"
options:
state:
description:
- Indicate desired state of the target.
default: present
choices: ['present', 'absent']
id:
description:
- Numeric, the droplet id you want to operate on.
aliases: ['droplet_id']
name:
description:
- String, this is the name of the droplet - must be formatted by hostname rules, or the name of a SSH key, or the name of a domain.
ip:
description:
- An 'A' record for '@' ($ORIGIN) will be created with the value 'ip'. 'ip' is an IP version 4 address.
extends_documentation_fragment:
- community.general.digital_ocean.documentation
notes:
- Environment variables DO_OAUTH_TOKEN can be used for the oauth_token.
- As of Ansible 1.9.5 and 2.0, Version 2 of the DigitalOcean API is used, this removes C(client_id) and C(api_key) options in favor of C(oauth_token).
- If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired.
requirements:
- "python >= 2.6"
'''
EXAMPLES = '''
# Create a domain
- digital_ocean_domain:
state: present
name: my.digitalocean.domain
ip: 127.0.0.1
# Create a droplet and a corresponding domain
- digital_ocean:
state: present
name: test_droplet
size_id: 1gb
region_id: sgp1
image_id: ubuntu-14-04-x64
register: test_droplet
- digital_ocean_domain:
state: present
name: "{{ test_droplet.droplet.name }}.my.domain"
ip: "{{ test_droplet.droplet.ip_address }}"
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
class DoManager(DigitalOceanHelper, object):
def __init__(self, module):
super(DoManager, self).__init__(module)
self.domain_name = module.params.get('name', None)
self.domain_ip = module.params.get('ip', None)
self.domain_id = module.params.get('id', None)
@staticmethod
def jsonify(response):
return response.status_code, response.json
def all_domains(self):
resp = self.get('domains/')
return resp
def find(self):
if self.domain_name is None and self.domain_id is None:
return False
domains = self.all_domains()
status, json = self.jsonify(domains)
for domain in json['domains']:
if domain['name'] == self.domain_name:
return True
return False
def add(self):
params = {'name': self.domain_name, 'ip_address': self.domain_ip}
resp = self.post('domains/', data=params)
status = resp.status_code
json = resp.json
if status == 201:
return json['domain']
else:
return json
def all_domain_records(self):
resp = self.get('domains/%s/records/' % self.domain_name)
return resp.json
def domain_record(self):
resp = self.get('domains/%s' % self.domain_name)
status, json = self.jsonify(resp)
return json
def destroy_domain(self):
resp = self.delete('domains/%s' % self.domain_name)
status, json = self.jsonify(resp)
if status == 204:
return True
else:
return json
def edit_domain_record(self, record):
params = {'name': '@',
'data': self.module.params.get('ip')}
resp = self.put('domains/%s/records/%s' % (self.domain_name, record['id']), data=params)
status, json = self.jsonify(resp)
return json['domain_record']
def create_domain_record(self):
params = {'name': '@',
'type': 'A',
'data': self.module.params.get('ip')}
resp = self.post('domains/%s/records' % (self.domain_name), data=params)
status, json = self.jsonify(resp)
return json['domain_record']
def core(module):
do_manager = DoManager(module)
state = module.params.get('state')
domain = do_manager.find()
if state == 'present':
if not domain:
domain = do_manager.add()
if 'message' in domain:
module.fail_json(changed=False, msg=domain['message'])
else:
module.exit_json(changed=True, domain=domain)
else:
records = do_manager.all_domain_records()
at_record = None
for record in records['domain_records']:
if record['name'] == "@" and record['type'] == 'A':
at_record = record
if not at_record:
do_manager.create_domain_record()
module.exit_json(changed=True, domain=do_manager.find())
elif not at_record['data'] == module.params.get('ip'):
do_manager.edit_domain_record(at_record)
module.exit_json(changed=True, domain=do_manager.find())
else:
module.exit_json(changed=False, domain=do_manager.domain_record())
elif state == 'absent':
if not domain:
module.exit_json(changed=False, msg="Domain not found")
else:
delete_event = do_manager.destroy_domain()
if not delete_event:
module.fail_json(changed=False, msg=delete_event['message'])
else:
module.exit_json(changed=True, event=None)
delete_event = do_manager.destroy_domain()
module.exit_json(changed=delete_event)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
state=dict(choices=['present', 'absent'], default='present'),
name=dict(type='str'),
id=dict(aliases=['droplet_id'], type='int'),
ip=dict(type='str')
)
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(
['id', 'name'],
),
)
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_domain_info.py

View file

@ -0,0 +1,144 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_domain_info
short_description: Gather information about DigitalOcean Domains
description:
- This module can be used to gather information about DigitalOcean provided Domains.
- This module was called C(digital_ocean_domain_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
options:
domain_name:
description:
- Name of the domain to gather information for.
required: false
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about all domains
digital_ocean_domain_info:
oauth_token: "{{ oauth_token }}"
- name: Gather information about domain with given name
digital_ocean_domain_info:
oauth_token: "{{ oauth_token }}"
domain_name: "example.com"
- name: Get ttl from domain
digital_ocean_domain_info:
register: resp_out
- set_fact:
domain_ttl: "{{ item.ttl }}"
loop: "{{ resp_out.data|json_query(name) }}"
vars:
name: "[?name=='example.com']"
- debug: var=domain_ttl
'''
RETURN = '''
data:
description: DigitalOcean Domain information
returned: success
type: list
sample: [
{
"domain_records": [
{
"data": "ns1.digitalocean.com",
"flags": null,
"id": 37826823,
"name": "@",
"port": null,
"priority": null,
"tag": null,
"ttl": 1800,
"type": "NS",
"weight": null
},
],
"name": "myexample123.com",
"ttl": 1800,
"zone_file": "myexample123.com. IN SOA ns1.digitalocean.com. hostmaster.myexample123.com. 1520702984 10800 3600 604800 1800\n",
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
domain_name = module.params.get('domain_name', None)
rest = DigitalOceanHelper(module)
domain_results = []
if domain_name is not None:
response = rest.get("domains/%s" % domain_name)
status_code = response.status_code
if status_code != 200:
module.fail_json(msg="Failed to retrieve domain for DigitalOcean")
resp_json = response.json
domains = [resp_json['domain']]
else:
domains = rest.get_paginated_data(base_url="domains?", data_key_name='domains')
for temp_domain in domains:
temp_domain_dict = {
"name": temp_domain['name'],
"ttl": temp_domain['ttl'],
"zone_file": temp_domain['zone_file'],
"domain_records": list(),
}
base_url = "domains/%s/records?" % temp_domain['name']
temp_domain_dict["domain_records"] = rest.get_paginated_data(base_url=base_url, data_key_name='domain_records')
domain_results.append(temp_domain_dict)
module.exit_json(changed=False, data=domain_results)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
domain_name=dict(type='str', required=False),
)
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_domain_facts':
module.deprecate("The 'digital_ocean_domain_facts' module has been renamed to 'digital_ocean_domain_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,349 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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: digital_ocean_droplet
short_description: Create and delete a DigitalOcean droplet
description:
- Create and delete a droplet in DigitalOcean and optionally wait for it to be active.
author: "Gurchet Rai (@gurch101)"
options:
state:
description:
- Indicate desired state of the target.
default: present
choices: ['present', 'absent']
id:
description:
- Numeric, the droplet id you want to operate on.
aliases: ['droplet_id']
name:
description:
- String, this is the name of the droplet - must be formatted by hostname rules.
unique_name:
description:
- require unique hostnames. By default, DigitalOcean allows multiple hosts with the same name. Setting this to "yes" allows only one host
per name. Useful for idempotence.
default: False
type: bool
size:
description:
- This is the slug of the size you would like the droplet created with.
aliases: ['size_id']
image:
description:
- This is the slug of the image you would like the droplet created with.
aliases: ['image_id']
region:
description:
- This is the slug of the region you would like your server to be created in.
aliases: ['region_id']
ssh_keys:
description:
- array of SSH key Fingerprint that you would like to be added to the server.
required: False
private_networking:
description:
- add an additional, private network interface to droplet for inter-droplet communication.
default: False
type: bool
user_data:
description:
- opaque blob of data which is made available to the droplet
required: False
ipv6:
description:
- enable IPv6 for your droplet.
required: False
default: False
type: bool
wait:
description:
- Wait for the droplet to be active before returning. If wait is "no" an ip_address may not be returned.
required: False
default: True
type: bool
wait_timeout:
description:
- How long before wait gives up, in seconds, when creating a droplet.
default: 120
backups:
description:
- indicates whether automated backups should be enabled.
required: False
default: False
type: bool
monitoring:
description:
- indicates whether to install the DigitalOcean agent for monitoring.
required: False
default: False
type: bool
tags:
description:
- List, A list of tag names as strings to apply to the Droplet after it is created. Tag names can either be existing or new tags.
required: False
volumes:
description:
- List, A list including the unique string identifier for each Block Storage volume to be attached to the Droplet.
required: False
oauth_token:
description:
- DigitalOcean OAuth token. Can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
aliases: ['API_TOKEN']
required: True
requirements:
- "python >= 2.6"
'''
EXAMPLES = '''
- name: create a new droplet
digital_ocean_droplet:
state: present
name: mydroplet
oauth_token: XXX
size: 2gb
region: sfo1
image: ubuntu-16-04-x64
wait_timeout: 500
ssh_keys: [ .... ]
register: my_droplet
- debug:
msg: "ID is {{ my_droplet.data.droplet.id }}, IP is {{ my_droplet.data.ip_address }}"
- name: ensure a droplet is present
digital_ocean_droplet:
state: present
id: 123
name: mydroplet
oauth_token: XXX
size: 2gb
region: sfo1
image: ubuntu-16-04-x64
wait_timeout: 500
- name: ensure a droplet is present with SSH keys installed
digital_ocean_droplet:
state: present
id: 123
name: mydroplet
oauth_token: XXX
size: 2gb
region: sfo1
ssh_keys: ['1534404', '1784768']
image: ubuntu-16-04-x64
wait_timeout: 500
'''
RETURN = '''
# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#droplets
data:
description: a DigitalOcean Droplet
returned: changed
type: dict
sample: {
"ip_address": "104.248.118.172",
"ipv6_address": "2604:a880:400:d1::90a:6001",
"private_ipv4_address": "10.136.122.141",
"droplet": {
"id": 3164494,
"name": "example.com",
"memory": 512,
"vcpus": 1,
"disk": 20,
"locked": true,
"status": "new",
"kernel": {
"id": 2233,
"name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic",
"version": "3.13.0-37-generic"
},
"created_at": "2014-11-14T16:36:31Z",
"features": ["virtio"],
"backup_ids": [],
"snapshot_ids": [],
"image": {},
"volume_ids": [],
"size": {},
"size_slug": "512mb",
"networks": {},
"region": {},
"tags": ["web"]
}
}
'''
import time
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
class DODroplet(object):
def __init__(self, module):
self.rest = DigitalOceanHelper(module)
self.module = module
self.wait = self.module.params.pop('wait', True)
self.wait_timeout = self.module.params.pop('wait_timeout', 120)
self.unique_name = self.module.params.pop('unique_name', False)
# pop the oauth token so we don't include it in the POST data
self.module.params.pop('oauth_token')
def get_by_id(self, droplet_id):
if not droplet_id:
return None
response = self.rest.get('droplets/{0}'.format(droplet_id))
json_data = response.json
if response.status_code == 200:
return json_data
return None
def get_by_name(self, droplet_name):
if not droplet_name:
return None
page = 1
while page is not None:
response = self.rest.get('droplets?page={0}'.format(page))
json_data = response.json
if response.status_code == 200:
for droplet in json_data['droplets']:
if droplet['name'] == droplet_name:
return {'droplet': droplet}
if 'links' in json_data and 'pages' in json_data['links'] and 'next' in json_data['links']['pages']:
page += 1
else:
page = None
return None
def get_addresses(self, data):
"""
Expose IP addresses as their own property allowing users extend to additional tasks
"""
_data = data
for k, v in data.items():
setattr(self, k, v)
networks = _data['droplet']['networks']
for network in networks.get('v4', []):
if network['type'] == 'public':
_data['ip_address'] = network['ip_address']
else:
_data['private_ipv4_address'] = network['ip_address']
for network in networks.get('v6', []):
if network['type'] == 'public':
_data['ipv6_address'] = network['ip_address']
else:
_data['private_ipv6_address'] = network['ip_address']
return _data
def get_droplet(self):
json_data = self.get_by_id(self.module.params['id'])
if not json_data and self.unique_name:
json_data = self.get_by_name(self.module.params['name'])
return json_data
def create(self):
json_data = self.get_droplet()
droplet_data = None
if json_data:
droplet_data = self.get_addresses(json_data)
self.module.exit_json(changed=False, data=droplet_data)
if self.module.check_mode:
self.module.exit_json(changed=True)
request_params = dict(self.module.params)
del request_params['id']
response = self.rest.post('droplets', data=request_params)
json_data = response.json
if response.status_code >= 400:
self.module.fail_json(changed=False, msg=json_data['message'])
if self.wait:
json_data = self.ensure_power_on(json_data['droplet']['id'])
droplet_data = self.get_addresses(json_data)
self.module.exit_json(changed=True, data=droplet_data)
def delete(self):
json_data = self.get_droplet()
if json_data:
if self.module.check_mode:
self.module.exit_json(changed=True)
response = self.rest.delete('droplets/{0}'.format(json_data['droplet']['id']))
json_data = response.json
if response.status_code == 204:
self.module.exit_json(changed=True, msg='Droplet deleted')
self.module.fail_json(changed=False, msg='Failed to delete droplet')
else:
self.module.exit_json(changed=False, msg='Droplet not found')
def ensure_power_on(self, droplet_id):
end_time = time.time() + self.wait_timeout
while time.time() < end_time:
response = self.rest.get('droplets/{0}'.format(droplet_id))
json_data = response.json
if json_data['droplet']['status'] == 'active':
return json_data
time.sleep(min(2, end_time - time.time()))
self.module.fail_json(msg='Wait for droplet powering on timeout')
def core(module):
state = module.params.pop('state')
droplet = DODroplet(module)
if state == 'present':
droplet.create()
elif state == 'absent':
droplet.delete()
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(choices=['present', 'absent'], default='present'),
oauth_token=dict(
aliases=['API_TOKEN'],
no_log=True,
fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN'])
),
name=dict(type='str'),
size=dict(aliases=['size_id']),
image=dict(aliases=['image_id']),
region=dict(aliases=['region_id']),
ssh_keys=dict(type='list'),
private_networking=dict(type='bool', default=False),
backups=dict(type='bool', default=False),
monitoring=dict(type='bool', default=False),
id=dict(aliases=['droplet_id'], type='int'),
user_data=dict(default=None),
ipv6=dict(type='bool', default=False),
volumes=dict(type='list'),
tags=dict(type='list'),
wait=dict(type='bool', default=True),
wait_timeout=dict(default=120, type='int'),
unique_name=dict(type='bool', default=False),
),
required_one_of=(
['id', 'name'],
),
required_if=([
('state', 'present', ['name', 'size', 'image', 'region']),
]),
supports_check_mode=True,
)
core(module)
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_firewall_info.py

View file

@ -0,0 +1,137 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Anthony Bond <ajbond2005@gmail.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: digital_ocean_firewall_info
short_description: Gather information about DigitalOcean firewalls
description:
- This module can be used to gather information about DigitalOcean firewalls.
- This module was called C(digital_ocean_firewall_facts) before Ansible 2.9. The usage did not change.
author: "Anthony Bond (@BondAnthony)"
options:
name:
description:
- Firewall rule name that can be used to identify and reference a specific firewall rule.
required: false
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about all firewalls
digital_ocean_firewall_info:
oauth_token: "{{ oauth_token }}"
- name: Gather information about a specific firewall by name
digital_ocean_firewall_info:
oauth_token: "{{ oauth_token }}"
name: "firewall_name"
- name: Gather information from a firewall rule
digital_ocean_firewall_info:
name: SSH
register: resp_out
- set_fact:
firewall_id: "{{ resp_out.data.id }}"
- debug:
msg: "{{ firewall_id }}"
'''
RETURN = '''
data:
description: DigitalOcean firewall information
returned: success
type: list
sample: [
{
"id": "435tbg678-1db53-32b6-t543-28322569t252",
"name": "metrics",
"status": "succeeded",
"inbound_rules": [
{
"protocol": "tcp",
"ports": "9100",
"sources": {
"addresses": [
"1.1.1.1"
]
}
}
],
"outbound_rules": [],
"created_at": "2018-01-15T07:04:25Z",
"droplet_ids": [
87426985
],
"tags": [],
"pending_changes": []
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
firewall_name = module.params.get('name', None)
rest = DigitalOceanHelper(module)
base_url = 'firewalls?'
response = rest.get("%s" % base_url)
status_code = response.status_code
if status_code != 200:
module.fail_json(msg="Failed to retrieve firewalls from Digital Ocean")
firewalls = rest.get_paginated_data(base_url=base_url, data_key_name='firewalls')
if firewall_name is not None:
rule = {}
for firewall in firewalls:
if firewall['name'] == firewall_name:
rule.update(firewall)
module.exit_json(changed=False, data=rule)
else:
module.exit_json(changed=False, data=firewalls)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
name=dict(type='str', required=False),
)
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_firewall_facts':
module.deprecate("The 'digital_ocean_firewall_facts' module has been renamed to 'digital_ocean_firewall_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1,315 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2015, Patrick F. Marques <patrickfmarques@gmail.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: digital_ocean_floating_ip
short_description: Manage DigitalOcean Floating IPs
description:
- Create/delete/assign a floating IP.
author: "Patrick Marques (@pmarques)"
options:
state:
description:
- Indicate desired state of the target.
default: present
choices: ['present', 'absent']
ip:
description:
- Public IP address of the Floating IP. Used to remove an IP
region:
description:
- The region that the Floating IP is reserved to.
droplet_id:
description:
- The Droplet that the Floating IP has been assigned to.
oauth_token:
description:
- DigitalOcean OAuth token.
required: true
notes:
- Version 2 of DigitalOcean API is used.
requirements:
- "python >= 2.6"
'''
EXAMPLES = '''
- name: "Create a Floating IP in region lon1"
digital_ocean_floating_ip:
state: present
region: lon1
- name: "Create a Floating IP assigned to Droplet ID 123456"
digital_ocean_floating_ip:
state: present
droplet_id: 123456
- name: "Delete a Floating IP with ip 1.2.3.4"
digital_ocean_floating_ip:
state: absent
ip: "1.2.3.4"
'''
RETURN = '''
# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#floating-ips
data:
description: a DigitalOcean Floating IP resource
returned: success and no resource constraint
type: dict
sample: {
"action": {
"id": 68212728,
"status": "in-progress",
"type": "assign_ip",
"started_at": "2015-10-15T17:45:44Z",
"completed_at": null,
"resource_id": 758603823,
"resource_type": "floating_ip",
"region": {
"name": "New York 3",
"slug": "nyc3",
"sizes": [
"512mb",
"1gb",
"2gb",
"4gb",
"8gb",
"16gb",
"32gb",
"48gb",
"64gb"
],
"features": [
"private_networking",
"backups",
"ipv6",
"metadata"
],
"available": true
},
"region_slug": "nyc3"
}
}
'''
import json
import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.urls import fetch_url
class Response(object):
def __init__(self, resp, info):
self.body = None
if resp:
self.body = resp.read()
self.info = info
@property
def json(self):
if not self.body:
if "body" in self.info:
return json.loads(self.info["body"])
return None
try:
return json.loads(self.body)
except ValueError:
return None
@property
def status_code(self):
return self.info["status"]
class Rest(object):
def __init__(self, module, headers):
self.module = module
self.headers = headers
self.baseurl = 'https://api.digitalocean.com/v2'
def _url_builder(self, path):
if path[0] == '/':
path = path[1:]
return '%s/%s' % (self.baseurl, path)
def send(self, method, path, data=None, headers=None):
url = self._url_builder(path)
data = self.module.jsonify(data)
timeout = self.module.params['timeout']
resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method, timeout=timeout)
# Exceptions in fetch_url may result in a status -1, the ensures a
if info['status'] == -1:
self.module.fail_json(msg=info['msg'])
return Response(resp, info)
def get(self, path, data=None, headers=None):
return self.send('GET', path, data, headers)
def put(self, path, data=None, headers=None):
return self.send('PUT', path, data, headers)
def post(self, path, data=None, headers=None):
return self.send('POST', path, data, headers)
def delete(self, path, data=None, headers=None):
return self.send('DELETE', path, data, headers)
def wait_action(module, rest, ip, action_id, timeout=10):
end_time = time.time() + 10
while time.time() < end_time:
response = rest.get('floating_ips/{0}/actions/{1}'.format(ip, action_id))
status_code = response.status_code
status = response.json['action']['status']
# TODO: check status_code == 200?
if status == 'completed':
return True
elif status == 'errored':
module.fail_json(msg='Floating ip action error [ip: {0}: action: {1}]'.format(
ip, action_id), data=json)
module.fail_json(msg='Floating ip action timeout [ip: {0}: action: {1}]'.format(
ip, action_id), data=json)
def core(module):
api_token = module.params['oauth_token']
state = module.params['state']
ip = module.params['ip']
droplet_id = module.params['droplet_id']
rest = Rest(module, {'Authorization': 'Bearer {0}'.format(api_token),
'Content-type': 'application/json'})
if state in ('present'):
if droplet_id is not None and module.params['ip'] is not None:
# Lets try to associate the ip to the specified droplet
associate_floating_ips(module, rest)
else:
create_floating_ips(module, rest)
elif state in ('absent'):
response = rest.delete("floating_ips/{0}".format(ip))
status_code = response.status_code
json_data = response.json
if status_code == 204:
module.exit_json(changed=True)
elif status_code == 404:
module.exit_json(changed=False)
else:
module.exit_json(changed=False, data=json_data)
def get_floating_ip_details(module, rest):
ip = module.params['ip']
response = rest.get("floating_ips/{0}".format(ip))
status_code = response.status_code
json_data = response.json
if status_code == 200:
return json_data['floating_ip']
else:
module.fail_json(msg="Error assigning floating ip [{0}: {1}]".format(
status_code, json_data["message"]), region=module.params['region'])
def assign_floating_id_to_droplet(module, rest):
ip = module.params['ip']
payload = {
"type": "assign",
"droplet_id": module.params['droplet_id'],
}
response = rest.post("floating_ips/{0}/actions".format(ip), data=payload)
status_code = response.status_code
json_data = response.json
if status_code == 201:
wait_action(module, rest, ip, json_data['action']['id'])
module.exit_json(changed=True, data=json_data)
else:
module.fail_json(msg="Error creating floating ip [{0}: {1}]".format(
status_code, json_data["message"]), region=module.params['region'])
def associate_floating_ips(module, rest):
floating_ip = get_floating_ip_details(module, rest)
droplet = floating_ip['droplet']
# TODO: If already assigned to a droplet verify if is one of the specified as valid
if droplet is not None and str(droplet['id']) in [module.params['droplet_id']]:
module.exit_json(changed=False)
else:
assign_floating_id_to_droplet(module, rest)
def create_floating_ips(module, rest):
payload = {
}
if module.params['region'] is not None:
payload["region"] = module.params['region']
if module.params['droplet_id'] is not None:
payload["droplet_id"] = module.params['droplet_id']
response = rest.post("floating_ips", data=payload)
status_code = response.status_code
json_data = response.json
if status_code == 202:
module.exit_json(changed=True, data=json_data)
else:
module.fail_json(msg="Error creating floating ip [{0}: {1}]".format(
status_code, json_data["message"]), region=module.params['region'])
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(choices=['present', 'absent'], default='present'),
ip=dict(aliases=['id'], required=False),
region=dict(required=False),
droplet_id=dict(required=False),
oauth_token=dict(
no_log=True,
# Support environment variable for DigitalOcean OAuth Token
fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN']),
required=True,
),
validate_certs=dict(type='bool', default=True),
timeout=dict(type='int', default=30),
),
required_if=[
('state', 'delete', ['ip'])
],
mutually_exclusive=[
['region', 'droplet_id']
],
)
core(module)
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_floating_ip_info.py

View file

@ -0,0 +1,124 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (C) 2017-18, 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 = {
'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'
}
DOCUMENTATION = '''
---
module: digital_ocean_floating_ip_info
short_description: DigitalOcean Floating IPs information
description:
- This module can be used to fetch DigitalOcean Floating IPs information.
- This module was called C(digital_ocean_floating_ip_facts) before Ansible 2.9. The usage did not change.
author: "Patrick Marques (@pmarques)"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
notes:
- Version 2 of DigitalOcean API is used.
requirements:
- "python >= 2.6"
'''
EXAMPLES = '''
- name: "Gather information about all Floating IPs"
digital_ocean_floating_ip_info:
register: result
- name: "List of current floating ips"
debug: var=result.floating_ips
'''
RETURN = '''
# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#floating-ips
floating_ips:
description: a DigitalOcean Floating IP resource
returned: success and no resource constraint
type: list
sample: [
{
"ip": "45.55.96.47",
"droplet": null,
"region": {
"name": "New York 3",
"slug": "nyc3",
"sizes": [
"512mb",
"1gb",
"2gb",
"4gb",
"8gb",
"16gb",
"32gb",
"48gb",
"64gb"
],
"features": [
"private_networking",
"backups",
"ipv6",
"metadata"
],
"available": true
},
"locked": false
}
]
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
rest = DigitalOceanHelper(module)
page = 1
has_next = True
floating_ips = []
status_code = None
while has_next or status_code != 200:
response = rest.get("floating_ips?page={0}&per_page=20".format(page))
status_code = response.status_code
# stop if any error during pagination
if status_code != 200:
break
page += 1
floating_ips.extend(response.json["floating_ips"])
has_next = "pages" in response.json["links"] and "next" in response.json["links"]["pages"]
if status_code == 200:
module.exit_json(changed=False, floating_ips=floating_ips)
else:
module.fail_json(msg="Error fetching information [{0}: {1}]".format(
status_code, response.json["message"]))
def main():
module = AnsibleModule(
argument_spec=DigitalOceanHelper.digital_ocean_argument_spec()
)
if module._name == 'digital_ocean_floating_ip_facts':
module.deprecate("The 'digital_ocean_floating_ip_facts' module has been renamed to 'digital_ocean_floating_ip_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e))
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_image_info.py

View file

@ -0,0 +1,154 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_image_info
short_description: Gather information about DigitalOcean images
description:
- This module can be used to gather information about DigitalOcean provided images.
- These images can be either of type C(distribution), C(application) and C(private).
- This module was called C(digital_ocean_image_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
options:
image_type:
description:
- Specifies the type of image information to be retrieved.
- If set to C(application), then information are gathered related to all application images.
- If set to C(distribution), then information are gathered related to all distribution images.
- If set to C(private), then information are gathered related to all private images.
- If not set to any of above, then information are gathered related to all images.
default: 'all'
choices: [ 'all', 'application', 'distribution', 'private' ]
required: false
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about all images
digital_ocean_image_info:
image_type: all
oauth_token: "{{ oauth_token }}"
- name: Gather information about application images
digital_ocean_image_info:
image_type: application
oauth_token: "{{ oauth_token }}"
- name: Gather information about distribution images
digital_ocean_image_info:
image_type: distribution
oauth_token: "{{ oauth_token }}"
- name: Get distribution about image with slug coreos-beta
digital_ocean_image_info:
register: resp_out
- set_fact:
distribution_name: "{{ item.distribution }}"
loop: "{{ resp_out.data|json_query(name) }}"
vars:
name: "[?slug=='coreos-beta']"
- debug: var=distribution_name
'''
RETURN = '''
data:
description: DigitalOcean image information
returned: success
type: list
sample: [
{
"created_at": "2018-02-02T07:11:43Z",
"distribution": "CoreOS",
"id": 31434061,
"min_disk_size": 20,
"name": "1662.1.0 (beta)",
"public": true,
"regions": [
"nyc1",
"sfo1",
"nyc2",
"ams2",
"sgp1",
"lon1",
"nyc3",
"ams3",
"fra1",
"tor1",
"sfo2",
"blr1"
],
"size_gigabytes": 0.42,
"slug": "coreos-beta",
"type": "snapshot"
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
image_type = module.params['image_type']
rest = DigitalOceanHelper(module)
base_url = 'images?'
if image_type == 'distribution':
base_url += "type=distribution&"
elif image_type == 'application':
base_url += "type=application&"
elif image_type == 'private':
base_url += "private=true&"
images = rest.get_paginated_data(base_url=base_url, data_key_name='images')
module.exit_json(changed=False, data=images)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
image_type=dict(type='str',
required=False,
choices=['all', 'application', 'distribution', 'private'],
default='all'
)
)
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_image_facts':
module.deprecate("The 'digital_ocean_image_facts' module has been renamed to 'digital_ocean_image_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_load_balancer_info.py

View file

@ -0,0 +1,121 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_load_balancer_info
short_description: Gather information about DigitalOcean load balancers
description:
- This module can be used to gather information about DigitalOcean provided load balancers.
- This module was called C(digital_ocean_load_balancer_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
options:
load_balancer_id:
description:
- Load balancer ID that can be used to identify and reference a load_balancer.
required: false
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about all load balancers
digital_ocean_load_balancer_info:
oauth_token: "{{ oauth_token }}"
- name: Gather information about load balancer with given id
digital_ocean_load_balancer_info:
oauth_token: "{{ oauth_token }}"
load_balancer_id: "4de7ac8b-495b-4884-9a69-1050c6793cd6"
- name: Get name from load balancer id
digital_ocean_load_balancer_info:
register: resp_out
- set_fact:
load_balancer_name: "{{ item.name }}"
loop: "{{ resp_out.data|json_query(name) }}"
vars:
name: "[?id=='4de7ac8b-495b-4884-9a69-1050c6793cd6']"
- debug: var=load_balancer_name
'''
RETURN = '''
data:
description: DigitalOcean Load balancer information
returned: success
type: list
sample: [
{
"id": "4de7ac8b-495b-4884-9a69-1050c6793cd6",
"name": "example-lb-01",
"ip": "104.131.186.241",
"algorithm": "round_robin",
"status": "new",
"created_at": "2017-02-01T22:22:58Z",
...
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
load_balancer_id = module.params.get('load_balancer_id', None)
rest = DigitalOceanHelper(module)
base_url = 'load_balancers?'
if load_balancer_id is not None:
response = rest.get("%s/%s" % (base_url, load_balancer_id))
status_code = response.status_code
if status_code != 200:
module.fail_json(msg="Failed to retrieve load balancers for DigitalOcean")
resp_json = response.json
load_balancer = resp_json['load_balancer']
else:
load_balancer = rest.get_paginated_data(base_url=base_url, data_key_name='load_balancers')
module.exit_json(changed=False, data=load_balancer)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
argument_spec.update(
load_balancer_id=dict(type='str', required=False),
)
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_load_balancer_facts':
module.deprecate("The 'digital_ocean_load_balancer_facts' module has been renamed to 'digital_ocean_load_balancer_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_region_info.py

View file

@ -0,0 +1,121 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_region_info
short_description: Gather information about DigitalOcean regions
description:
- This module can be used to gather information about regions.
- This module was called C(digital_ocean_region_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
requirements:
- "python >= 2.6"
'''
EXAMPLES = '''
- name: Gather information about all regions
digital_ocean_region_info:
oauth_token: "{{ oauth_token }}"
- name: Get Name of region where slug is known
digital_ocean_region_info:
oauth_token: "{{ oauth_token }}"
register: resp_out
- debug: var=resp_out
- set_fact:
region_slug: "{{ item.name }}"
loop: "{{ resp_out.data|json_query(name) }}"
vars:
name: "[?slug==`nyc1`]"
- debug: var=region_slug
'''
RETURN = '''
data:
description: DigitalOcean regions information
returned: success
type: list
sample: [
{
"available": true,
"features": [
"private_networking",
"backups",
"ipv6",
"metadata",
"install_agent",
"storage"
],
"name": "New York 1",
"sizes": [
"512mb",
"s-1vcpu-1gb",
"1gb",
"s-3vcpu-1gb",
"s-1vcpu-2gb",
"s-2vcpu-2gb",
"2gb",
"s-1vcpu-3gb",
"s-2vcpu-4gb",
"4gb",
"c-2",
"m-1vcpu-8gb",
"8gb",
"s-4vcpu-8gb",
"s-6vcpu-16gb",
"16gb"
],
"slug": "nyc1"
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
rest = DigitalOceanHelper(module)
base_url = 'regions?'
regions = rest.get_paginated_data(base_url=base_url, data_key_name='regions')
module.exit_json(changed=False, data=regions)
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
module = AnsibleModule(argument_spec=argument_spec)
if module._name == 'digital_ocean_region_facts':
module.deprecate("The 'digital_ocean_region_facts' module has been renamed to 'digital_ocean_region_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
digital_ocean_size_info.py

View file

@ -0,0 +1,119 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.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: digital_ocean_size_info
short_description: Gather information about DigitalOcean Droplet sizes
description:
- This module can be used to gather information about droplet sizes.
- This module was called C(digital_ocean_size_facts) before Ansible 2.9. The usage did not change.
author: "Abhijeet Kasurde (@Akasurde)"
requirements:
- "python >= 2.6"
extends_documentation_fragment:
- community.general.digital_ocean.documentation
'''
EXAMPLES = '''
- name: Gather information about all droplet sizes
digital_ocean_size_info:
oauth_token: "{{ oauth_token }}"
- name: Get droplet Size Slug where vcpus is 1
digital_ocean_size_info:
oauth_token: "{{ oauth_token }}"
register: resp_out
- debug: var=resp_out
- set_fact:
size_slug: "{{ item.slug }}"
loop: "{{ resp_out.data|json_query(name) }}"
vars:
name: "[?vcpus==`1`]"
- debug: var=size_slug
'''
RETURN = '''
data:
description: DigitalOcean droplet size information
returned: success
type: list
sample: [
{
"available": true,
"disk": 20,
"memory": 512,
"price_hourly": 0.00744,
"price_monthly": 5.0,
"regions": [
"ams2",
"ams3",
"blr1",
"fra1",
"lon1",
"nyc1",
"nyc2",
"nyc3",
"sfo1",
"sfo2",
"sgp1",
"tor1"
],
"slug": "512mb",
"transfer": 1.0,
"vcpus": 1
},
]
'''
from traceback import format_exc
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.digital_ocean import DigitalOceanHelper
from ansible.module_utils._text import to_native
def core(module):
rest = DigitalOceanHelper(module)
response = rest.get('sizes')
if response.status_code != 200:
module.fail_json(msg="Failed to fetch 'sizes' information due to error : %s" % response.json['message'])
module.exit_json(changed=False, data=response.json['sizes'])
def main():
argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
module = AnsibleModule(
argument_spec=argument_spec,
)
if module._name == 'digital_ocean_size_facts':
module.deprecate("The 'digital_ocean_size_facts' module has been renamed to 'digital_ocean_size_info'", version='2.13')
try:
core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=format_exc())
if __name__ == '__main__':
main()

Some files were not shown because too many files have changed in this diff Show more