mirror of https://github.com/ansible/ansible.git
merge devel
commit
3db78457ce
@ -0,0 +1,16 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- deadsnakes
|
||||
packages:
|
||||
- python2.4
|
||||
- python2.6
|
||||
script:
|
||||
- python2.4 -m compileall -fq -x 'cloud/' .
|
||||
- python2.4 -m compileall -fq cloud/amazon/_ec2_ami_search.py cloud/amazon/ec2_facts.py
|
||||
- python2.6 -m compileall -fq .
|
||||
- python2.7 -m compileall -fq .
|
@ -0,0 +1,302 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ec2_ami_find
|
||||
version_added: 2.0
|
||||
short_description: Searches for AMIs to obtain the AMI ID and other information
|
||||
description:
|
||||
- Returns list of matching AMIs with AMI ID, along with other useful information
|
||||
- Can search AMIs with different owners
|
||||
- Can search by matching tag(s), by AMI name and/or other criteria
|
||||
- Results can be sorted and sliced
|
||||
author: "Tom Bamford (@tombamford)"
|
||||
notes:
|
||||
- This module is not backwards compatible with the previous version of the ec2_search_ami module which worked only for Ubuntu AMIs listed on cloud-images.ubuntu.com.
|
||||
- See the example below for a suggestion of how to search by distro/release.
|
||||
options:
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use.
|
||||
required: true
|
||||
aliases: [ 'aws_region', 'ec2_region' ]
|
||||
owner:
|
||||
description:
|
||||
- Search AMIs owned by the specified owner
|
||||
- Can specify an AWS account ID, or one of the special IDs 'self', 'amazon' or 'aws-marketplace'
|
||||
- If not specified, all EC2 AMIs in the specified region will be searched.
|
||||
- You can include wildcards in many of the search options. An asterisk (*) matches zero or more characters, and a question mark (?) matches exactly one character. You can escape special characters using a backslash (\) before the character. For example, a value of \*amazon\?\\ searches for the literal string *amazon?\.
|
||||
required: false
|
||||
default: null
|
||||
ami_id:
|
||||
description:
|
||||
- An AMI ID to match.
|
||||
default: null
|
||||
required: false
|
||||
ami_tags:
|
||||
description:
|
||||
- A hash/dictionary of tags to match for the AMI.
|
||||
default: null
|
||||
required: false
|
||||
architecture:
|
||||
description:
|
||||
- An architecture type to match (e.g. x86_64).
|
||||
default: null
|
||||
required: false
|
||||
hypervisor:
|
||||
description:
|
||||
- A hypervisor type type to match (e.g. xen).
|
||||
default: null
|
||||
required: false
|
||||
is_public:
|
||||
description:
|
||||
- Whether or not the image(s) are public.
|
||||
choices: ['yes', 'no']
|
||||
default: null
|
||||
required: false
|
||||
name:
|
||||
description:
|
||||
- An AMI name to match.
|
||||
default: null
|
||||
required: false
|
||||
platform:
|
||||
description:
|
||||
- Platform type to match.
|
||||
default: null
|
||||
required: false
|
||||
sort:
|
||||
description:
|
||||
- Optional attribute which with to sort the results.
|
||||
- If specifying 'tag', the 'tag_name' parameter is required.
|
||||
choices: ['name', 'description', 'tag']
|
||||
default: null
|
||||
required: false
|
||||
sort_tag:
|
||||
description:
|
||||
- Tag name with which to sort results.
|
||||
- Required when specifying 'sort=tag'.
|
||||
default: null
|
||||
required: false
|
||||
sort_order:
|
||||
description:
|
||||
- Order in which to sort results.
|
||||
- Only used when the 'sort' parameter is specified.
|
||||
choices: ['ascending', 'descending']
|
||||
default: 'ascending'
|
||||
required: false
|
||||
sort_start:
|
||||
description:
|
||||
- Which result to start with (when sorting).
|
||||
- Corresponds to Python slice notation.
|
||||
default: null
|
||||
required: false
|
||||
sort_end:
|
||||
description:
|
||||
- Which result to end with (when sorting).
|
||||
- Corresponds to Python slice notation.
|
||||
default: null
|
||||
required: false
|
||||
state:
|
||||
description:
|
||||
- AMI state to match.
|
||||
default: 'available'
|
||||
required: false
|
||||
virtualization_type:
|
||||
description:
|
||||
- Virtualization type to match (e.g. hvm).
|
||||
default: null
|
||||
required: false
|
||||
no_result_action:
|
||||
description:
|
||||
- What to do when no results are found.
|
||||
- "'success' reports success and returns an empty array"
|
||||
- "'fail' causes the module to report failure"
|
||||
choices: ['success', 'fail']
|
||||
default: 'success'
|
||||
required: false
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- boto
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Note: These examples do not set authentication details, see the AWS Guide for details.
|
||||
|
||||
# Search for the AMI tagged "project:website"
|
||||
- ec2_ami_find:
|
||||
owner: self
|
||||
ami_tags:
|
||||
project: website
|
||||
no_result_action: fail
|
||||
register: ami_find
|
||||
|
||||
# Search for the latest Ubuntu 14.04 AMI
|
||||
- ec2_ami_find:
|
||||
name: "ubuntu/images/ebs/ubuntu-trusty-14.04-amd64-server-*"
|
||||
owner: 099720109477
|
||||
sort: name
|
||||
sort_order: descending
|
||||
sort_end: 1
|
||||
register: ami_find
|
||||
|
||||
# Launch an EC2 instance
|
||||
- ec2:
|
||||
image: "{{ ami_find.results[0].ami_id }}"
|
||||
instance_type: m3.medium
|
||||
key_name: mykey
|
||||
wait: yes
|
||||
'''
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
HAS_BOTO=True
|
||||
except ImportError:
|
||||
HAS_BOTO=False
|
||||
|
||||
import json
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
region = dict(required=True,
|
||||
aliases = ['aws_region', 'ec2_region']),
|
||||
owner = dict(required=False, default=None),
|
||||
ami_id = dict(required=False),
|
||||
ami_tags = dict(required=False, type='dict',
|
||||
aliases = ['search_tags', 'image_tags']),
|
||||
architecture = dict(required=False),
|
||||
hypervisor = dict(required=False),
|
||||
is_public = dict(required=False),
|
||||
name = dict(required=False),
|
||||
platform = dict(required=False),
|
||||
sort = dict(required=False, default=None,
|
||||
choices=['name', 'description', 'tag']),
|
||||
sort_tag = dict(required=False),
|
||||
sort_order = dict(required=False, default='ascending',
|
||||
choices=['ascending', 'descending']),
|
||||
sort_start = dict(required=False),
|
||||
sort_end = dict(required=False),
|
||||
state = dict(required=False, default='available'),
|
||||
virtualization_type = dict(required=False),
|
||||
no_result_action = dict(required=False, default='success',
|
||||
choices = ['success', 'fail']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
if not HAS_BOTO:
|
||||
module.fail_json(msg='boto required for this module, install via pip or your package manager')
|
||||
|
||||
ami_id = module.params.get('ami_id')
|
||||
ami_tags = module.params.get('ami_tags')
|
||||
architecture = module.params.get('architecture')
|
||||
hypervisor = module.params.get('hypervisor')
|
||||
is_public = module.params.get('is_public')
|
||||
name = module.params.get('name')
|
||||
owner = module.params.get('owner')
|
||||
platform = module.params.get('platform')
|
||||
sort = module.params.get('sort')
|
||||
sort_tag = module.params.get('sort_tag')
|
||||
sort_order = module.params.get('sort_order')
|
||||
sort_start = module.params.get('sort_start')
|
||||
sort_end = module.params.get('sort_end')
|
||||
state = module.params.get('state')
|
||||
virtualization_type = module.params.get('virtualization_type')
|
||||
no_result_action = module.params.get('no_result_action')
|
||||
|
||||
filter = {'state': state}
|
||||
|
||||
if ami_id:
|
||||
filter['image_id'] = ami_id
|
||||
if ami_tags:
|
||||
for tag in ami_tags:
|
||||
filter['tag:'+tag] = ami_tags[tag]
|
||||
if architecture:
|
||||
filter['architecture'] = architecture
|
||||
if hypervisor:
|
||||
filter['hypervisor'] = hypervisor
|
||||
if is_public:
|
||||
filter['is_public'] = is_public
|
||||
if name:
|
||||
filter['name'] = name
|
||||
if platform:
|
||||
filter['platform'] = platform
|
||||
if virtualization_type:
|
||||
filter['virtualization_type'] = virtualization_type
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
images_result = ec2.get_all_images(owners=owner, filters=filter)
|
||||
|
||||
if no_result_action == 'fail' and len(images_result) == 0:
|
||||
module.fail_json(msg="No AMIs matched the attributes: %s" % json.dumps(filter))
|
||||
|
||||
results = []
|
||||
for image in images_result:
|
||||
data = {
|
||||
'ami_id': image.id,
|
||||
'architecture': image.architecture,
|
||||
'description': image.description,
|
||||
'is_public': image.is_public,
|
||||
'name': image.name,
|
||||
'owner_id': image.owner_id,
|
||||
'platform': image.platform,
|
||||
'root_device_name': image.root_device_name,
|
||||
'root_device_type': image.root_device_type,
|
||||
'state': image.state,
|
||||
'tags': image.tags,
|
||||
'virtualization_type': image.virtualization_type,
|
||||
}
|
||||
|
||||
if image.kernel_id:
|
||||
data['kernel_id'] = image.kernel_id
|
||||
if image.ramdisk_id:
|
||||
data['ramdisk_id'] = image.ramdisk_id
|
||||
|
||||
results.append(data)
|
||||
|
||||
if sort == 'tag':
|
||||
if not sort_tag:
|
||||
module.fail_json(msg="'sort_tag' option must be given with 'sort=tag'")
|
||||
results.sort(key=lambda e: e['tags'][sort_tag], reverse=(sort_order=='descending'))
|
||||
elif sort:
|
||||
results.sort(key=lambda e: e[sort], reverse=(sort_order=='descending'))
|
||||
|
||||
try:
|
||||
if sort and sort_start and sort_end:
|
||||
results = results[int(sort_start):int(sort_end)]
|
||||
elif sort and sort_start:
|
||||
results = results[int(sort_start):]
|
||||
elif sort and sort_end:
|
||||
results = results[:int(sort_end)]
|
||||
except TypeError:
|
||||
module.fail_json(msg="Please supply numeric values for sort_start and/or sort_end")
|
||||
|
||||
module.exit_json(results=results)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -0,0 +1,157 @@
|
||||
#!/usr/bin/python
|
||||
# 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/>.
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: elasticache_subnet_group
|
||||
version_added: "2.0"
|
||||
short_description: manage Elasticache subnet groups
|
||||
description:
|
||||
- Creates, modifies, and deletes Elasticache subnet groups. This module has a dependency on python-boto >= 2.5.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Specifies whether the subnet should be present or absent.
|
||||
required: true
|
||||
default: present
|
||||
choices: [ 'present' , 'absent' ]
|
||||
name:
|
||||
description:
|
||||
- Database subnet group identifier.
|
||||
required: true
|
||||
description:
|
||||
description:
|
||||
- Elasticache subnet group description. Only set when a new group is added.
|
||||
required: false
|
||||
default: null
|
||||
subnets:
|
||||
description:
|
||||
- List of subnet IDs that make up the Elasticache subnet group.
|
||||
required: false
|
||||
default: null
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the AWS_REGION or EC2_REGION environment variable, if any, is used.
|
||||
required: true
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
author: "Tim Mahoney (@timmahoney)"
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add or change a subnet group
|
||||
- elasticache_subnet_group
|
||||
state: present
|
||||
name: norwegian-blue
|
||||
description: My Fancy Ex Parrot Subnet Group
|
||||
subnets:
|
||||
- subnet-aaaaaaaa
|
||||
- subnet-bbbbbbbb
|
||||
|
||||
# Remove a subnet group
|
||||
- elasticache_subnet_group:
|
||||
state: absent
|
||||
name: norwegian-blue
|
||||
'''
|
||||
|
||||
try:
|
||||
import boto
|
||||
from boto.elasticache.layer1 import ElastiCacheConnection
|
||||
from boto.regioninfo import RegionInfo
|
||||
from boto.exception import BotoServerError
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state = dict(required=True, choices=['present', 'absent']),
|
||||
name = dict(required=True),
|
||||
description = dict(required=False),
|
||||
subnets = dict(required=False, type='list'),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
if not HAS_BOTO:
|
||||
module.fail_json(msg='boto required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
group_name = module.params.get('name').lower()
|
||||
group_description = module.params.get('description')
|
||||
group_subnets = module.params.get('subnets') or {}
|
||||
|
||||
if state == 'present':
|
||||
for required in ['name', 'description', 'subnets']:
|
||||
if not module.params.get(required):
|
||||
module.fail_json(msg = str("Parameter %s required for state='present'" % required))
|
||||
else:
|
||||
for not_allowed in ['description', 'subnets']:
|
||||
if module.params.get(not_allowed):
|
||||
module.fail_json(msg = str("Parameter %s not allowed for state='absent'" % not_allowed))
|
||||
|
||||
# Retrieve any AWS settings from the environment.
|
||||
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
|
||||
|
||||
if not region:
|
||||
module.fail_json(msg = str("Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set."))
|
||||
|
||||
|
||||
"""Get an elasticache connection"""
|
||||
try:
|
||||
endpoint = "elasticache.%s.amazonaws.com" % region
|
||||
connect_region = RegionInfo(name=region, endpoint=endpoint)
|
||||
conn = ElastiCacheConnection(region=connect_region, **aws_connect_kwargs)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
try:
|
||||
changed = False
|
||||
exists = False
|
||||
|
||||
try:
|
||||
matching_groups = conn.describe_cache_subnet_groups(group_name, max_records=100)
|
||||
exists = len(matching_groups) > 0
|
||||
except BotoServerError, e:
|
||||
if e.error_code != 'CacheSubnetGroupNotFoundFault':
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
if state == 'absent':
|
||||
if exists:
|
||||
conn.delete_cache_subnet_group(group_name)
|
||||
changed = True
|
||||
else:
|
||||
if not exists:
|
||||
new_group = conn.create_cache_subnet_group(group_name, cache_subnet_group_description=group_description, subnet_ids=group_subnets)
|
||||
changed = True
|
||||
else:
|
||||
changed_group = conn.modify_cache_subnet_group(group_name, cache_subnet_group_description=group_description, subnet_ids=group_subnets)
|
||||
changed = True
|
||||
|
||||
except BotoServerError, e:
|
||||
if e.error_message != 'No modifications were requested.':
|
||||
module.fail_json(msg = e.error_message)
|
||||
else:
|
||||
changed = False
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -0,0 +1,714 @@
|
||||
#!/usr/bin/python
|
||||
# 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/>.
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: iam
|
||||
short_description: Manage IAM users, groups, roles and keys
|
||||
description:
|
||||
- Allows for the management of IAM users, groups, roles and access keys.
|
||||
version_added: "2.0"
|
||||
options:
|
||||
iam_type:
|
||||
description:
|
||||
- Type of IAM resource
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "user", "group", "role"]
|
||||
name:
|
||||
description:
|
||||
- Name of IAM resource to create or identify
|
||||
required: true
|
||||
new_name:
|
||||
description:
|
||||
- When state is update, will replace name with new_name on IAM resource
|
||||
required: false
|
||||
default: null
|
||||
new_path:
|
||||
description:
|
||||
- When state is update, will replace the path with new_path on the IAM resource
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Whether to create, delete or update the IAM resource. Note, roles cannot be updated.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "present", "absent", "update" ]
|
||||
path:
|
||||
description:
|
||||
- When creating or updating, specify the desired path of the resource. If state is present, it will replace the current path to match what is passed in when they do not match.
|
||||
required: false
|
||||
default: "/"
|
||||
access_key_state:
|
||||
description:
|
||||
- When type is user, it creates, removes, deactivates or activates a user's access key(s). Note that actions apply only to keys specified.
|
||||
required: false
|
||||
default: null
|
||||
choices: [ "create", "remove", "active", "inactive"]
|
||||
key_count:
|
||||
description:
|
||||
- When access_key_state is create it will ensure this quantity of keys are present. Defaults to 1.
|
||||
required: false
|
||||
default: '1'
|
||||
access_key_ids:
|
||||
description:
|
||||
- A list of the keys that you want impacted by the access_key_state paramter.
|
||||
groups:
|
||||
description:
|
||||
- A list of groups the user should belong to. When update, will gracefully remove groups not listed.
|
||||
required: false
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- When type is user and state is present, define the users login password. Also works with update. Note that always returns changed.
|
||||
required: false
|
||||
default: null
|
||||
update_password:
|
||||
required: false
|
||||
default: always
|
||||
choices: ['always', 'on_create']
|
||||
description:
|
||||
- C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users.
|
||||
aws_secret_key:
|
||||
description:
|
||||
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'ec2_secret_key', 'secret_key' ]
|
||||
aws_access_key:
|
||||
description:
|
||||
- AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'ec2_access_key', 'access_key' ]
|
||||
notes:
|
||||
- 'Currently boto does not support the removal of Managed Policies, the module will error out if your user/group/role has managed policies when you try to do state=absent. They will need to be removed manually.'
|
||||
author:
|
||||
- "Jonathan I. Davila (@defionscode)"
|
||||
- "Paul Seiffert (@seiffert)"
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic user creation example
|
||||
tasks:
|
||||
- name: Create two new IAM users with API keys
|
||||
iam:
|
||||
iam_type: user
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
password: "{{ temp_pass }}"
|
||||
access_key_state: create
|
||||
with_items:
|
||||
- jcleese
|
||||
- mpython
|
||||
|
||||
# Advanced example, create two new groups and add the pre-existing user
|
||||
# jdavila to both groups.
|
||||
task:
|
||||
- name: Create Two Groups, Mario and Luigi
|
||||
iam:
|
||||
iam_type: group
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- Mario
|
||||
- Luigi
|
||||
register: new_groups
|
||||
|
||||
- name:
|
||||
iam:
|
||||
iam_type: user
|
||||
name: jdavila
|
||||
state: update
|
||||
groups: "{{ item.created_group.group_name }}"
|
||||
with_items: new_groups.results
|
||||
|
||||
'''
|
||||
|
||||
import json
|
||||
import itertools
|
||||
import sys
|
||||
try:
|
||||
import boto
|
||||
import boto.iam
|
||||
import boto.ec2
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
||||
def boto_exception(err):
|
||||
'''generic error message handler'''
|
||||
if hasattr(err, 'error_message'):
|
||||
error = err.error_message
|
||||
elif hasattr(err, 'message'):
|
||||
error = err.message
|
||||
else:
|
||||
error = '%s: %s' % (Exception, err)
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def create_user(module, iam, name, pwd, path, key_state, key_count):
|
||||
key_qty = 0
|
||||
keys = []
|
||||
try:
|
||||
user_meta = iam.create_user(
|
||||
name, path).create_user_response.create_user_result.user
|
||||
changed = True
|
||||
if pwd is not None:
|
||||
pwd = iam.create_login_profile(name, pwd)
|
||||
if key_state in ['create']:
|
||||
if key_count:
|
||||
while key_count > key_qty:
|
||||
keys.append(iam.create_access_key(
|
||||
user_name=name).create_access_key_response.\
|
||||
create_access_key_result.\
|
||||
access_key)
|
||||
key_qty += 1
|
||||
else:
|
||||
keys = None
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=False, msg=str(err))
|
||||
else:
|
||||
user_info = dict(created_user=user_meta, password=pwd, access_keys=keys)
|
||||
return (user_info, changed)
|
||||
|
||||
|
||||
def delete_user(module, iam, name):
|
||||
try:
|
||||
current_keys = [ck['access_key_id'] for ck in
|
||||
iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata]
|
||||
for key in current_keys:
|
||||
iam.delete_access_key(key, name)
|
||||
del_meta = iam.delete_user(name).delete_user_response
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('must detach all policies first') in error_msg:
|
||||
for policy in iam.get_all_user_policies(name).list_user_policies_result.policy_names:
|
||||
iam.delete_user_policy(name, policy)
|
||||
try:
|
||||
del_meta = iam.delete_user(name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('must detach all policies first') in error_msg:
|
||||
module.fail_json(changed=changed, msg="All inline polices have been removed. Though it appears"
|
||||
"that %s has Managed Polices. This is not "
|
||||
"currently supported by boto. Please detach the polices "
|
||||
"through the console and try again." % name)
|
||||
else:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
changed = True
|
||||
return del_meta, name, changed
|
||||
else:
|
||||
changed = True
|
||||
return del_meta, name, changed
|
||||
|
||||
|
||||
def update_user(module, iam, name, new_name, new_path, key_state, key_count, keys, pwd, updated):
|
||||
changed = False
|
||||
name_change = False
|
||||
if updated and new_name:
|
||||
name = new_name
|
||||
try:
|
||||
current_keys, status = \
|
||||
[ck['access_key_id'] for ck in
|
||||
iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata],\
|
||||
[ck['status'] for ck in
|
||||
iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata]
|
||||
key_qty = len(current_keys)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'cannot be found' in error_msg and updated:
|
||||
current_keys, status = \
|
||||
[ck['access_key_id'] for ck in
|
||||
iam.get_all_access_keys(new_name).list_access_keys_result.access_key_metadata],\
|
||||
[ck['status'] for ck in
|
||||
iam.get_all_access_keys(new_name).list_access_keys_result.access_key_metadata]
|
||||
name = new_name
|
||||
else:
|
||||
module.fail_json(changed=False, msg=str(err))
|
||||
|
||||
updated_key_list = {}
|
||||
|
||||
if new_name or new_path:
|
||||
c_path = iam.get_user(name).get_user_result.user['path']
|
||||
if (name != new_name) or (c_path != new_path):
|
||||
changed = True
|
||||
try:
|
||||
if not updated:
|
||||
user = iam.update_user(
|
||||
name, new_user_name=new_name, new_path=new_path).update_user_response.response_metadata
|
||||
else:
|
||||
user = iam.update_user(
|
||||
name, new_path=new_path).update_user_response.response_metadata
|
||||
user['updates'] = dict(
|
||||
old_username=name, new_username=new_name, old_path=c_path, new_path=new_path)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
module.fail_json(changed=False, msg=str(err))
|
||||
else:
|
||||
if not updated:
|
||||
name_change = True
|
||||
|
||||
if pwd:
|
||||
try:
|
||||
iam.update_login_profile(name, pwd)
|
||||
changed = True
|
||||
except boto.exception.BotoServerError:
|
||||
try:
|
||||
iam.create_login_profile(name, pwd)
|
||||
changed = True
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(str(err))
|
||||
if 'Password does not conform to the account password policy' in error_msg:
|
||||
module.fail_json(changed=False, msg="Passsword doesn't conform to policy")
|
||||
else:
|
||||
module.fail_json(msg=error_msg)
|
||||
else:
|
||||
try:
|
||||
iam.delete_login_profile(name)
|
||||
changed = True
|
||||
except boto.exception.BotoServerError:
|
||||
pass
|
||||
|
||||
if key_state == 'create':
|
||||
try:
|
||||
while key_count > key_qty:
|
||||
new_key = iam.create_access_key(
|
||||
user_name=name).create_access_key_response.create_access_key_result.access_key
|
||||
key_qty += 1
|
||||
changed = True
|
||||
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=False, msg=str(err))
|
||||
|
||||
if keys and key_state:
|
||||
for access_key in keys:
|
||||
if access_key in current_keys:
|
||||
for current_key, current_key_state in zip(current_keys, status):
|
||||
if key_state != current_key_state.lower():
|
||||
try:
|
||||
iam.update_access_key(
|
||||
access_key, key_state.capitalize(), user_name=name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=False, msg=str(err))
|
||||
else:
|
||||
changed = True
|
||||
|
||||
if key_state == 'remove':
|
||||
try:
|
||||
iam.delete_access_key(access_key, user_name=name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=False, msg=str(err))
|
||||
else:
|
||||
changed = True
|
||||
|
||||
try:
|
||||
final_keys, final_key_status = \
|
||||
[ck['access_key_id'] for ck in
|
||||
iam.get_all_access_keys(name).
|
||||
list_access_keys_result.
|
||||
access_key_metadata],\
|
||||
[ck['status'] for ck in
|
||||
iam.get_all_access_keys(name).
|
||||
list_access_keys_result.
|
||||
access_key_metadata]
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
|
||||
for fk, fks in zip(final_keys, final_key_status):
|
||||
updated_key_list.update({fk: fks})
|
||||
|
||||
return name_change, updated_key_list, changed
|
||||
|
||||
|
||||
def set_users_groups(module, iam, name, groups, updated=None,
|
||||
new_name=None):
|
||||
""" Sets groups for a user, will purge groups not explictly passed, while
|
||||
retaining pre-existing groups that also are in the new list.
|
||||
"""
|
||||
changed = False
|
||||
|
||||
if updated:
|
||||
name = new_name
|
||||
|
||||
try:
|
||||
orig_users_groups = [og['group_name'] for og in iam.get_groups_for_user(
|
||||
name).list_groups_for_user_result.groups]
|
||||
remove_groups = [
|
||||
rg for rg in frozenset(orig_users_groups).difference(groups)]
|
||||
new_groups = [
|
||||
ng for ng in frozenset(groups).difference(orig_users_groups)]
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
if len(orig_users_groups) > 0:
|
||||
for new in new_groups:
|
||||
iam.add_user_to_group(new, name)
|
||||
for rm in remove_groups:
|
||||
iam.remove_user_from_group(rm, name)
|
||||
else:
|
||||
for group in groups:
|
||||
try:
|
||||
iam.add_user_to_group(group, name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('The group with name %s cannot be found.' % group) in error_msg:
|
||||
module.fail_json(changed=False, msg="Group %s doesn't exist" % group)
|
||||
|
||||
|
||||
if len(remove_groups) > 0 or len(new_groups) > 0:
|
||||
changed = True
|
||||
|
||||
return (groups, changed)
|
||||
|
||||
|
||||
def create_group(module=None, iam=None, name=None, path=None):
|
||||
changed = False
|
||||
try:
|
||||
iam.create_group(
|
||||
name, path).create_group_response.create_group_result.group
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
changed = True
|
||||
return name, changed
|
||||
|
||||
|
||||
def delete_group(module=None, iam=None, name=None):
|
||||
changed = False
|
||||
try:
|
||||
iam.delete_group(name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('must detach all policies first') in error_msg:
|
||||
for policy in iam.get_all_group_policies(name).list_group_policies_result.policy_names:
|
||||
iam.delete_group_policy(name, policy)
|
||||
try:
|
||||
iam.delete_group(name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('must detach all policies first') in error_msg:
|
||||
module.fail_json(changed=changed, msg="All inline polices have been removed. Though it appears"
|
||||
"that %s has Managed Polices. This is not "
|
||||
"currently supported by boto. Please detach the polices "
|
||||
"through the console and try again." % name)
|
||||
else:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
changed = True
|
||||
else:
|
||||
changed = True
|
||||
return changed, name
|
||||
|
||||
def update_group(module=None, iam=None, name=None, new_name=None, new_path=None):
|
||||
changed = False
|
||||
try:
|
||||
current_group_path = iam.get_group(
|
||||
name).get_group_response.get_group_result.group['path']
|
||||
if new_path:
|
||||
if current_group_path != new_path:
|
||||
iam.update_group(name, new_path=new_path)
|
||||
changed = True
|
||||
if new_name:
|
||||
if name != new_name:
|
||||
iam.update_group(name, new_group_name=new_name, new_path=new_path)
|
||||
changed = True
|
||||
name = new_name
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
|
||||
return changed, name, new_path, current_group_path
|
||||
|
||||
|
||||
def create_role(module, iam, name, path, role_list, prof_list):
|
||||
changed = False
|
||||
try:
|
||||
if name not in role_list:
|
||||
changed = True
|
||||
iam.create_role(
|
||||
name, path=path).create_role_response.create_role_result.role.role_name
|
||||
|
||||
if name not in prof_list:
|
||||
iam.create_instance_profile(name, path=path)
|
||||
iam.add_role_to_instance_profile(name, name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
updated_role_list = [rl['role_name'] for rl in iam.list_roles().list_roles_response.
|
||||
list_roles_result.roles]
|
||||
return changed, updated_role_list
|
||||
|
||||
|
||||
def delete_role(module, iam, name, role_list, prof_list):
|
||||
changed = False
|
||||
try:
|
||||
if name in role_list:
|
||||
cur_ins_prof = [rp['instance_profile_name'] for rp in
|
||||
iam.list_instance_profiles_for_role(name).
|
||||
list_instance_profiles_for_role_result.
|
||||
instance_profiles]
|
||||
for profile in cur_ins_prof:
|
||||
iam.remove_role_from_instance_profile(profile, name)
|
||||
try:
|
||||
iam.delete_role(name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('must detach all policies first') in error_msg:
|
||||
for policy in iam.list_role_policies(name).list_role_policies_result.policy_names:
|
||||
iam.delete_role_policy(name, policy)
|
||||
try:
|
||||
iam.delete_role(name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if ('must detach all policies first') in error_msg:
|
||||
module.fail_json(changed=changed, msg="All inline polices have been removed. Though it appears"
|
||||
"that %s has Managed Polices. This is not "
|
||||
"currently supported by boto. Please detach the polices "
|
||||
"through the console and try again." % name)
|
||||
else:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
changed = True
|
||||
|
||||
else:
|
||||
changed = True
|
||||
|
||||
for prof in prof_list:
|
||||
if name == prof:
|
||||
iam.delete_instance_profile(name)
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err))
|
||||
else:
|
||||
updated_role_list = [rl['role_name'] for rl in iam.list_roles().list_roles_response.
|
||||
list_roles_result.roles]
|
||||
return changed, updated_role_list
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
iam_type=dict(
|
||||
default=None, required=True, choices=['user', 'group', 'role']),
|
||||
groups=dict(type='list', default=None, required=False),
|
||||
state=dict(
|
||||
default=None, required=True, choices=['present', 'absent', 'update']),
|
||||
password=dict(default=None, required=False),
|
||||
update_password=dict(default='always', required=False, choices=['always', 'on_create']),
|
||||
access_key_state=dict(default=None, required=False, choices=[
|
||||
'active', 'inactive', 'create', 'remove',
|
||||
'Active', 'Inactive', 'Create', 'Remove']),
|
||||
access_key_ids=dict(type='list', default=None, required=False),
|
||||
key_count=dict(type='int', default=1, required=False),
|
||||
name=dict(default=None, required=False),
|
||||
new_name=dict(default=None, required=False),
|
||||
path=dict(default='/', required=False),
|
||||
new_path=dict(default=None, required=False)
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[],
|
||||
)
|
||||
|
||||
if not HAS_BOTO:
|
||||
module.fail_json(msg='This module requires boto, please install it')
|
||||
|
||||
state = module.params.get('state').lower()
|
||||
iam_type = module.params.get('iam_type').lower()
|
||||
groups = module.params.get('groups')
|
||||
name = module.params.get('name')
|
||||
new_name = module.params.get('new_name')
|
||||
password = module.params.get('password')
|
||||
update_pw = module.params.get('update_password')
|
||||
path = module.params.get('path')
|
||||
new_path = module.params.get('new_path')
|
||||
key_count = module.params.get('key_count')
|
||||
key_state = module.params.get('access_key_state')
|
||||
if key_state:
|
||||
key_state = key_state.lower()
|
||||
if any([n in key_state for n in ['active', 'inactive']]) and not key_ids:
|
||||
module.fail_json(changed=False, msg="At least one access key has to be defined in order"
|
||||
" to use 'active' or 'inactive'")
|
||||
key_ids = module.params.get('access_key_ids')
|
||||
|
||||
if iam_type == 'user' and module.params.get('password') is not None:
|
||||
pwd = module.params.get('password')
|
||||
elif iam_type != 'user' and module.params.get('password') is not None:
|
||||
module.fail_json(msg="a password is being specified when the iam_type "
|
||||
"is not user. Check parameters")
|
||||
else:
|
||||
pwd = None
|
||||
|
||||
if iam_type != 'user' and (module.params.get('access_key_state') is not None or
|
||||
module.params.get('access_key_id') is not None):
|
||||
module.fail_json(msg="the IAM type must be user, when IAM access keys "
|
||||
"are being modified. Check parameters")
|
||||
|
||||
if iam_type == 'role' and state == 'update':
|
||||
module.fail_json(changed=False, msg="iam_type: role, cannot currently be updated, "
|
||||
"please specificy present or absent")
|
||||
|
||||
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
|
||||
|
||||
try:
|
||||
iam = boto.iam.connection.IAMConnection(**aws_connect_kwargs)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
result = {}
|
||||
changed = False
|
||||
|
||||
orig_group_list = [gl['group_name'] for gl in iam.get_all_groups().
|
||||
list_groups_result.
|
||||
groups]
|
||||
orig_user_list = [ul['user_name'] for ul in iam.get_all_users().
|
||||
list_users_result.
|
||||
users]
|
||||
orig_role_list = [rl['role_name'] for rl in iam.list_roles().list_roles_response.
|
||||
list_roles_result.
|
||||
roles]
|
||||
orig_prof_list = [ap['instance_profile_name'] for ap in iam.list_instance_profiles().
|
||||
list_instance_profiles_response.
|
||||
list_instance_profiles_result.
|
||||
instance_profiles]
|
||||
|
||||
if iam_type == 'user':
|
||||
been_updated = False
|
||||
user_groups = None
|
||||
user_exists = any([n in [name, new_name] for n in orig_user_list])
|
||||
if user_exists:
|
||||
current_path = iam.get_user(name).get_user_result.user['path']
|
||||
if not new_path and current_path != path:
|
||||
new_path = path
|
||||
path = current_path
|
||||
|
||||
if state == 'present' and not user_exists and not new_name:
|
||||
(meta, changed) = create_user(
|
||||
module, iam, name, password, path, key_state, key_count)
|
||||
keys = iam.get_all_access_keys(name).list_access_keys_result.\
|
||||
access_key_metadata
|
||||
if groups:
|
||||
(user_groups, changed) = set_users_groups(
|
||||
module, iam, name, groups, been_updated, new_name)
|
||||
module.exit_json(
|
||||
user_meta=meta, groups=user_groups, keys=keys, changed=changed)
|
||||
|
||||
elif state in ['present', 'update'] and user_exists:
|
||||
if update_pw == 'on_create':
|
||||
password = None
|
||||
if name not in orig_user_list and new_name in orig_user_list:
|
||||
been_updated = True
|
||||
name_change, key_list, user_changed = update_user(
|
||||
module, iam, name, new_name, new_path, key_state, key_count, key_ids, password, been_updated)
|
||||
if name_change and new_name:
|
||||
orig_name = name
|
||||
name = new_name
|
||||
if groups:
|
||||
user_groups, groups_changed = set_users_groups(
|
||||
module, iam, name, groups, been_updated, new_name)
|
||||
if groups_changed == user_changed:
|
||||
changed = groups_changed
|
||||
else:
|
||||
changed = True
|
||||
else:
|
||||
changed = user_changed
|
||||
if new_name and new_path:
|
||||
module.exit_json(changed=changed, groups=user_groups, old_user_name=orig_name,
|
||||
new_user_name=new_name, old_path=path, new_path=new_path, keys=key_list)
|
||||
elif new_name and not new_path and not been_updated:
|
||||
module.exit_json(
|
||||
changed=changed, groups=user_groups, old_user_name=orig_name, new_user_name=new_name, keys=key_list)
|
||||
elif new_name and not new_path and been_updated:
|
||||
module.exit_json(
|
||||
changed=changed, groups=user_groups, user_name=new_name, keys=key_list, key_state=key_state)
|
||||
elif not new_name and new_path:
|
||||
module.exit_json(
|
||||
changed=changed, groups=user_groups, user_name=name, old_path=path, new_path=new_path, keys=key_list)
|
||||
else:
|
||||
module.exit_json(
|
||||
changed=changed, groups=user_groups, user_name=name, keys=key_list)
|
||||
elif state == 'update' and not user_exists:
|
||||
module.fail_json(
|
||||
msg="The user %s does not exit. No update made." % name)
|
||||
elif state == 'absent':
|
||||
if name in orig_user_list:
|
||||
set_users_groups(module, iam, name, '')
|
||||
del_meta, name, changed = delete_user(module, iam, name)
|
||||
module.exit_json(
|
||||
deletion_meta=del_meta, deleted_user=name, changed=changed)
|
||||
else:
|
||||
module.exit_json(
|
||||
changed=False, msg="User %s is already absent from your AWS IAM users" % name)
|
||||
|
||||
elif iam_type == 'group':
|
||||
group_exists = name in orig_group_list
|
||||
|
||||
if state == 'present' and not group_exists:
|
||||
new_group, changed = create_group(iam=iam, name=name, path=path)
|
||||
module.exit_json(changed=changed, group_name=new_group)
|
||||
elif state in ['present', 'update'] and group_exists:
|
||||
changed, updated_name, updated_path, cur_path = update_group(
|
||||
iam=iam, name=name, new_name=new_name, new_path=new_path)
|
||||
|
||||
if new_path and new_name:
|
||||
module.exit_json(changed=changed, old_group_name=name,
|
||||
new_group_name=updated_name, old_path=cur_path,
|
||||
new_group_path=updated_path)
|
||||
|
||||
if new_path and not new_name:
|
||||
module.exit_json(changed=changed, group_name=name,
|
||||
old_path=cur_path,
|
||||
new_group_path=updated_path)
|
||||
|
||||
if not new_path and new_name:
|
||||
module.exit_json(changed=changed, old_group_name=name,
|
||||
new_group_name=updated_name, group_path=cur_path)
|
||||
|
||||
if not new_path and not new_name:
|
||||
module.exit_json(
|
||||
changed=changed, group_name=name, group_path=cur_path)
|
||||
elif state == 'update' and not group_exists:
|
||||
module.fail_json(
|
||||
changed=changed, msg="Update Failed. Group %s doesn't seem to exit!" % name)
|
||||
elif state == 'absent':
|
||||
if name in orig_group_list:
|
||||
removed_group, changed = delete_group(iam=iam, name=name)
|
||||
module.exit_json(changed=changed, delete_group=removed_group)
|
||||
else:
|
||||
module.exit_json(changed=changed, msg="Group already absent")
|
||||
|
||||
elif iam_type == 'role':
|
||||
role_list = []
|
||||
if state == 'present':
|
||||
changed, role_list = create_role(
|
||||
module, iam, name, path, orig_role_list, orig_prof_list)
|
||||
elif state == 'absent':
|
||||
changed, role_list = delete_role(
|
||||
module, iam, name, orig_role_list, orig_prof_list)
|
||||
elif state == 'update':
|
||||
module.fail_json(
|
||||
changed=False, msg='Role update not currently supported by boto.')
|
||||
module.exit_json(changed=changed, roles=role_list)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -0,0 +1,294 @@
|
||||
#!/usr/bin/python
|
||||
# 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/>.
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: iam_cert
|
||||
short_description: Manage server certificates for use on ELBs and CloudFront
|
||||
description:
|
||||
- Allows for the management of server certificates
|
||||
version_added: "2.0"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of certificate to add, update or remove.
|
||||
required: true
|
||||
aliases: []
|
||||
new_name:
|
||||
description:
|
||||
- When present, this will update the name of the cert with the value passed here.
|
||||
required: false
|
||||
aliases: []
|
||||
new_path:
|
||||
description:
|
||||
- When present, this will update the path of the cert with the value passed here.
|
||||
required: false
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Whether to create, delete certificate. When present is specified it will attempt to make an update if new_path or new_name is specified.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "present", "absent" ]
|
||||
aliases: []
|
||||
path:
|
||||
description:
|
||||
- When creating or updating, specify the desired path of the certificate
|
||||
required: false
|
||||
default: "/"
|
||||
aliases: []
|
||||
cert_chain:
|
||||
description:
|
||||
- The path to the CA certificate chain in PEM encoded format.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
cert:
|
||||
description:
|
||||
- The path to the certificate body in PEM encoded format.
|
||||
required: false
|
||||
aliases: []
|
||||
key:
|
||||
description:
|
||||
- The path to the private key of the certificate in PEM encoded format.
|
||||
dup_ok:
|
||||
description:
|
||||
- By default the module will not upload a certifcate that is already uploaded into AWS. If set to True, it will upload the certifcate as long as the name is unique.
|
||||
required: false
|
||||
default: False
|
||||
aliases: []
|
||||
aws_secret_key:
|
||||
description:
|
||||
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'ec2_secret_key', 'secret_key' ]
|
||||
aws_access_key:
|
||||
description:
|
||||
- AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'ec2_access_key', 'access_key' ]
|
||||
|
||||
|
||||
requirements: [ "boto" ]
|
||||
author: Jonathan I. Davila
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic server certificate upload
|
||||
tasks:
|
||||
- name: Upload Certifcate
|
||||
iam_cert:
|
||||
name: very_ssl
|
||||
state: present
|
||||
cert: somecert.pem
|
||||
key: privcertkey
|
||||
cert_chain: myverytrustedchain
|
||||
|
||||
'''
|
||||
import json
|
||||
import sys
|
||||
try:
|
||||
import boto
|
||||
import boto.iam
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
||||
def boto_exception(err):
|
||||
'''generic error message handler'''
|
||||
if hasattr(err, 'error_message'):
|
||||
error = err.error_message
|
||||
elif hasattr(err, 'message'):
|
||||
error = err.message
|
||||
else:
|
||||
error = '%s: %s' % (Exception, err)
|
||||
|
||||
return error
|
||||
|
||||
def cert_meta(iam, name):
|
||||
opath = iam.get_server_certificate(name).get_server_certificate_result.\
|
||||
server_certificate.\
|
||||
server_certificate_metadata.\
|
||||
path
|
||||
ocert = iam.get_server_certificate(name).get_server_certificate_result.\
|
||||
server_certificate.\
|
||||
certificate_body
|
||||
ocert_id = iam.get_server_certificate(name).get_server_certificate_result.\
|
||||
server_certificate.\
|
||||
server_certificate_metadata.\
|
||||
server_certificate_id
|
||||
upload_date = iam.get_server_certificate(name).get_server_certificate_result.\
|
||||
server_certificate.\
|
||||
server_certificate_metadata.\
|
||||
upload_date
|
||||
exp = iam.get_server_certificate(name).get_server_certificate_result.\
|
||||
server_certificate.\
|
||||
server_certificate_metadata.\
|
||||
expiration
|
||||
return opath, ocert, ocert_id, upload_date, exp
|
||||
|
||||
def dup_check(module, iam, name, new_name, cert, orig_cert_names, orig_cert_bodies, dup_ok):
|
||||
update=False
|
||||
if any(ct in orig_cert_names for ct in [name, new_name]):
|
||||
for i_name in [name, new_name]:
|
||||
if i_name is None:
|
||||
continue
|
||||
|
||||
if cert is not None:
|
||||
try:
|
||||
c_index=orig_cert_names.index(i_name)
|
||||
except NameError:
|
||||
continue
|
||||
else:
|
||||
if orig_cert_bodies[c_index] == cert:
|
||||
update=True
|
||||
break
|
||||
elif orig_cert_bodies[c_index] != cert:
|
||||
module.fail_json(changed=False, msg='A cert with the name %s already exists and'
|
||||
' has a different certificate body associated'
|
||||
' with it. Certifcates cannot have the same name')
|
||||
else:
|
||||
update=True
|
||||
break
|
||||
elif cert in orig_cert_bodies and not dup_ok:
|
||||
for crt_name, crt_body in zip(orig_cert_names, orig_cert_bodies):
|
||||
if crt_body == cert:
|
||||
module.fail_json(changed=False, msg='This certificate already'
|
||||
' exists under the name %s' % crt_name)
|
||||
|
||||
return update
|
||||
|
||||
|
||||
def cert_action(module, iam, name, cpath, new_name, new_path, state,
|
||||
cert, key, chain, orig_cert_names, orig_cert_bodies, dup_ok):
|
||||
if state == 'present':
|
||||
update = dup_check(module, iam, name, new_name, cert, orig_cert_names,
|
||||
orig_cert_bodies, dup_ok)
|
||||
if update:
|
||||
opath, ocert, ocert_id, upload_date, exp = cert_meta(iam, name)
|
||||
changed=True
|
||||
if new_name and new_path:
|
||||
iam.update_server_cert(name, new_cert_name=new_name, new_path=new_path)
|
||||
module.exit_json(changed=changed, original_name=name, new_name=new_name,
|
||||
original_path=opath, new_path=new_path, cert_body=ocert,
|
||||
upload_date=upload_date, expiration_date=exp)
|
||||
elif new_name and not new_path:
|
||||
iam.update_server_cert(name, new_cert_name=new_name)
|
||||
module.exit_json(changed=changed, original_name=name, new_name=new_name,
|
||||
cert_path=opath, cert_body=ocert,
|
||||
upload_date=upload_date, expiration_date=exp)
|
||||
elif not new_name and new_path:
|
||||
iam.update_server_cert(name, new_path=new_path)
|
||||
module.exit_json(changed=changed, name=new_name,
|
||||
original_path=opath, new_path=new_path, cert_body=ocert,
|
||||
upload_date=upload_date, expiration_date=exp)
|
||||
else:
|
||||
changed=False
|
||||
module.exit_json(changed=changed, name=name, cert_path=opath, cert_body=ocert,
|
||||
upload_date=upload_date, expiration_date=exp,
|
||||
msg='No new path or name specified. No changes made')
|
||||
else:
|
||||
changed=True
|
||||
iam.upload_server_cert(name, cert, key, cert_chain=chain, path=cpath)
|
||||
opath, ocert, ocert_id, upload_date, exp = cert_meta(iam, name)
|
||||
module.exit_json(changed=changed, name=name, cert_path=opath, cert_body=ocert,
|
||||
upload_date=upload_date, expiration_date=exp)
|
||||
elif state == 'absent':
|
||||
if name in orig_cert_names:
|
||||
changed=True
|
||||
iam.delete_server_cert(name)
|
||||
module.exit_json(changed=changed, deleted_cert=name)
|
||||
else:
|
||||
changed=False
|
||||
module.exit_json(changed=changed, msg='Certifcate with the name %s already absent' % name)
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state=dict(
|
||||
default=None, required=True, choices=['present', 'absent']),
|
||||
name=dict(default=None, required=False),
|
||||
cert=dict(default=None, required=False),
|
||||
key=dict(default=None, required=False),
|
||||
cert_chain=dict(default=None, required=False),
|
||||
new_name=dict(default=None, required=False),
|
||||
path=dict(default='/', required=False),
|
||||
new_path=dict(default=None, required=False),
|
||||
dup_ok=dict(default=False, required=False, choices=[False, True])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[],
|
||||
)
|
||||
|
||||
if not HAS_BOTO:
|
||||
module.fail_json(msg="Boto is required for this module")
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
try:
|
||||
iam = boto.iam.connection.IAMConnection(
|
||||
aws_access_key_id=aws_access_key,
|
||||
aws_secret_access_key=aws_secret_key,
|
||||
)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
state = module.params.get('state')
|
||||
name = module.params.get('name')
|
||||
path = module.params.get('path')
|
||||
new_name = module.params.get('new_name')
|
||||
new_path = module.params.get('new_path')
|
||||
cert_chain = module.params.get('cert_chain')
|
||||
dup_ok = module.params.get('dup_ok')
|
||||
if state == 'present':
|
||||
cert = open(module.params.get('cert'), 'r').read().rstrip()
|
||||
key = open(module.params.get('key'), 'r').read().rstrip()
|
||||
if cert_chain is not None:
|
||||
cert_chain = open(module.params.get('cert_chain'), 'r').read()
|
||||
else:
|
||||
key=cert=chain=None
|
||||
|
||||
orig_certs = [ctb['server_certificate_name'] for ctb in \
|
||||
iam.get_all_server_certs().\
|
||||
list_server_certificates_result.\
|
||||
server_certificate_metadata_list]
|
||||
orig_bodies = [iam.get_server_certificate(thing).\
|
||||
get_server_certificate_result.\
|
||||
certificate_body \
|
||||
for thing in orig_certs]
|
||||
if new_name == name:
|
||||
new_name = None
|
||||
if new_path == path:
|
||||
new_path = None
|
||||
|
||||
changed = False
|
||||
try:
|
||||
cert_action(module, iam, name, path, new_name, new_path, state,
|
||||
cert, key, cert_chain, orig_certs, orig_bodies, dup_ok)
|
||||
except boto.exception.BotoServerError, err:
|
||||
module.fail_json(changed=changed, msg=str(err), debug=[cert,key])
|
||||
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,325 @@
|
||||
#!/usr/bin/python
|
||||
# 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/>.
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: iam_policy
|
||||
short_description: Manage IAM policies for users, groups, and roles
|
||||
description:
|
||||
- Allows uploading or removing IAM policies for IAM users, groups or roles.
|
||||
version_added: "2.0"
|
||||
options:
|
||||
iam_type:
|
||||
description:
|
||||
- Type of IAM resource
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "user", "group", "role"]
|
||||
aliases: []
|
||||
iam_name:
|
||||
description:
|
||||
- Name of IAM resource you wish to target for policy actions. In other words, the user name, group name or role name.
|
||||
required: true
|
||||
aliases: []
|
||||
policy_name:
|
||||
description:
|
||||
- The name label for the policy to create or remove.
|
||||
required: false
|
||||
aliases: []
|
||||
policy_document:
|
||||
description:
|
||||
- The path to the properly json formatted policy file
|
||||
required: false
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Whether to create or delete the IAM policy.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "present", "absent"]
|
||||
aliases: []
|
||||
skip_duplicates:
|
||||
description:
|
||||
- By default the module looks for any policies that match the document you pass in, if there is a match it will not make a new policy object with the same rules. You can override this by specifying false which would allow for two policy objects with different names but same rules.
|
||||
required: false
|
||||
default: "/"
|
||||
aliases: []
|
||||
aws_secret_key:
|
||||
description:
|
||||
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'ec2_secret_key', 'secret_key' ]
|
||||
aws_access_key:
|
||||
description:
|
||||
- AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'ec2_access_key', 'access_key' ]
|
||||
|
||||
requirements: [ "boto" ]
|
||||
notes:
|
||||
- 'Currently boto does not support the removal of Managed Policies, the module will not work removing/adding managed policies.'
|
||||
author: "Jonathan I. Davila (@defionscode)"
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create and policy with the name of 'Admin' to the group 'administrators'
|
||||
tasks:
|
||||
- name: Create two new IAM users with API keys
|
||||
iam_policy:
|
||||
iam_type: group
|
||||
iam_name: administrators
|
||||
policy_name: Admin
|
||||
state: present
|
||||
policy_document: admin_policy.json
|
||||
|
||||
# Advanced example, create two new groups and add a READ-ONLY policy to both
|
||||
# groups.
|
||||
task:
|
||||
- name: Create Two Groups, Mario and Luigi
|
||||
iam:
|
||||
iam_type: group
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
- Mario
|
||||
- Luigi
|
||||
register: new_groups
|
||||
|
||||
- name:
|
||||
iam_policy:
|
||||
iam_type: group
|
||||
iam_name: "{{ item.created_group.group_name }}"
|
||||
policy_name: "READ-ONLY"
|
||||
policy_document: readonlypolicy.json
|
||||
state: present
|
||||
with_items: new_groups.results
|
||||
|
||||
'''
|
||||
import json
|
||||
import urllib
|
||||
import sys
|
||||
try:
|
||||
import boto
|
||||
import boto.iam
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def boto_exception(err):
|
||||
'''generic error message handler'''
|
||||
if hasattr(err, 'error_message'):
|
||||
error = err.error_message
|
||||
elif hasattr(err, 'message'):
|
||||
error = err.message
|
||||
else:
|
||||
error = '%s: %s' % (Exception, err)
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def user_action(module, iam, name, policy_name, skip, pdoc, state):
|
||||
policy_match = False
|
||||
changed = False
|
||||
try:
|
||||
current_policies = [cp for cp in iam.get_all_user_policies(name).
|
||||
list_user_policies_result.
|
||||
policy_names]
|
||||
for pol in current_policies:
|
||||
'''
|
||||
urllib is needed here because boto returns url encoded strings instead
|
||||
'''
|
||||
if urllib.unquote(iam.get_user_policy(name, pol).
|
||||
get_user_policy_result.policy_document) == pdoc:
|
||||
policy_match = True
|
||||
if policy_match:
|
||||
msg=("The policy document you specified already exists "
|
||||
"under the name %s." % pol)
|
||||
if state == 'present' and skip:
|
||||
if policy_name not in current_policies and not policy_match:
|
||||
changed = True
|
||||
iam.put_user_policy(name, policy_name, pdoc)
|
||||
elif state == 'present' and not skip:
|
||||
changed = True
|
||||
iam.put_user_policy(name, policy_name, pdoc)
|
||||
elif state == 'absent':
|
||||
try:
|
||||
iam.delete_user_policy(name, policy_name)
|
||||
changed = True
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'cannot be found.' in error_msg:
|
||||
changed = False
|
||||
module.exit_json(changed=changed, msg="%s policy is already absent" % policy_name)
|
||||
|
||||
updated_policies = [cp for cp in iam.get_all_user_policies(name).
|
||||
list_user_policies_result.
|
||||
policy_names]
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
module.fail_json(changed=changed, msg=error_msg)
|
||||
|
||||
return changed, name, updated_policies
|
||||
|
||||
|
||||
def role_action(module, iam, name, policy_name, skip, pdoc, state):
|
||||
policy_match = False
|
||||
changed = False
|
||||
try:
|
||||
current_policies = [cp for cp in iam.list_role_policies(name).
|
||||
list_role_policies_result.
|
||||
policy_names]
|
||||
for pol in current_policies:
|
||||
if urllib.unquote(iam.get_role_policy(name, pol).
|
||||
get_role_policy_result.policy_document) == pdoc:
|
||||
policy_match = True
|
||||
if policy_match:
|
||||
msg=("The policy document you specified already exists "
|
||||
"under the name %s." % pol)
|
||||
if state == 'present' and skip:
|
||||
if policy_name not in current_policies and not policy_match:
|
||||
changed = True
|
||||
iam.put_role_policy(name, policy_name, pdoc)
|
||||
elif state == 'present' and not skip:
|
||||
changed = True
|
||||
iam.put_role_policy(name, policy_name, pdoc)
|
||||
elif state == 'absent':
|
||||
try:
|
||||
iam.delete_role_policy(name, policy_name)
|
||||
changed = True
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'cannot be found.' in error_msg:
|
||||
changed = False
|
||||
module.exit_json(changed=changed,
|
||||
msg="%s policy is already absent" % policy_name)
|
||||
|
||||
updated_policies = [cp for cp in iam.list_role_policies(name).
|
||||
list_role_policies_result.
|
||||
policy_names]
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
module.fail_json(changed=changed, msg=error_msg)
|
||||
|
||||
return changed, name, updated_policies
|
||||
|
||||
|
||||
def group_action(module, iam, name, policy_name, skip, pdoc, state):
|
||||
policy_match = False
|
||||
changed = False
|
||||
msg=''
|
||||
try:
|
||||
current_policies = [cp for cp in iam.get_all_group_policies(name).
|
||||
list_group_policies_result.
|
||||
policy_names]
|
||||
for pol in current_policies:
|
||||
if urllib.unquote(iam.get_group_policy(name, pol).
|
||||
get_group_policy_result.policy_document) == pdoc:
|
||||
policy_match = True
|
||||
if policy_match:
|
||||
msg=("The policy document you specified already exists "
|
||||
"under the name %s." % pol)
|
||||
if state == 'present' and skip:
|
||||
if policy_name not in current_policies and not policy_match:
|
||||
changed = True
|
||||
iam.put_group_policy(name, policy_name, pdoc)
|
||||
elif state == 'present' and not skip:
|
||||
changed = True
|
||||
iam.put_group_policy(name, policy_name, pdoc)
|
||||
elif state == 'absent':
|
||||
try:
|
||||
iam.delete_group_policy(name, policy_name)
|
||||
changed = True
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'cannot be found.' in error_msg:
|
||||
changed = False
|
||||
module.exit_json(changed=changed,
|
||||
msg="%s policy is already absent" % policy_name)
|
||||
|
||||
updated_policies = [cp for cp in iam.get_all_group_policies(name).
|
||||
list_group_policies_result.
|
||||
policy_names]
|
||||
except boto.exception.BotoServerError, err:
|
||||
error_msg = boto_exception(err)
|
||||
module.fail_json(changed=changed, msg=error_msg)
|
||||
|
||||
return changed, name, updated_policies, msg
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
iam_type=dict(
|
||||
default=None, required=True, choices=['user', 'group', 'role']),
|
||||
state=dict(
|
||||
default=None, required=True, choices=['present', 'absent']),
|
||||
iam_name=dict(default=None, required=False),
|
||||
policy_name=dict(default=None, required=True),
|
||||
policy_document=dict(default=None, required=False),
|
||||
skip_duplicates=dict(type='bool', default=True, required=False)
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
state = module.params.get('state').lower()
|
||||
iam_type = module.params.get('iam_type').lower()
|
||||
state = module.params.get('state')
|
||||
name = module.params.get('iam_name')
|
||||
policy_name = module.params.get('policy_name')
|
||||
skip = module.params.get('skip_duplicates')
|
||||
if module.params.get('policy_document') != None:
|
||||
with open(module.params.get('policy_document'), 'r') as json_data:
|
||||
pdoc = json.dumps(json.load(json_data))
|
||||
json_data.close()
|
||||
else:
|
||||
pdoc=None
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
try:
|
||||
iam = boto.iam.connection.IAMConnection(
|
||||
aws_access_key_id=aws_access_key,
|
||||
aws_secret_access_key=aws_secret_key,
|
||||
)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
changed = False
|
||||
|
||||
if iam_type == 'user':
|
||||
changed, user_name, current_policies = user_action(module, iam, name,
|
||||
policy_name, skip, pdoc,
|
||||
state)
|
||||
module.exit_json(changed=changed, user_name=name, policies=current_policies)
|
||||
elif iam_type == 'role':
|
||||
changed, role_name, current_policies = role_action(module, iam, name,
|
||||
policy_name, skip, pdoc,
|
||||
state)
|
||||
module.exit_json(changed=changed, role_name=name, policies=current_policies)
|
||||
elif iam_type == 'group':
|
||||
changed, group_name, current_policies, msg = group_action(module, iam, name,
|
||||
policy_name, skip, pdoc,
|
||||
state)
|
||||
module.exit_json(changed=changed, group_name=name, policies=current_policies, msg=msg)
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -0,0 +1,56 @@
|
||||
OpenStack Ansible Modules
|
||||
=========================
|
||||
|
||||
These are a set of modules for interacting with OpenStack as either an admin
|
||||
or an end user. If the module does not begin with os_, it's either deprecated
|
||||
or soon to be. This document serves as developer coding guidelines for
|
||||
modules intended to be here.
|
||||
|
||||
Naming
|
||||
------
|
||||
|
||||
* All modules should start with os_
|
||||
* If the module is one that a cloud consumer would expect to use, it should be
|
||||
named after the logical resource it manages. Thus, os\_server not os\_nova.
|
||||
The reasoning for this is that there are more than one resource that are
|
||||
managed by more than one service and which one manages it is a deployment
|
||||
detail. A good example of this are floating IPs, which can come from either
|
||||
Nova or Neutron, but which one they come from is immaterial to an end user.
|
||||
* If the module is one that a cloud admin would expect to use, it should be
|
||||
be named with the service and the resouce, such as os\_keystone\_domain.
|
||||
* If the module is one that a cloud admin and a cloud consumer could both use,
|
||||
the cloud consumer rules apply.
|
||||
|
||||
Interface
|
||||
---------
|
||||
|
||||
* If the resource being managed has an id, it should be returned.
|
||||
* If the resource being managed has an associated object more complex than
|
||||
an id, it should also be returned.
|
||||
|
||||
Interoperability
|
||||
----------------
|
||||
|
||||
* It should be assumed that the cloud consumer does not know a bazillion
|
||||
details about the deployment choices their cloud provider made, and a best
|
||||
effort should be made to present one sane interface to the ansible user
|
||||
regardless of deployer insanity.
|
||||
* All modules should work appropriately against all existing known public
|
||||
OpenStack clouds.
|
||||
* It should be assumed that a user may have more than one cloud account that
|
||||
they wish to combine as part of a single ansible managed infrastructure.
|
||||
|
||||
Libraries
|
||||
---------
|
||||
|
||||
* All modules should use openstack\_full\_argument\_spec to pick up the
|
||||
standard input such as auth and ssl support.
|
||||
* All modules should extends\_documentation\_fragment: openstack to go along
|
||||
with openstack\_full\_argument\_spec.
|
||||
* All complex cloud interaction or interoperability code should be housed in
|
||||
the [shade](http://git.openstack.org/cgit/openstack-infra/shade) library.
|
||||
* All OpenStack API interactions should happen via shade and not via
|
||||
OpenStack Client libraries. The OpenStack Client libraries do no have end
|
||||
users as a primary audience, they are for intra-server communication. The
|
||||
python-openstacksdk is the future there, and shade will migrate to it when
|
||||
its ready in a manner that is not noticable to ansible users.
|
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os_client_config
|
||||
from os_client_config import exceptions
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_client_config
|
||||
short_description: Get OpenStack Client config
|
||||
description:
|
||||
- Get I(openstack) client config data from clouds.yaml or environment
|
||||
version_added: "2.0"
|
||||
requirements: [ os-client-config ]
|
||||
author: "Monty Taylor (@emonty)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Get list of clouds that do not support security groups
|
||||
- os-client-config:
|
||||
- debug: var={{ item }}
|
||||
with_items: "{{ openstack.clouds|rejectattr('secgroup_source', 'none')|list() }}"
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule({})
|
||||
p = module.params
|
||||
|
||||
try:
|
||||
config = os_client_config.OpenStackConfig()
|
||||
clouds = []
|
||||
for cloud in config.get_all_clouds():
|
||||
cloud.config['name'] = cloud.name
|
||||
clouds.append(cloud.config)
|
||||
module.exit_json(ansible_facts=dict(openstack=dict(clouds=clouds)))
|
||||
except exceptions.OpenStackConfigException as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -0,0 +1,188 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2013, Benno Joy <benno@ansible.com>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#TODO(mordred): we need to support "location"(v1) and "locations"(v2)
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_image
|
||||
short_description: Add/Delete images from OpenStack Cloud
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Add or Remove images from the OpenStack Image Repository
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name that has to be given to the image
|
||||
required: true
|
||||
default: None
|
||||
disk_format:
|
||||
description:
|
||||
- The format of the disk that is getting uploaded
|
||||
required: false
|
||||
default: qcow2
|
||||
container_format:
|
||||
description:
|
||||
- The format of the container
|
||||
required: false
|
||||
default: bare
|
||||
owner:
|
||||
description:
|
||||
- The owner of the image
|
||||
required: false
|
||||
default: None
|
||||
min_disk:
|
||||
description:
|
||||
- The minimum disk space required to deploy this image
|
||||
required: false
|
||||
default: None
|
||||
min_ram:
|
||||
description:
|
||||
- The minimum ram required to deploy this image
|
||||
required: false
|
||||
default: None
|
||||
is_public:
|
||||
description:
|
||||
- Whether the image can be accessed publicly. Note that publicizing an image requires admin role by default.
|
||||
required: false
|
||||
default: 'yes'
|
||||
filename:
|
||||
description:
|
||||
- The path to the file which has to be uploaded
|
||||
required: false
|
||||
default: None
|
||||
ramdisk:
|
||||
descrption:
|
||||
- The name of an existing ramdisk image that will be associated with this image
|
||||
required: false
|
||||
default: None
|
||||
kernel:
|
||||
descrption:
|
||||
- The name of an existing kernel image that will be associated with this image
|
||||
required: false
|
||||
default: None
|
||||
properties:
|
||||
description:
|
||||
- Additional properties to be associated with this image
|
||||
required: false
|
||||
default: {}
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
requirements: ["shade"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Upload an image from a local file named cirros-0.3.0-x86_64-disk.img
|
||||
- os_image:
|
||||
auth:
|
||||
auth_url: http://localhost/auth/v2.0
|
||||
username: admin
|
||||
password: passme
|
||||
project_name: admin
|
||||
name: cirros
|
||||
container_format: bare
|
||||
disk_format: qcow2
|
||||
state: present
|
||||
filename: cirros-0.3.0-x86_64-disk.img
|
||||
kernel: cirros-vmlinuz
|
||||
ramdisk: cirros-initrd
|
||||
properties:
|
||||
cpu_arch: x86_64
|
||||
distro: ubuntu
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name = dict(required=True),
|
||||
disk_format = dict(default='qcow2', choices=['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso']),
|
||||
container_format = dict(default='bare', choices=['ami', 'aki', 'ari', 'bare', 'ovf', 'ova']),
|
||||
owner = dict(default=None),
|
||||
min_disk = dict(default=None),
|
||||
min_ram = dict(default=None),
|
||||
is_public = dict(default=False),
|
||||
filename = dict(default=None),
|
||||
ramdisk = dict(default=None),
|
||||
kernel = dict(default=None),
|
||||
properties = dict(default={}),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
|
||||
changed = False
|
||||
image = cloud.get_image(name_or_id=module.params['name'])
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
if not image:
|
||||
image = cloud.create_image(
|
||||
name=module.params['name'],
|
||||
filename=module.params['filename'],
|
||||
disk_format=module.params['disk_format'],
|
||||
container_format=module.params['container_format'],
|
||||
wait=module.params['wait'],
|
||||
timeout=module.params['timeout']
|
||||
)
|
||||
changed = True
|
||||
if not module.params['wait']:
|
||||
module.exit_json(changed=changed, image=image, id=image.id)
|
||||
|
||||
cloud.update_image_properties(
|
||||
image=image,
|
||||
kernel=module.params['kernel'],
|
||||
ramdisk=module.params['ramdisk'],
|
||||
**module.params['properties'])
|
||||
image = cloud.get_image(name_or_id=image.id)
|
||||
module.exit_json(changed=changed, image=image, id=image.id)
|
||||
|
||||
elif module.params['state'] == 'absent':
|
||||
if not image:
|
||||
changed = False
|
||||
else:
|
||||
cloud.delete_image(
|
||||
name_or_id=module.params['name'],
|
||||
wait=module.params['wait'],
|
||||
timeout=module.params['timeout'])
|
||||
changed = True
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message, extra_data=e.extra_data)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -0,0 +1,353 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
import jsonpatch
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_ironic
|
||||
short_description: Create/Delete Bare Metal Resources from OpenStack
|
||||
extends_documentation_fragment: openstack
|
||||
author: "Monty Taylor (@emonty)"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Create or Remove Ironic nodes from OpenStack.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicates desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
uuid:
|
||||
description:
|
||||
- globally unique identifier (UUID) to be given to the resource. Will
|
||||
be auto-generated if not specified, and name is specified.
|
||||
- Definition of a UUID will always take precedence to a name value.
|
||||
required: false
|
||||
default: None
|
||||
name:
|
||||
description:
|
||||
- unique name identifier to be given to the resource.
|
||||
required: false
|
||||
default: None
|
||||
driver:
|
||||
description:
|
||||
- The name of the Ironic Driver to use with this node.
|
||||
required: true
|
||||
default: None
|
||||
chassis_uuid:
|
||||
description:
|
||||
- Associate the node with a pre-defined chassis.
|
||||
required: false
|
||||
default: None
|
||||
ironic_url:
|
||||
description:
|
||||
- If noauth mode is utilized, this is required to be set to the
|
||||
endpoint URL for the Ironic API. Use with "auth" and "auth_type"
|
||||
settings set to None.
|
||||
required: false
|
||||
default: None
|
||||
driver_info:
|
||||
description:
|
||||
- Information for this server's driver. Will vary based on which
|
||||
driver is in use. Any sub-field which is populated will be validated
|
||||
during creation.
|
||||
suboptions:
|
||||
power:
|
||||
description:
|
||||
- Information necessary to turn this server on / off.
|
||||
This often includes such things as IPMI username, password, and IP address.
|
||||
required: true
|
||||
deploy:
|
||||
description:
|
||||
- Information necessary to deploy this server directly, without using Nova. THIS IS NOT RECOMMENDED.
|
||||
console:
|
||||
description:
|
||||
- Information necessary to connect to this server's serial console. Not all drivers support this.
|
||||
management:
|
||||
description:
|
||||
- Information necessary to interact with this server's management interface. May be shared by power_info in some cases.
|
||||
required: true
|
||||
nics:
|
||||
description:
|
||||
- 'A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc"'
|
||||
required: true
|
||||
properties:
|
||||
description:
|
||||
- Definition of the physical characteristics of this server, used for scheduling purposes
|
||||
suboptions:
|
||||
cpu_arch:
|
||||
description:
|
||||
- CPU architecture (x86_64, i686, ...)
|
||||
default: x86_64
|
||||
cpus:
|
||||
description:
|
||||
- Number of CPU cores this machine has
|
||||
default: 1
|
||||
ram:
|
||||
description:
|
||||
- amount of RAM this machine has, in MB
|
||||
default: 1
|
||||
disk_size:
|
||||
description:
|
||||
- size of first storage device in this machine (typically /dev/sda), in GB
|
||||
default: 1
|
||||
skip_update_of_driver_password:
|
||||
description:
|
||||
- Allows the code that would assert changes to nodes to skip the
|
||||
update if the change is a single line consisting of the password
|
||||
field. As of Kilo, by default, passwords are always masked to API
|
||||
requests, which means the logic as a result always attempts to
|
||||
re-assert the password field.
|
||||
required: false
|
||||
default: false
|
||||
|
||||
requirements: ["shade", "jsonpatch"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Enroll a node with some basic properties and driver info
|
||||
- os_ironic:
|
||||
cloud: "devstack"
|
||||
driver: "pxe_ipmitool"
|
||||
uuid: "00000000-0000-0000-0000-000000000002"
|
||||
properties:
|
||||
cpus: 2
|
||||
cpu_arch: "x86_64"
|
||||
ram: 8192
|
||||
disk_size: 64
|
||||
nics:
|
||||
- mac: "aa:bb:cc:aa:bb:cc"
|
||||
- mac: "dd:ee:ff:dd:ee:ff"
|
||||
driver_info:
|
||||
power:
|
||||
ipmi_address: "1.2.3.4"
|
||||
ipmi_username: "admin"
|
||||
ipmi_password: "adminpass"
|
||||
chassis_uuid: "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def _parse_properties(module):
|
||||
p = module.params['properties']
|
||||
props = dict(
|
||||
cpu_arch=p.get('cpu_arch') if p.get('cpu_arch') else 'x86_64',
|
||||
cpus=p.get('cpus') if p.get('cpus') else 1,
|
||||
memory_mb=p.get('ram') if p.get('ram') else 1,
|
||||
local_gb=p.get('disk_size') if p.get('disk_size') else 1,
|
||||
)
|
||||
return props
|
||||
|
||||
|
||||
def _parse_driver_info(module):
|
||||
p = module.params['driver_info']
|
||||
info = p.get('power')
|
||||
if not info:
|
||||
raise shade.OpenStackCloudException(
|
||||
"driver_info['power'] is required")
|
||||
if p.get('console'):
|
||||
info.update(p.get('console'))
|
||||
if p.get('management'):
|
||||
info.update(p.get('management'))
|
||||
if p.get('deploy'):
|
||||
info.update(p.get('deploy'))
|
||||
return info
|
||||
|
||||
|
||||
def _choose_id_value(module):
|
||||
if module.params['uuid']:
|
||||
return module.params['uuid']
|
||||
if module.params['name']:
|
||||
return module.params['name']
|
||||
return None
|
||||
|
||||
|
||||
def _is_value_true(value):
|
||||
true_values = [True, 'yes', 'Yes', 'True', 'true']
|
||||
if value in true_values:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _choose_if_password_only(module, patch):
|
||||
if len(patch) is 1:
|
||||
if 'password' in patch[0]['path'] and _is_value_true(
|
||||
module.params['skip_update_of_masked_password']):
|
||||
# Return false to aabort update as the password appears
|
||||
# to be the only element in the patch.
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _exit_node_not_updated(module, server):
|
||||
module.exit_json(
|
||||
changed=False,
|
||||
result="Node not updated",
|
||||
uuid=server['uuid'],
|
||||
provision_state=server['provision_state']
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
uuid=dict(required=False),
|
||||
name=dict(required=False),
|
||||
driver=dict(required=False),
|
||||
driver_info=dict(type='dict', required=True),
|
||||
nics=dict(type='list', required=True),
|
||||
properties=dict(type='dict', default={}),
|
||||
ironic_url=dict(required=False),
|
||||
chassis_uuid=dict(required=False),
|
||||
skip_update_of_masked_password=dict(required=False, choices=BOOLEANS),
|
||||
state=dict(required=False, default='present')
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
if (module.params['auth_type'] in [None, 'None'] and
|
||||
module.params['ironic_url'] is None):
|
||||
module.fail_json(msg="Authentication appears to be disabled, "
|
||||
"Please define an ironic_url parameter")
|
||||
|
||||
if (module.params['ironic_url'] and
|
||||
module.params['auth_type'] in [None, 'None']):
|
||||
module.params['auth'] = dict(
|
||||
endpoint=module.params['ironic_url']
|
||||
)
|
||||
|
||||
node_id = _choose_id_value(module)
|
||||
|
||||
try:
|
||||
cloud = shade.operator_cloud(**module.params)
|
||||
server = cloud.get_machine(node_id)
|
||||
if module.params['state'] == 'present':
|
||||
if module.params['driver'] is None:
|
||||
module.fail_json(msg="A driver must be defined in order "
|
||||
"to set a node to present.")
|
||||
|
||||
properties = _parse_properties(module)
|
||||
driver_info = _parse_driver_info(module)
|
||||
kwargs = dict(
|
||||
driver=module.params['driver'],
|
||||
properties=properties,
|
||||
driver_info=driver_info,
|
||||
name=module.params['name'],
|
||||
)
|
||||
|
||||
if module.params['chassis_uuid']:
|
||||
kwargs['chassis_uuid'] = module.params['chassis_uuid']
|
||||
|
||||
if server is None:
|
||||
# Note(TheJulia): Add a specific UUID to the request if
|
||||
# present in order to be able to re-use kwargs for if
|
||||
# the node already exists logic, since uuid cannot be
|
||||
# updated.
|
||||
if module.params['uuid']:
|
||||
kwargs['uuid'] = module.params['uuid']
|
||||
|
||||
server = cloud.register_machine(module.params['nics'],
|
||||
**kwargs)
|
||||
module.exit_json(changed=True, uuid=server['uuid'],
|
||||
provision_state=server['provision_state'])
|
||||
else:
|
||||
# TODO(TheJulia): Presently this does not support updating
|
||||
# nics. Support needs to be added.
|
||||
#
|
||||
# Note(TheJulia): This message should never get logged
|
||||
# however we cannot realistically proceed if neither a
|
||||
# name or uuid was supplied to begin with.
|
||||
if not node_id:
|
||||
module.fail_json(msg="A uuid or name value "
|
||||
"must be defined")
|
||||
|
||||
# Note(TheJulia): Constructing the configuration to compare
|
||||
# against. The items listed in the server_config block can
|
||||
# be updated via the API.
|
||||
|
||||
server_config = dict(
|
||||
driver=server['driver'],
|
||||
properties=server['properties'],
|
||||
driver_info=server['driver_info'],
|
||||
name=server['name'],
|
||||
)
|
||||
|
||||
# Add the pre-existing chassis_uuid only if
|
||||
# it is present in the server configuration.
|
||||
if hasattr(server, 'chassis_uuid'):
|
||||
server_config['chassis_uuid'] = server['chassis_uuid']
|
||||
|
||||
# Note(TheJulia): If a password is defined and concealed, a
|
||||
# patch will always be generated and re-asserted.
|
||||
patch = jsonpatch.JsonPatch.from_diff(server_config, kwargs)
|
||||
|
||||
if not patch:
|
||||
_exit_node_not_updated(module, server)
|
||||
elif _choose_if_password_only(module, list(patch)):
|
||||
# Note(TheJulia): Normally we would allow the general
|
||||
# exception catch below, however this allows a specific
|
||||
# message.
|
||||
try:
|
||||
server = cloud.patch_machine(
|
||||
server['uuid'],
|
||||
list(patch))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to update node, "
|
||||
"Error: %s" % e.message)
|
||||
|
||||
# Enumerate out a list of changed paths.
|
||||
change_list = []
|
||||
for change in list(patch):
|
||||
change_list.append(change['path'])
|
||||
module.exit_json(changed=True,
|
||||
result="Node Updated",
|
||||
changes=change_list,
|
||||
uuid=server['uuid'],
|
||||
provision_state=server['provision_state'])
|
||||
|
||||
# Return not updated by default as the conditions were not met
|
||||
# to update.
|
||||
_exit_node_not_updated(module, server)
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
if not node_id:
|
||||
module.fail_json(msg="A uuid or name value must be defined "
|
||||
"in order to remove a node.")
|
||||
|
||||
if server is not None:
|
||||
cloud.unregister_machine(module.params['nics'],
|
||||
server['uuid'])
|
||||
module.exit_json(changed=True, result="deleted")
|
||||
else:
|
||||
module.exit_json(changed=False, result="Server not found")
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -0,0 +1,333 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_ironic_node
|
||||
short_description: Activate/Deactivate Bare Metal Resources from OpenStack
|
||||
author: "Monty Taylor (@emonty)"
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Deploy to nodes controlled by Ironic.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicates desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
deploy:
|
||||
description:
|
||||
- Indicates if the resource should be deployed. Allows for deployment
|
||||
logic to be disengaged and control of the node power or maintenance
|
||||
state to be changed.
|
||||
choices: ['true', 'false']
|
||||
default: true
|
||||
uuid:
|
||||
description:
|
||||
- globally unique identifier (UUID) to be given to the resource.
|
||||
required: false
|
||||
default: None
|
||||
ironic_url:
|
||||
description:
|
||||
- If noauth mode is utilized, this is required to be set to the
|
||||
endpoint URL for the Ironic API. Use with "auth" and "auth_type"
|
||||
settings set to None.
|
||||
required: false
|
||||
default: None
|
||||
config_drive:
|
||||
description:
|
||||
- A configdrive file or HTTP(S) URL that will be passed along to the
|
||||
node.
|
||||
required: false
|
||||
default: None
|
||||
instance_info:
|
||||
description:
|
||||
- Definition of the instance information which is used to deploy
|
||||
the node. This information is only required when an instance is
|
||||
set to present.
|
||||
suboptions:
|
||||
image_source:
|
||||
description:
|
||||
- An HTTP(S) URL where the image can be retrieved from.
|
||||
image_checksum:
|
||||
description:
|
||||
- The checksum of image_source.
|
||||
image_disk_format:
|
||||
description:
|
||||
- The type of image that has been requested to be deployed.
|
||||
power:
|
||||
description:
|
||||
- A setting to allow power state to be asserted allowing nodes
|
||||
that are not yet deployed to be powered on, and nodes that
|
||||
are deployed to be powered off.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
maintenance:
|
||||
description:
|
||||
- A setting to allow the direct control if a node is in
|
||||
maintenance mode.
|
||||
required: false
|
||||
default: false
|
||||
maintenance_reason:
|
||||
description:
|
||||
- A string expression regarding the reason a node is in a
|
||||
maintenance mode.
|
||||
required: false
|
||||
default: None
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Activate a node by booting an image with a configdrive attached
|
||||
os_ironic_node:
|
||||
cloud: "openstack"
|
||||
uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69"
|
||||
state: present
|
||||
power: present
|
||||
deploy: True
|
||||
maintenance: False
|
||||
config_drive: "http://192.168.1.1/host-configdrive.iso"
|
||||
instance_info:
|
||||
image_source: "http://192.168.1.1/deploy_image.img"
|
||||
image_checksum: "356a6b55ecc511a20c33c946c4e678af"
|
||||
image_disk_format: "qcow"
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
|
||||
def _choose_id_value(module):
|
||||
if module.params['uuid']:
|
||||
return module.params['uuid']
|
||||
if module.params['name']:
|
||||
return module.params['name']
|
||||
return None
|
||||
|
||||
|
||||
# TODO(TheJulia): Change this over to use the machine patch method
|
||||
# in shade once it is available.
|
||||
def _prepare_instance_info_patch(instance_info):
|
||||
patch = []
|
||||
patch.append({
|
||||
'op': 'replace',
|
||||
'path': '/instance_info',
|
||||
'value': instance_info
|
||||
})
|
||||
return patch
|
||||
|
||||
|
||||
def _is_true(value):
|
||||
true_values = [True, 'yes', 'Yes', 'True', 'true', 'present', 'on']
|
||||
if value in true_values:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_false(value):
|
||||
false_values = [False, None, 'no', 'No', 'False', 'false', 'absent', 'off']
|
||||
if value in false_values:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _check_set_maintenance(module, cloud, node):
|
||||
if _is_true(module.params['maintenance']):
|
||||
if _is_false(node['maintenance']):
|
||||
cloud.set_machine_maintenance_state(
|
||||
node['uuid'],
|
||||
True,
|
||||
reason=module.params['maintenance_reason'])
|
||||
module.exit_json(changed=True, msg="Node has been set into "
|
||||
"maintenance mode")
|
||||
else:
|
||||
# User has requested maintenance state, node is already in the
|
||||
# desired state, checking to see if the reason has changed.
|
||||
if (str(node['maintenance_reason']) not in
|
||||
str(module.params['maintenance_reason'])):
|
||||
cloud.set_machine_maintenance_state(
|
||||
node['uuid'],
|
||||
True,
|
||||
reason=module.params['maintenance_reason'])
|
||||
module.exit_json(changed=True, msg="Node maintenance reason "
|
||||
"updated, cannot take any "
|
||||
"additional action.")
|
||||
elif _is_false(module.params['maintenance']):
|
||||
if node['maintenance'] is True:
|
||||
cloud.remove_machine_from_maintenance(node['uuid'])
|
||||
return True
|
||||
else:
|
||||
module.fail_json(msg="maintenance parameter was set but a valid "
|
||||
"the value was not recognized.")
|
||||
return False
|
||||
|
||||
|
||||
def _check_set_power_state(module, cloud, node):
|
||||
if 'power on' in str(node['power_state']):
|
||||
if _is_false(module.params['power']):
|
||||
# User has requested the node be powered off.
|
||||
cloud.set_machine_power_off(node['uuid'])
|
||||
module.exit_json(changed=True, msg="Power requested off")
|
||||
if 'power off' in str(node['power_state']):
|
||||
if (_is_false(module.params['power']) and
|
||||
_is_false(module.params['state'])):
|
||||
return False
|
||||
if (_is_false(module.params['power']) and
|
||||
_is_false(module.params['state'])):
|
||||
module.exit_json(
|
||||
changed=False,
|
||||
msg="Power for node is %s, node must be reactivated "
|
||||
"OR set to state absent"
|
||||
)
|
||||
# In the event the power has been toggled on and
|
||||
# deployment has been requested, we need to skip this
|
||||
# step.
|
||||
if (_is_true(module.params['power']) and
|
||||
_is_false(module.params['deploy'])):
|
||||
# Node is powered down when it is not awaiting to be provisioned
|
||||
cloud.set_machine_power_on(node['uuid'])
|
||||
return True
|
||||
# Default False if no action has been taken.
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
uuid=dict(required=False),
|
||||
name=dict(required=False),
|
||||
instance_info=dict(type='dict', required=False),
|
||||
config_drive=dict(required=False),
|
||||
ironic_url=dict(required=False),
|
||||
state=dict(required=False, default='present'),
|
||||
maintenance=dict(required=False),
|
||||
maintenance_reason=dict(required=False),
|
||||
power=dict(required=False, default='present'),
|
||||
deploy=dict(required=False, default=True),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
if (module.params['auth_type'] in [None, 'None'] and
|
||||
module.params['ironic_url'] is None):
|
||||
module.fail_json(msg="Authentication appears disabled, Please "
|
||||
"define an ironic_url parameter")
|
||||
|
||||
if (module.params['ironic_url'] and
|
||||
module.params['auth_type'] in [None, 'None']):
|
||||
module.params['auth'] = dict(
|
||||
endpoint=module.params['ironic_url']
|
||||
)
|
||||
|
||||
node_id = _choose_id_value(module)
|
||||
|
||||
if not node_id:
|
||||
module.fail_json(msg="A uuid or name value must be defined "
|
||||
"to use this module.")
|
||||
try:
|
||||
cloud = shade.operator_cloud(**module.params)
|
||||
node = cloud.get_machine(node_id)
|
||||
|
||||
if node is None:
|
||||
module.fail_json(msg="node not found")
|
||||
|
||||
uuid = node['uuid']
|
||||
instance_info = module.params['instance_info']
|
||||
changed = False
|
||||
|
||||
# User has reqeusted desired state to be in maintenance state.
|
||||
if module.params['state'] is 'maintenance':
|
||||
module.params['maintenance'] = True
|
||||
|
||||
if node['provision_state'] in [
|
||||
'cleaning',
|
||||
'deleting',
|
||||
'wait call-back']:
|
||||
module.fail_json(msg="Node is in %s state, cannot act upon the "
|
||||
"request as the node is in a transition "
|
||||
"state" % node['provision_state'])
|
||||
# TODO(TheJulia) This is in-development code, that requires
|
||||
# code in the shade library that is still in development.
|
||||
if _check_set_maintenance(module, cloud, node):
|
||||
if node['provision_state'] in 'active':
|
||||
module.exit_json(changed=True,
|
||||
result="Maintenance state changed")
|
||||
changed = True
|
||||
node = cloud.get_machine(node_id)
|
||||
|
||||
if _check_set_power_state(module, cloud, node):
|
||||
changed = True
|
||||
node = cloud.get_machine(node_id)
|
||||
|
||||
if _is_true(module.params['state']):
|
||||
if _is_false(module.params['deploy']):
|
||||
module.exit_json(
|
||||
changed=changed,
|
||||
result="User request has explicitly disabled "
|
||||
"deployment logic"
|
||||
)
|
||||
|
||||
if 'active' in node['provision_state']:
|
||||
module.exit_json(
|
||||
changed=changed,
|
||||
result="Node already in an active state."
|
||||
)
|
||||
|
||||
if instance_info is None:
|
||||
module.fail_json(
|
||||
changed=changed,
|
||||
msg="When setting an instance to present, "
|
||||
"instance_info is a required variable.")
|
||||
|
||||
# TODO(TheJulia): Update instance info, however info is
|
||||
# deployment specific. Perhaps consider adding rebuild
|
||||
# support, although there is a known desire to remove
|
||||
# rebuild support from Ironic at some point in the future.
|
||||
patch = _prepare_instance_info_patch(instance_info)
|
||||
cloud.set_node_instance_info(uuid, patch)
|
||||
cloud.validate_node(uuid)
|
||||
cloud.activate_node(uuid, module.params['config_drive'])
|
||||
# TODO(TheJulia): Add more error checking and a wait option.
|
||||
# We will need to loop, or just add the logic to shade,
|
||||
# although this could be a very long running process as
|
||||
# baremetal deployments are not a "quick" task.
|
||||
module.exit_json(changed=changed, result="node activated")
|
||||
|
||||
elif _is_false(module.params['state']):
|
||||
if node['provision_state'] not in "deleted":
|
||||
cloud.purge_node_instance_info(uuid)
|
||||
cloud.deactivate_node(uuid)
|
||||
module.exit_json(changed=True, result="deleted")
|
||||
else:
|
||||
module.exit_json(changed=False, result="node not found")
|
||||
else:
|
||||
module.fail_json(msg="State must be present, absent, "
|
||||
"maintenance, off")
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2013, Benno Joy <benno@ansible.com>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_network
|
||||
short_description: Creates/Removes networks from OpenStack
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Add or Remove network from OpenStack.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name to be assigned to the network.
|
||||
required: true
|
||||
shared:
|
||||
description:
|
||||
- Whether this network is shared or not.
|
||||
required: false
|
||||
default: false
|
||||
admin_state_up:
|
||||
description:
|
||||
- Whether the state should be marked as up or down.
|
||||
required: false
|
||||
default: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource.
|
||||
choices: ['present', 'absent']
|
||||
required: false
|
||||
default: present
|
||||
requirements: ["shade"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- os_network:
|
||||
name=t1network
|
||||
state=present
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name=dict(required=True),
|
||||
shared=dict(default=False, type='bool'),
|
||||
admin_state_up=dict(default=True, type='bool'),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
state = module.params['state']
|
||||
name = module.params['name']
|
||||
shared = module.params['shared']
|
||||
admin_state_up = module.params['admin_state_up']
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
net = cloud.get_network(name)
|
||||
|
||||
if state == 'present':
|
||||
if not net:
|
||||
net = cloud.create_network(name, shared, admin_state_up)
|
||||
module.exit_json(changed=False, network=net, id=net['id'])
|
||||
|
||||
elif state == 'absent':
|
||||
if not net:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
cloud.delete_network(name)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -0,0 +1,125 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2013, Benno Joy <benno@ansible.com>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_object
|
||||
short_description: Create or Delete objects and containers from OpenStack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
extends_documentation_fragment: openstack
|
||||
description:
|
||||
- Create or Delete objects and containers from OpenStack
|
||||
options:
|
||||
container:
|
||||
description:
|
||||
- The name of the container in which to create the object
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- Name to be give to the object. If omitted, operations will be on
|
||||
the entire container
|
||||
required: false
|
||||
file:
|
||||
description:
|
||||
- Path to local file to be uploaded.
|
||||
required: false
|
||||
container_access:
|
||||
description:
|
||||
- desired container access level.
|
||||
required: false
|
||||
choices: ['private', 'public']
|
||||
default: private
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Creates a object named 'fstab' in the 'config' container
|
||||
- os_object: cloud=mordred state=present name=fstab container=config file=/etc/fstab
|
||||
|
||||
# Deletes a container called config and all of its contents
|
||||
- os_object: cloud=rax-iad state=absent container=config
|
||||
'''
|
||||
|
||||
|
||||
def process_object(
|
||||
cloud_obj, container, name, filename, container_access, **kwargs):
|
||||
|
||||
changed = False
|
||||
container_obj = cloud_obj.get_container(container)
|
||||
if kwargs['state'] == 'present':
|
||||
if not container_obj:
|
||||
container_obj = cloud_obj.create_container(container)
|
||||
changed = True
|
||||
if cloud_obj.get_container_access(container) != container_access:
|
||||
cloud_obj.set_container_access(container, container_access)
|
||||
changed = True
|
||||
if name:
|
||||
if cloud_obj.is_object_stale(container, name, filename):
|
||||
cloud_obj.create_object(container, name, filename)
|
||||
changed = True
|
||||
else:
|
||||
if container_obj:
|
||||
if name:
|
||||
if cloud_obj.get_object_metadata(container, name):
|
||||
cloud_obj.delete_object(container, name)
|
||||
changed= True
|
||||
else:
|
||||
cloud_obj.delete_container(container)
|
||||
changed= True
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name=dict(required=False, default=None),
|
||||
container=dict(required=True),
|
||||
filename=dict(required=False, default=None),
|
||||
container_access=dict(default='private', choices=['private', 'public']),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
|
||||
changed = process_object(cloud, **module.params)
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -0,0 +1,142 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2013, Benno Joy <benno@ansible.com>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_security_group
|
||||
short_description: Add/Delete security groups from an OpenStack cloud.
|
||||
extends_documentation_fragment: openstack
|
||||
author: "Monty Taylor (@emonty)"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Add or Remove security groups from an OpenStack cloud.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name that has to be given to the security group. This module
|
||||
requires that security group names be unique.
|
||||
required: true
|
||||
description:
|
||||
description:
|
||||
- Long description of the purpose of the security group
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a security group
|
||||
- os_security_group:
|
||||
cloud=mordred
|
||||
state=present
|
||||
name=foo
|
||||
description=security group for foo servers
|
||||
|
||||
# Update the existing 'foo' security group description
|
||||
- os_security_group:
|
||||
cloud=mordred
|
||||
state=present
|
||||
name=foo
|
||||
description=updated description for the foo security group
|
||||
'''
|
||||
|
||||
|
||||
def _needs_update(module, secgroup):
|
||||
"""Check for differences in the updatable values.
|
||||
|
||||
NOTE: We don't currently allow name updates.
|
||||
"""
|
||||
if secgroup['description'] != module.params['description']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _system_state_change(module, secgroup):
|
||||
state = module.params['state']
|
||||
if state == 'present':
|
||||
if not secgroup:
|
||||
return True
|
||||
return _needs_update(module, secgroup)
|
||||
if state == 'absent' and secgroup:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name=dict(required=True),
|
||||
description=dict(default=None),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec,
|
||||
supports_check_mode=True,
|
||||
**module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
description = module.params['description']
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
secgroup = cloud.get_security_group(name)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=_system_state_change(module, secgroup))
|
||||
|
||||
changed = False
|
||||
if state == 'present':
|
||||
if not secgroup:
|
||||
secgroup = cloud.create_security_group(name, description)
|
||||
changed = True
|
||||
else:
|
||||
if _needs_update(module, secgroup):
|
||||
secgroup = cloud.update_security_group(
|
||||
secgroup['id'], description=description)
|
||||
changed = True
|
||||
module.exit_json(
|
||||
changed=changed, id=secgroup['id'], secgroup=secgroup)
|
||||
|
||||
if state == 'absent':
|
||||
if secgroup:
|
||||
cloud.delete_security_group(secgroup['id'])
|
||||
changed = True
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -0,0 +1,451 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2013, Benno Joy <benno@ansible.com>
|
||||
# Copyright (c) 2013, John Dewey <john@dewey.ws>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
try:
|
||||
import shade
|
||||
from shade import meta
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_server
|
||||
short_description: Create/Delete Compute Instances from OpenStack
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Create or Remove compute instances from OpenStack.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name that has to be given to the instance
|
||||
required: true
|
||||
image:
|
||||
description:
|
||||
- The name or id of the base image to boot.
|
||||
required: true
|
||||
image_exclude:
|
||||
description:
|
||||
- Text to use to filter image names, for the case, such as HP, where
|
||||
there are multiple image names matching the common identifying
|
||||
portions. image_exclude is a negative match filter - it is text that
|
||||
may not exist in the image name. Defaults to "(deprecated)"
|
||||
flavor:
|
||||
description:
|
||||
- The name or id of the flavor in which the new instance has to be
|
||||
created. Mutually exclusive with flavor_ram
|
||||
required: false
|
||||
default: 1
|
||||
flavor_ram:
|
||||
description:
|
||||
- The minimum amount of ram in MB that the flavor in which the new
|
||||
instance has to be created must have. Mutually exclusive with flavor.
|
||||
required: false
|
||||
default: 1
|
||||
flavor_include:
|
||||
description:
|
||||
- Text to use to filter flavor names, for the case, such as Rackspace,
|
||||
where there are multiple flavors that have the same ram count.
|
||||
flavor_include is a positive match filter - it must exist in the
|
||||
flavor name.
|
||||
key_name:
|
||||
description:
|
||||
- The key pair name to be used when creating a instance
|
||||
required: false
|
||||
default: None
|
||||
security_groups:
|
||||
description:
|
||||
- The name of the security group to which the instance should be added
|
||||
required: false
|
||||
default: None
|
||||
nics:
|
||||
description:
|
||||
- A list of networks to which the instance's interface should
|
||||
be attached. Networks may be referenced by net-id or net-name.
|
||||
required: false
|
||||
default: None
|
||||
public_ip:
|
||||
description:
|
||||
- Ensure instance has public ip however the cloud wants to do that
|
||||
required: false
|
||||
default: 'yes'
|
||||
floating_ips:
|
||||
description:
|
||||
- list of valid floating IPs that pre-exist to assign to this node
|
||||
required: false
|
||||
default: None
|
||||
floating_ip_pools:
|
||||
description:
|
||||
- list of floating IP pools from which to choose a floating IP
|
||||
required: false
|
||||
default: None
|
||||
meta:
|
||||
description:
|
||||
- A list of key value pairs that should be provided as a metadata to
|
||||
the new instance.
|
||||
required: false
|
||||
default: None
|
||||
wait:
|
||||
description:
|
||||
- If the module should wait for the instance to be created.
|
||||
required: false
|
||||
default: 'yes'
|
||||
timeout:
|
||||
description:
|
||||
- The amount of time the module should wait for the instance to get
|
||||
into active state.
|
||||
required: false
|
||||
default: 180
|
||||
config_drive:
|
||||
description:
|
||||
- Whether to boot the server with config drive enabled
|
||||
required: false
|
||||
default: 'no'
|
||||
userdata:
|
||||
description:
|
||||
- Opaque blob of data which is made available to the instance
|
||||
required: false
|
||||
default: None
|
||||
root_volume:
|
||||
description:
|
||||
- Boot instance from a volume
|
||||
required: false
|
||||
default: None
|
||||
terminate_volume:
|
||||
description:
|
||||
- If true, delete volume when deleting instance (if booted from volume)
|
||||
default: false
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Creates a new instance and attaches to a network and passes metadata to
|
||||
# the instance
|
||||
- os_server:
|
||||
state: present
|
||||
auth:
|
||||
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
|
||||
username: admin
|
||||
password: admin
|
||||
project_name: admin
|
||||
name: vm1
|
||||
image: 4f905f38-e52a-43d2-b6ec-754a13ffb529
|
||||
key_name: ansible_key
|
||||
timeout: 200
|
||||
flavor: 4
|
||||
nics:
|
||||
- net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723
|
||||
- net-name: another_network
|
||||
meta:
|
||||
hostname: test1
|
||||
group: uge_master
|
||||
|
||||
# Creates a new instance in HP Cloud AE1 region availability zone az2 and
|
||||
# automatically assigns a floating IP
|
||||
- name: launch a compute instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
os_server:
|
||||
state: present
|
||||
auth:
|
||||
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
|
||||
username: username
|
||||
password: Equality7-2521
|
||||
project_name: username-project1
|
||||
name: vm1
|
||||
region_name: region-b.geo-1
|
||||
availability_zone: az2
|
||||
image: 9302692b-b787-4b52-a3a6-daebb79cb498
|
||||
key_name: test
|
||||
timeout: 200
|
||||
flavor: 101
|
||||
security_groups: default
|
||||
auto_floating_ip: yes
|
||||
|
||||
# Creates a new instance in named cloud mordred availability zone az2
|
||||
# and assigns a pre-known floating IP
|
||||
- name: launch a compute instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
os_server:
|
||||
state: present
|
||||
cloud: mordred
|
||||
name: vm1
|
||||
availability_zone: az2
|
||||
image: 9302692b-b787-4b52-a3a6-daebb79cb498
|
||||
key_name: test
|
||||
timeout: 200
|
||||
flavor: 101
|
||||
floating-ips:
|
||||
- 12.34.56.79
|
||||
|
||||
# Creates a new instance with 4G of RAM on Ubuntu Trusty, ignoring
|
||||
# deprecated images
|
||||
- name: launch a compute instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
os_server:
|
||||
name: vm1
|
||||
state: present
|
||||
cloud: mordred
|
||||
region_name: region-b.geo-1
|
||||
image: Ubuntu Server 14.04
|
||||
image_exclude: deprecated
|
||||
flavor_ram: 4096
|
||||
|
||||
# Creates a new instance with 4G of RAM on Ubuntu Trusty on a Performance node
|
||||
- name: launch a compute instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
os_server:
|
||||
name: vm1
|
||||
cloud: rax-dfw
|
||||
state: present
|
||||
image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)
|
||||
flavor_ram: 4096
|
||||
flavor_include: Performance
|
||||
'''
|
||||
|
||||
|
||||
def _exit_hostvars(module, cloud, server, changed=True):
|
||||
hostvars = meta.get_hostvars_from_server(cloud, server)
|
||||
module.exit_json(
|
||||
changed=changed, server=server, id=server.id, openstack=hostvars)
|
||||
|
||||
|
||||
def _network_args(module, cloud):
|
||||
args = []
|
||||
for net in module.params['nics']:
|
||||
if net.get('net-id'):
|
||||
args.append(net)
|
||||
elif net.get('net-name'):
|
||||
by_name = cloud.get_network(net['net-name'])
|
||||
if not by_name:
|
||||
module.fail_json(
|
||||
msg='Could not find network by net-name: %s' %
|
||||
net['net-name'])
|
||||
args.append({'net-id': by_name['id']})
|
||||
return args
|
||||
|
||||
|
||||
def _delete_server(module, cloud):
|
||||
try:
|
||||
cloud.delete_server(
|
||||
module.params['name'], wait=module.params['wait'],
|
||||
timeout=module.params['timeout'])
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Error in deleting vm: %s" % e.message)
|
||||
module.exit_json(changed=True, result='deleted')
|
||||
|
||||
|
||||
def _create_server(module, cloud):
|
||||
flavor = module.params['flavor']
|
||||
flavor_ram = module.params['flavor_ram']
|
||||
flavor_include = module.params['flavor_include']
|
||||
|
||||
image_id = None
|
||||
if not module.params['root_volume']:
|
||||
image_id = cloud.get_image_id(
|
||||
module.params['image'], module.params['image_exclude'])
|
||||
|
||||
if flavor:
|
||||
flavor_dict = cloud.get_flavor(flavor)
|
||||
else:
|
||||
flavor_dict = cloud.get_flavor_by_ram(flavor_ram, flavor_include)
|
||||
|
||||
nics = _network_args(module, cloud)
|
||||
|
||||
bootkwargs = dict(
|
||||
name=module.params['name'],
|
||||
image=image_id,
|
||||
flavor=flavor_dict['id'],
|
||||
nics=nics,
|
||||
meta=module.params['meta'],
|
||||
security_groups=module.params['security_groups'].split(','),
|
||||
userdata=module.params['userdata'],
|
||||
config_drive=module.params['config_drive'],
|
||||
)
|
||||
for optional_param in ('region_name', 'key_name', 'availability_zone'):
|
||||
if module.params[optional_param]:
|
||||
bootkwargs[optional_param] = module.params[optional_param]
|
||||
|
||||
server = cloud.create_server(
|
||||
ip_pool=module.params['floating_ip_pools'],
|
||||
ips=module.params['floating_ips'],
|
||||
auto_ip=module.params['auto_floating_ip'],
|
||||
root_volume=module.params['root_volume'],
|
||||
terminate_volume=module.params['terminate_volume'],
|
||||
wait=module.params['wait'], timeout=module.params['timeout'],
|
||||
**bootkwargs
|
||||
)
|
||||
|
||||
_exit_hostvars(module, cloud, server)
|
||||
|
||||
|
||||
def _delete_floating_ip_list(cloud, server, extra_ips):
|
||||
for ip in extra_ips:
|
||||
cloud.nova_client.servers.remove_floating_ip(
|
||||
server=server.id, address=ip)
|
||||
|
||||
|
||||
def _check_floating_ips(module, cloud, server):
|
||||
changed = False
|
||||
|
||||
auto_floating_ip = module.params['auto_floating_ip']
|
||||
floating_ips = module.params['floating_ips']
|
||||
floating_ip_pools = module.params['floating_ip_pools']
|
||||
|
||||
if floating_ip_pools or floating_ips or auto_floating_ip:
|
||||
ips = openstack_find_nova_addresses(server.addresses, 'floating')
|
||||
if not ips:
|
||||
# If we're configured to have a floating but we don't have one,
|
||||
# let's add one
|
||||
server = cloud.add_ips_to_server(
|
||||
server,
|
||||
auto_ip=auto_floating_ip,
|
||||
ips=floating_ips,
|
||||
ip_pool=floating_ip_pools,
|
||||
)
|
||||
changed = True
|
||||
elif floating_ips:
|
||||
# we were configured to have specific ips, let's make sure we have
|
||||
# those
|
||||
missing_ips = []
|
||||
for ip in floating_ips:
|
||||
if ip not in ips:
|
||||
missing_ips.append(ip)
|
||||
if missing_ips:
|
||||
server = cloud.add_ip_list(server, missing_ips)
|
||||
changed = True
|
||||
extra_ips = []
|
||||
for ip in ips:
|
||||
if ip not in floating_ips:
|
||||
extra_ips.append(ip)
|
||||
if extra_ips:
|
||||
_delete_floating_ip_list(cloud, server, extra_ips)
|
||||
changed = True
|
||||
return (changed, server)
|
||||
|
||||
|
||||
def _get_server_state(module, cloud):
|
||||
state = module.params['state']
|
||||
server = cloud.get_server(module.params['name'])
|
||||
if server and state == 'present':
|
||||
if server.status != 'ACTIVE':
|
||||
module.fail_json(
|
||||
msg="The instance is available but not Active state: "
|
||||
+ server.status)
|
||||
(ip_changed, server) = _check_floating_ips(module, cloud, server)
|
||||
_exit_hostvars(module, cloud, server, ip_changed)
|
||||
if server and state == 'absent':
|
||||
return True
|
||||
if state == 'absent':
|
||||
module.exit_json(changed=False, result="not present")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name = dict(required=True),
|
||||
image = dict(default=None),
|
||||
image_exclude = dict(default='(deprecated)'),
|
||||
flavor = dict(default=None),
|
||||
flavor_ram = dict(default=None, type='int'),
|
||||
flavor_include = dict(default=None),
|
||||
key_name = dict(default=None),
|
||||
security_groups = dict(default='default'),
|
||||
nics = dict(default=[]),
|
||||
meta = dict(default=None),
|
||||
userdata = dict(default=None),
|
||||
config_drive = dict(default=False, type='bool'),
|
||||
auto_floating_ip = dict(default=True, type='bool'),
|
||||
floating_ips = dict(default=None),
|
||||
floating_ip_pools = dict(default=None),
|
||||
root_volume = dict(default=None),
|
||||
terminate_volume = dict(default=False, type='bool'),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs(
|
||||
mutually_exclusive=[
|
||||
['auto_floating_ip', 'floating_ips'],
|
||||
['auto_floating_ip', 'floating_ip_pools'],
|
||||
['floating_ips', 'floating_ip_pools'],
|
||||
['flavor', 'flavor_ram'],
|
||||
['image', 'root_volume'],
|
||||
],
|
||||
)
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
state = module.params['state']
|
||||
image = module.params['image']
|
||||
root_volume = module.params['root_volume']
|
||||
flavor = module.params['flavor']
|
||||
flavor_ram = module.params['flavor_ram']
|
||||
|
||||
if state == 'present':
|
||||
if not (image or root_volume):
|
||||
module.fail_json(
|
||||
msg="Parameter 'image' or 'root_volume' is required "
|
||||
"if state == 'present'"
|
||||
)
|
||||
if not flavor and not flavor_ram:
|
||||
module.fail_json(
|
||||
msg="Parameter 'flavor' or 'flavor_ram' is required "
|
||||
"if state == 'present'"
|
||||
)
|
||||
|
||||
try:
|
||||
cloud_params = dict(module.params)
|
||||
cloud_params.pop('userdata', None)
|
||||
cloud = shade.openstack_cloud(**cloud_params)
|
||||
|
||||
if state == 'present':
|
||||
_get_server_state(module, cloud)
|
||||
_create_server(module, cloud)
|
||||
elif state == 'absent':
|
||||
_get_server_state(module, cloud)
|
||||
_delete_server(module, cloud)
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message, extra_data=e.extra_data)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,193 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2015, Jesse Keating <jlk@derpops.bike>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
try:
|
||||
import shade
|
||||
from shade import meta
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_server_actions
|
||||
short_description: Perform actions on Compute Instances from OpenStack
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Jesse Keating (@j2sol)"
|
||||
description:
|
||||
- Perform server actions on an existing compute instance from OpenStack.
|
||||
This module does not return any data other than changed true/false.
|
||||
options:
|
||||
server:
|
||||
description:
|
||||
- Name or ID of the instance
|
||||
required: true
|
||||
wait:
|
||||
description:
|
||||
- If the module should wait for the instance action to be performed.
|
||||
required: false
|
||||
default: 'yes'
|
||||
timeout:
|
||||
description:
|
||||
- The amount of time the module should wait for the instance to perform
|
||||
the requested action.
|
||||
required: false
|
||||
default: 180
|
||||
action:
|
||||
description:
|
||||
- Perform the given action. The lock and unlock actions always return
|
||||
changed as the servers API does not provide lock status.
|
||||
choices: [pause, unpause, lock, unlock, suspend, resume]
|
||||
default: present
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Pauses a compute instance
|
||||
- os_server_actions:
|
||||
action: pause
|
||||
auth:
|
||||
auth_url: https://mycloud.openstack.blueboxgrid.com:5001/v2.0
|
||||
username: admin
|
||||
password: admin
|
||||
project_name: admin
|
||||
server: vm1
|
||||
timeout: 200
|
||||
'''
|
||||
|
||||
_action_map = {'pause': 'PAUSED',
|
||||
'unpause': 'ACTIVE',
|
||||
'lock': 'ACTIVE', # API doesn't show lock/unlock status
|
||||
'unlock': 'ACTIVE',
|
||||
'suspend': 'SUSPENDED',
|
||||
'resume': 'ACTIVE',}
|
||||
|
||||
_admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock']
|
||||
|
||||
def _wait(timeout, cloud, server, action):
|
||||
"""Wait for the server to reach the desired state for the given action."""
|
||||
|
||||
for count in shade._iterate_timeout(
|
||||
timeout,
|
||||
"Timeout waiting for server to complete %s" % action):
|
||||
try:
|
||||
server = cloud.get_server(server.id)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if server.status == _action_map[action]:
|
||||
return
|
||||
|
||||
if server.status == 'ERROR':
|
||||
module.fail_json(msg="Server reached ERROR state while attempting to %s" % action)
|
||||
|
||||
def _system_state_change(action, status):
|
||||
"""Check if system state would change."""
|
||||
if status == _action_map[action]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
server=dict(required=True),
|
||||
action=dict(required=True, choices=['pause', 'unpause', 'lock', 'unlock', 'suspend',
|
||||
'resume']),
|
||||
)
|
||||
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=True, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
action = module.params['action']
|
||||
wait = module.params['wait']
|
||||
timeout = module.params['timeout']
|
||||
|
||||
try:
|
||||
if action in _admin_actions:
|
||||
cloud = shade.operator_cloud(**module.params)
|
||||
else:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
server = cloud.get_server(module.params['server'])
|
||||
if not server:
|
||||
module.fail_json(msg='Could not find server %s' % server)
|
||||
status = server.status
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=_system_state_change(action, status))
|
||||
|
||||
if action == 'pause':
|
||||
if not _system_state_change(action, status):
|
||||
module.exit_json(changed=False)
|
||||
|
||||
cloud.nova_client.servers.pause(server=server.id)
|
||||
if wait:
|
||||
_wait(timeout, cloud, server, action)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
elif action == 'unpause':
|
||||
if not _system_state_change(action, status):
|
||||
module.exit_json(changed=False)
|
||||
|
||||
cloud.nova_client.servers.unpause(server=server.id)
|
||||
if wait:
|
||||
_wait(timeout, cloud, server, action)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
elif action == 'lock':
|
||||
# lock doesn't set a state, just do it
|
||||
cloud.nova_client.servers.lock(server=server.id)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
elif action == 'unlock':
|
||||
# unlock doesn't set a state, just do it
|
||||
cloud.nova_client.servers.unlock(server=server.id)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
elif action == 'suspend':
|
||||
if not _system_state_change(action, status):
|
||||
module.exit_json(changed=False)
|
||||
|
||||
cloud.nova_client.servers.suspend(server=server.id)
|
||||
if wait:
|
||||
_wait(timeout, cloud, server, action)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
elif action == 'resume':
|
||||
if not _system_state_change(action, status):
|
||||
module.exit_json(changed=False)
|
||||
|
||||
cloud.nova_client.servers.resume(server=server.id)
|
||||
if wait:
|
||||
_wait(timeout, cloud, server, action)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message, extra_data=e.extra_data)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
from shade import meta
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_server_facts
|
||||
short_description: Retrieve facts about a compute instance
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Retrieve facts about a server instance from OpenStack.
|
||||
notes:
|
||||
- Facts are placed in the C(openstack) variable.
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
options:
|
||||
server:
|
||||
description:
|
||||
- Name or ID of the instance
|
||||
required: true
|
||||
extends_documentation_fragment: openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Gather facts about a previously created server named vm1
|
||||
- os_server_facts:
|
||||
cloud: rax-dfw
|
||||
server: vm1
|
||||
- debug: var=openstack
|
||||
'''
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
server=dict(required=True),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
server = cloud.get_server(module.params['server'])
|
||||
hostvars = dict(openstack=meta.get_hostvars_from_server(
|
||||
cloud, server))
|
||||
module.exit_json(changed=False, ansible_facts=hostvars)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -0,0 +1,155 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
try:
|
||||
import shade
|
||||
from shade import meta
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_server_volume
|
||||
short_description: Attach/Detach Volumes from OpenStack VM's
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Attach or Detach volumes from OpenStack VM's
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
required: false
|
||||
server:
|
||||
description:
|
||||
- Name or ID of server you want to attach a volume to
|
||||
required: true
|
||||
volume:
|
||||
description:
|
||||
- Name or id of volume you want to attach to a server
|
||||
required: true
|
||||
device:
|
||||
description:
|
||||
- Device you want to attach. Defaults to auto finding a device name.
|
||||
required: false
|
||||
default: None
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Attaches a volume to a compute host
|
||||
- name: attach a volume
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: attach volume to host
|
||||
os_server_volume:
|
||||
state: present
|
||||
cloud: mordred
|
||||
server: Mysql-server
|
||||
volume: mysql-data
|
||||
device: /dev/vdb
|
||||
'''
|
||||
|
||||
|
||||
def _system_state_change(state, device):
|
||||
"""Check if system state would change."""
|
||||
if state == 'present':
|
||||
if device:
|
||||
return False
|
||||
return True
|
||||
if state == 'absent':
|
||||
if device:
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
server=dict(required=True),
|
||||
volume=dict(required=True),
|
||||
device=dict(default=None), # None == auto choose device name
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec,
|
||||
supports_check_mode=True,
|
||||
**module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
state = module.params['state']
|
||||
wait = module.params['wait']
|
||||
timeout = module.params['timeout']
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
server = cloud.get_server(module.params['server'])
|
||||
volume = cloud.get_volume(module.params['volume'])
|
||||
dev = cloud.get_volume_attach_device(volume, server.id)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=_system_state_change(state, dev))
|
||||
|
||||
if state == 'present':
|
||||
if dev:
|
||||
# Volume is already attached to this server
|
||||
module.exit_json(changed=False)
|
||||
|
||||
cloud.attach_volume(server, volume, module.params['device'],
|
||||
wait=wait, timeout=timeout)
|
||||
|
||||
server = cloud.get_server(module.params['server']) # refresh
|
||||
volume = cloud.get_volume(module.params['volume']) # refresh
|
||||
hostvars = meta.get_hostvars_from_server(cloud, server)
|
||||
|
||||
module.exit_json(
|
||||
changed=True,
|
||||
id=volume['id'],
|
||||
attachments=volume['attachments'],
|
||||
openstack=hostvars
|
||||
)
|
||||
|
||||
elif state == 'absent':
|
||||
if not dev:
|
||||
# Volume is not attached to this server
|
||||
module.exit_json(changed=False)
|
||||
|
||||
cloud.detach_volume(server, volume, wait=wait, timeout=timeout)
|
||||
module.exit_json(
|
||||
changed=True,
|
||||
result='Detached volume from server'
|
||||
)
|
||||
|
||||
except (shade.OpenStackCloudException, shade.OpenStackCloudTimeout) as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# this is magic, see lib/ansible/module_utils/common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,261 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Benno Joy <benno@ansible.com>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_subnet
|
||||
short_description: Add/Remove subnet to an OpenStack network
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Add or Remove a subnet to an OpenStack network
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
required: false
|
||||
default: present
|
||||
network_name:
|
||||
description:
|
||||
- Name of the network to which the subnet should be attached
|
||||
required: true when state is 'present'
|
||||
name:
|
||||
description:
|
||||
- The name of the subnet that should be created. Although Neutron
|
||||
allows for non-unique subnet names, this module enforces subnet
|
||||
name uniqueness.
|
||||
required: true
|
||||
cidr:
|
||||
description:
|
||||
- The CIDR representation of the subnet that should be assigned to
|
||||
the subnet.
|
||||
required: true when state is 'present'
|
||||
default: None
|
||||
ip_version:
|
||||
description:
|
||||
- The IP version of the subnet 4 or 6
|
||||
required: false
|
||||
default: 4
|
||||
enable_dhcp:
|
||||
description:
|
||||
- Whether DHCP should be enabled for this subnet.
|
||||
required: false
|
||||
default: true
|
||||
gateway_ip:
|
||||
description:
|
||||
- The ip that would be assigned to the gateway for this subnet
|
||||
required: false
|
||||
default: None
|
||||
dns_nameservers:
|
||||
description:
|
||||
- List of DNS nameservers for this subnet.
|
||||
required: false
|
||||
default: None
|
||||
allocation_pool_start:
|
||||
description:
|
||||
- From the subnet pool the starting address from which the IP should
|
||||
be allocated.
|
||||
required: false
|
||||
default: None
|
||||
allocation_pool_end:
|
||||
description:
|
||||
- From the subnet pool the last IP that should be assigned to the
|
||||
virtual machines.
|
||||
required: false
|
||||
default: None
|
||||
host_routes:
|
||||
description:
|
||||
- A list of host route dictionaries for the subnet.
|
||||
required: false
|
||||
default: None
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a new (or update an existing) subnet on the specified network
|
||||
- os_subnet:
|
||||
state=present
|
||||
network_name=network1
|
||||
name=net1subnet
|
||||
cidr=192.168.0.0/24
|
||||
dns_nameservers:
|
||||
- 8.8.8.7
|
||||
- 8.8.8.8
|
||||
host_routes:
|
||||
- destination: 0.0.0.0/0
|
||||
nexthop: 123.456.78.9
|
||||
- destination: 192.168.0.0/24
|
||||
nexthop: 192.168.0.1
|
||||
|
||||
# Delete a subnet
|
||||
- os_subnet:
|
||||
state=absent
|
||||
name=net1subnet
|
||||
'''
|
||||
|
||||
|
||||
def _needs_update(subnet, module):
|
||||
"""Check for differences in the updatable values."""
|
||||
enable_dhcp = module.params['enable_dhcp']
|
||||
subnet_name = module.params['name']
|
||||
pool_start = module.params['allocation_pool_start']
|
||||
pool_end = module.params['allocation_pool_end']
|
||||
gateway_ip = module.params['gateway_ip']
|
||||
dns = module.params['dns_nameservers']
|
||||
host_routes = module.params['host_routes']
|
||||
curr_pool = subnet['allocation_pools'][0]
|
||||
|
||||
if subnet['enable_dhcp'] != enable_dhcp:
|
||||
return True
|
||||
if subnet_name and subnet['name'] != subnet_name:
|
||||
return True
|
||||
if pool_start and curr_pool['start'] != pool_start:
|
||||
return True
|
||||
if pool_end and curr_pool['end'] != pool_end:
|
||||
return True
|
||||
if gateway_ip and subnet['gateway_ip'] != gateway_ip:
|
||||
return True
|
||||
if dns and sorted(subnet['dns_nameservers']) != sorted(dns):
|
||||
return True
|
||||
if host_routes:
|
||||
curr_hr = sorted(subnet['host_routes'], key=lambda t: t.keys())
|
||||
new_hr = sorted(host_routes, key=lambda t: t.keys())
|
||||
if sorted(curr_hr) != sorted(new_hr):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _system_state_change(module, subnet):
|
||||
state = module.params['state']
|
||||
if state == 'present':
|
||||
if not subnet:
|
||||
return True
|
||||
return _needs_update(subnet, module)
|
||||
if state == 'absent' and subnet:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
name=dict(required=True),
|
||||
network_name=dict(default=None),
|
||||
cidr=dict(default=None),
|
||||
ip_version=dict(default='4', choices=['4', '6']),
|
||||
enable_dhcp=dict(default='true', type='bool'),
|
||||
gateway_ip=dict(default=None),
|
||||
dns_nameservers=dict(default=None, type='list'),
|
||||
allocation_pool_start=dict(default=None),
|
||||
allocation_pool_end=dict(default=None),
|
||||
host_routes=dict(default=None, type='list'),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec,
|
||||
supports_check_mode=True,
|
||||
**module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
state = module.params['state']
|
||||
network_name = module.params['network_name']
|
||||
cidr = module.params['cidr']
|
||||
ip_version = module.params['ip_version']
|
||||
enable_dhcp = module.params['enable_dhcp']
|
||||
subnet_name = module.params['name']
|
||||
gateway_ip = module.params['gateway_ip']
|
||||
dns = module.params['dns_nameservers']
|
||||
pool_start = module.params['allocation_pool_start']
|
||||
pool_end = module.params['allocation_pool_end']
|
||||
host_routes = module.params['host_routes']
|
||||
|
||||
# Check for required parameters when state == 'present'
|
||||
if state == 'present':
|
||||
for p in ['network_name', 'cidr']:
|
||||
if not module.params[p]:
|
||||
module.fail_json(msg='%s required with present state' % p)
|
||||
|
||||
if pool_start and pool_end:
|
||||
pool = [dict(start=pool_start, end=pool_end)]
|
||||
elif pool_start or pool_end:
|
||||
module.fail_json(msg='allocation pool requires start and end values')
|
||||
else:
|
||||
pool = None
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
subnet = cloud.get_subnet(subnet_name)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=_system_state_change(module, subnet))
|
||||
|
||||
if state == 'present':
|
||||
if not subnet:
|
||||
subnet = cloud.create_subnet(network_name, cidr,
|
||||
ip_version=ip_version,
|
||||
enable_dhcp=enable_dhcp,
|
||||
subnet_name=subnet_name,
|
||||
gateway_ip=gateway_ip,
|
||||
dns_nameservers=dns,
|
||||
allocation_pools=pool,
|
||||
host_routes=host_routes)
|
||||
changed = True
|
||||
else:
|
||||
if _needs_update(subnet, module):
|
||||
cloud.update_subnet(subnet['id'],
|
||||
subnet_name=subnet_name,
|
||||
enable_dhcp=enable_dhcp,
|
||||
gateway_ip=gateway_ip,
|
||||
dns_nameservers=dns,
|
||||
allocation_pools=pool,
|
||||
host_routes=host_routes)
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
elif state == 'absent':
|
||||
if not subnet:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
cloud.delete_subnet(subnet_name)
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
try:
|
||||
import shade
|
||||
HAS_SHADE = True
|
||||
except ImportError:
|
||||
HAS_SHADE = False
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: os_volume
|
||||
short_description: Create/Delete Cinder Volumes
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Create or Remove cinder block storage volumes
|
||||
options:
|
||||
size:
|
||||
description:
|
||||
- Size of volume in GB
|
||||
required: only when state is 'present'
|
||||
default: None
|
||||
display_name:
|
||||
description:
|
||||
- Name of volume
|
||||
required: true
|
||||
display_description:
|
||||
description:
|
||||
- String describing the volume
|
||||
required: false
|
||||
default: None
|
||||
volume_type:
|
||||
description:
|
||||
- Volume type for volume
|
||||
required: false
|
||||
default: None
|
||||
image:
|
||||
description:
|
||||
- Image name or id for boot from volume
|
||||
required: false
|
||||
default: None
|
||||
snapshot_id:
|
||||
description:
|
||||
- Volume snapshot id to create from
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Creates a new volume
|
||||
- name: create a volume
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: create 40g test volume
|
||||
os_volume:
|
||||
state: present
|
||||
cloud: mordred
|
||||
availability_zone: az2
|
||||
size: 40
|
||||
display_name: test_volume
|
||||
'''
|
||||
|
||||
|
||||
def _present_volume(module, cloud):
|
||||
if cloud.volume_exists(module.params['display_name']):
|
||||
v = cloud.get_volume(module.params['display_name'])
|
||||
module.exit_json(changed=False, id=v['id'], volume=v)
|
||||
|
||||
volume_args = dict(
|
||||
size=module.params['size'],
|
||||
volume_type=module.params['volume_type'],
|
||||
display_name=module.params['display_name'],
|
||||
display_description=module.params['display_description'],
|
||||
snapshot_id=module.params['snapshot_id'],
|
||||
availability_zone=module.params['availability_zone'],
|
||||
)
|
||||
if module.params['image']:
|
||||
image_id = cloud.get_image_id(module.params['image'])
|
||||
volume_args['imageRef'] = image_id
|
||||
|
||||
volume = cloud.create_volume(
|
||||
wait=module.params['wait'], timeout=module.params['timeout'],
|
||||
**volume_args)
|
||||
module.exit_json(changed=True, id=volume['id'], volume=volume)
|
||||
|
||||
|
||||
def _absent_volume(module, cloud):
|
||||
try:
|
||||
cloud.delete_volume(
|
||||
name_or_id=module.params['display_name'],
|
||||
wait=module.params['wait'],
|
||||
timeout=module.params['timeout'])
|
||||
except shade.OpenStackCloudTimeout:
|
||||
module.exit_json(changed=False)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
size=dict(default=None),
|
||||
volume_type=dict(default=None),
|
||||
display_name=dict(required=True, aliases=['name']),
|
||||
display_description=dict(default=None, aliases=['description']),
|
||||
image=dict(default=None),
|
||||
snapshot_id=dict(default=None),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs(
|
||||
mutually_exclusive=[
|
||||
['image', 'snapshot_id'],
|
||||
],
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec, **module_kwargs)
|
||||
|
||||
if not HAS_SHADE:
|
||||
module.fail_json(msg='shade is required for this module')
|
||||
|
||||
state = module.params['state']
|
||||
|
||||
if state == 'present' and not module.params['size']:
|
||||
module.fail_json(msg="Size is required when state is 'present'")
|
||||
|
||||
try:
|
||||
cloud = shade.openstack_cloud(**module.params)
|
||||
if state == 'present':
|
||||
_present_volume(module, cloud)
|
||||
if state == 'absent':
|
||||
_absent_volume(module, cloud)
|
||||
except shade.OpenStackCloudException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue