mirror of https://github.com/ansible/ansible.git
changes for package loading of modules
parent
f35ed8a6c0
commit
e5116d2f9b
@ -1 +1 @@
|
||||
Subproject commit 417309a626e39396196a4abc6208c9f6db158f9d
|
||||
Subproject commit 385a037cd6bc42fc64e387973c0e7ef539b04df7
|
@ -1 +1 @@
|
||||
Subproject commit 50f105578a07361e60f95a63da6acb5660765871
|
||||
Subproject commit 110250d344be156387d08ea837f4bcb2c42034b4
|
@ -1,484 +0,0 @@
|
||||
#!/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: azure
|
||||
short_description: create or terminate a virtual machine in azure
|
||||
description:
|
||||
- Creates or terminates azure instances. When created optionally waits for it to be 'running'. This module has a dependency on python-azure >= 0.7.1
|
||||
version_added: "1.7"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of the virtual machine and associated cloud service.
|
||||
required: true
|
||||
default: null
|
||||
location:
|
||||
description:
|
||||
- the azure location to use (e.g. 'East US')
|
||||
required: true
|
||||
default: null
|
||||
subscription_id:
|
||||
description:
|
||||
- azure subscription id. Overrides the AZURE_SUBSCRIPTION_ID environement variable.
|
||||
required: false
|
||||
default: null
|
||||
management_cert_path:
|
||||
description:
|
||||
- path to an azure management certificate associated with the subscription id. Overrides the AZURE_CERT_PATH environement variable.
|
||||
required: false
|
||||
default: null
|
||||
storage_account:
|
||||
description:
|
||||
- the azure storage account in which to store the data disks.
|
||||
required: true
|
||||
image:
|
||||
description:
|
||||
- system image for creating the virtual machine (e.g., b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu_DAILY_BUILD-precise-12_04_3-LTS-amd64-server-20131205-en-us-30GB)
|
||||
required: true
|
||||
default: null
|
||||
role_size:
|
||||
description:
|
||||
- azure role size for the new virtual machine (e.g., Small, ExtraLarge, A6)
|
||||
required: false
|
||||
default: Small
|
||||
endpoints:
|
||||
description:
|
||||
- a comma-separated list of TCP ports to expose on the virtual machine (e.g., "22,80")
|
||||
required: false
|
||||
default: 22
|
||||
user:
|
||||
description:
|
||||
- the unix username for the new virtual machine.
|
||||
required: false
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- the unix password for the new virtual machine.
|
||||
required: false
|
||||
default: null
|
||||
ssh_cert_path:
|
||||
description:
|
||||
- path to an X509 certificate containing the public ssh key to install in the virtual machine. See http://www.windowsazure.com/en-us/manage/linux/tutorials/intro-to-linux/ for more details.
|
||||
- if this option is specified, password-based ssh authentication will be disabled.
|
||||
required: false
|
||||
default: null
|
||||
virtual_network_name:
|
||||
description:
|
||||
- Name of virtual network.
|
||||
required: false
|
||||
default: null
|
||||
hostname:
|
||||
description:
|
||||
- hostname to write /etc/hostname. Defaults to <name>.cloudapp.net.
|
||||
required: false
|
||||
default: null
|
||||
wait:
|
||||
description:
|
||||
- wait for the instance to be in state 'running' before returning
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 600
|
||||
aliases: []
|
||||
wait_timeout_redirects:
|
||||
description:
|
||||
- how long before wait gives up for redirects, in seconds
|
||||
default: 300
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- create or terminate instances
|
||||
required: false
|
||||
default: 'present'
|
||||
aliases: []
|
||||
|
||||
requirements: [ "azure" ]
|
||||
author: John Whitbeck
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Note: None of these examples set subscription_id or management_cert_path
|
||||
# It is assumed that their matching environment variables are set.
|
||||
|
||||
# Provision virtual machine example
|
||||
- local_action:
|
||||
module: azure
|
||||
name: my-virtual-machine
|
||||
role_size: Small
|
||||
image: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu_DAILY_BUILD-precise-12_04_3-LTS-amd64-server-20131205-en-us-30GB
|
||||
location: 'East US'
|
||||
user: ubuntu
|
||||
ssh_cert_path: /path/to/azure_x509_cert.pem
|
||||
storage_account: my-storage-account
|
||||
wait: yes
|
||||
|
||||
# Terminate virtual machine example
|
||||
- local_action:
|
||||
module: azure
|
||||
name: my-virtual-machine
|
||||
state: absent
|
||||
'''
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urlparse import urlparse
|
||||
|
||||
AZURE_LOCATIONS = ['South Central US',
|
||||
'Central US',
|
||||
'East US 2',
|
||||
'East US',
|
||||
'West US',
|
||||
'North Central US',
|
||||
'North Europe',
|
||||
'West Europe',
|
||||
'East Asia',
|
||||
'Southeast Asia',
|
||||
'Japan West',
|
||||
'Japan East',
|
||||
'Brazil South']
|
||||
|
||||
AZURE_ROLE_SIZES = ['ExtraSmall',
|
||||
'Small',
|
||||
'Medium',
|
||||
'Large',
|
||||
'ExtraLarge',
|
||||
'A5',
|
||||
'A6',
|
||||
'A7',
|
||||
'A8',
|
||||
'A9',
|
||||
'Basic_A0',
|
||||
'Basic_A1',
|
||||
'Basic_A2',
|
||||
'Basic_A3',
|
||||
'Basic_A4']
|
||||
|
||||
try:
|
||||
import azure as windows_azure
|
||||
|
||||
from azure import WindowsAzureError, WindowsAzureMissingResourceError
|
||||
from azure.servicemanagement import (ServiceManagementService, OSVirtualHardDisk, SSH, PublicKeys,
|
||||
PublicKey, LinuxConfigurationSet, ConfigurationSetInputEndpoints,
|
||||
ConfigurationSetInputEndpoint)
|
||||
except ImportError:
|
||||
print "failed=True msg='azure required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from types import MethodType
|
||||
import json
|
||||
|
||||
|
||||
def _wait_for_completion(azure, promise, wait_timeout, msg):
|
||||
if not promise: return
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait_timeout > time.time():
|
||||
operation_result = azure.get_operation_status(promise.request_id)
|
||||
time.sleep(5)
|
||||
if operation_result.status == "Succeeded":
|
||||
return
|
||||
|
||||
raise WindowsAzureError('Timed out waiting for async operation ' + msg + ' "' + str(promise.request_id) + '" to complete.')
|
||||
|
||||
|
||||
def get_ssh_certificate_tokens(module, ssh_cert_path):
|
||||
"""
|
||||
Returns the sha1 fingerprint and a base64-encoded PKCS12 version of the certificate.
|
||||
"""
|
||||
# This returns a string such as SHA1 Fingerprint=88:60:0B:13:A9:14:47:DA:4E:19:10:7D:34:92:2B:DF:A1:7D:CA:FF
|
||||
rc, stdout, stderr = module.run_command(['openssl', 'x509', '-in', ssh_cert_path, '-fingerprint', '-noout'])
|
||||
if rc != 0:
|
||||
module.fail_json(msg="failed to generate the key fingerprint, error was: %s" % stderr)
|
||||
fingerprint = stdout.strip()[17:].replace(':', '')
|
||||
|
||||
rc, stdout, stderr = module.run_command(['openssl', 'pkcs12', '-export', '-in', ssh_cert_path, '-nokeys', '-password', 'pass:'])
|
||||
if rc != 0:
|
||||
module.fail_json(msg="failed to generate the pkcs12 signature from the certificate, error was: %s" % stderr)
|
||||
pkcs12_base64 = base64.b64encode(stdout.strip())
|
||||
|
||||
return (fingerprint, pkcs12_base64)
|
||||
|
||||
|
||||
def create_virtual_machine(module, azure):
|
||||
"""
|
||||
Create new virtual machine
|
||||
|
||||
module : AnsibleModule object
|
||||
azure: authenticated azure ServiceManagementService object
|
||||
|
||||
Returns:
|
||||
True if a new virtual machine was created, false otherwise
|
||||
"""
|
||||
name = module.params.get('name')
|
||||
hostname = module.params.get('hostname') or name + ".cloudapp.net"
|
||||
endpoints = module.params.get('endpoints').split(',')
|
||||
ssh_cert_path = module.params.get('ssh_cert_path')
|
||||
user = module.params.get('user')
|
||||
password = module.params.get('password')
|
||||
location = module.params.get('location')
|
||||
role_size = module.params.get('role_size')
|
||||
storage_account = module.params.get('storage_account')
|
||||
image = module.params.get('image')
|
||||
virtual_network_name = module.params.get('virtual_network_name')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
# Check if a deployment with the same name already exists
|
||||
cloud_service_name_available = azure.check_hosted_service_name_availability(name)
|
||||
if not cloud_service_name_available.result:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
# Create cloud service if necessary
|
||||
try:
|
||||
result = azure.create_hosted_service(service_name=name, label=name, location=location)
|
||||
_wait_for_completion(azure, result, wait_timeout, "create_hosted_service")
|
||||
except WindowsAzureError as e:
|
||||
module.fail_json(msg="failed to create the new service name, it already exists: %s" % str(e))
|
||||
|
||||
# Create linux configuration
|
||||
disable_ssh_password_authentication = not password
|
||||
linux_config = LinuxConfigurationSet(hostname, user, password, disable_ssh_password_authentication)
|
||||
|
||||
# Add ssh certificates if specified
|
||||
if ssh_cert_path:
|
||||
fingerprint, pkcs12_base64 = get_ssh_certificate_tokens(module, ssh_cert_path)
|
||||
# Add certificate to cloud service
|
||||
result = azure.add_service_certificate(name, pkcs12_base64, 'pfx', '')
|
||||
_wait_for_completion(azure, result, wait_timeout, "add_service_certificate")
|
||||
|
||||
# Create ssh config
|
||||
ssh_config = SSH()
|
||||
ssh_config.public_keys = PublicKeys()
|
||||
authorized_keys_path = u'/home/%s/.ssh/authorized_keys' % user
|
||||
ssh_config.public_keys.public_keys.append(PublicKey(path=authorized_keys_path, fingerprint=fingerprint))
|
||||
# Append ssh config to linux machine config
|
||||
linux_config.ssh = ssh_config
|
||||
|
||||
# Create network configuration
|
||||
network_config = ConfigurationSetInputEndpoints()
|
||||
network_config.configuration_set_type = 'NetworkConfiguration'
|
||||
network_config.subnet_names = []
|
||||
for port in endpoints:
|
||||
network_config.input_endpoints.append(ConfigurationSetInputEndpoint(name='TCP-%s' % port,
|
||||
protocol='TCP',
|
||||
port=port,
|
||||
local_port=port))
|
||||
|
||||
# First determine where to store disk
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
disk_prefix = u'%s-%s' % (name, name)
|
||||
media_link = u'http://%s.blob.core.windows.net/vhds/%s-%s.vhd' % (storage_account, disk_prefix, today)
|
||||
# Create system hard disk
|
||||
os_hd = OSVirtualHardDisk(image, media_link)
|
||||
|
||||
# Spin up virtual machine
|
||||
try:
|
||||
result = azure.create_virtual_machine_deployment(service_name=name,
|
||||
deployment_name=name,
|
||||
deployment_slot='production',
|
||||
label=name,
|
||||
role_name=name,
|
||||
system_config=linux_config,
|
||||
network_config=network_config,
|
||||
os_virtual_hard_disk=os_hd,
|
||||
role_size=role_size,
|
||||
role_type='PersistentVMRole',
|
||||
virtual_network_name=virtual_network_name)
|
||||
_wait_for_completion(azure, result, wait_timeout, "create_virtual_machine_deployment")
|
||||
except WindowsAzureError as e:
|
||||
module.fail_json(msg="failed to create the new virtual machine, error was: %s" % str(e))
|
||||
|
||||
|
||||
try:
|
||||
deployment = azure.get_deployment_by_name(service_name=name, deployment_name=name)
|
||||
return (changed, urlparse(deployment.url).hostname, deployment)
|
||||
except WindowsAzureError as e:
|
||||
module.fail_json(msg="failed to lookup the deployment information for %s, error was: %s" % (name, str(e)))
|
||||
|
||||
|
||||
def terminate_virtual_machine(module, azure):
|
||||
"""
|
||||
Terminates a virtual machine
|
||||
|
||||
module : AnsibleModule object
|
||||
azure: authenticated azure ServiceManagementService object
|
||||
|
||||
Not yet supported: handle deletion of attached data disks.
|
||||
|
||||
Returns:
|
||||
True if a new virtual machine was deleted, false otherwise
|
||||
"""
|
||||
|
||||
# Whether to wait for termination to complete before returning
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
name = module.params.get('name')
|
||||
delete_empty_services = module.params.get('delete_empty_services')
|
||||
|
||||
changed = False
|
||||
|
||||
deployment = None
|
||||
public_dns_name = None
|
||||
disk_names = []
|
||||
try:
|
||||
deployment = azure.get_deployment_by_name(service_name=name, deployment_name=name)
|
||||
except WindowsAzureMissingResourceError as e:
|
||||
pass # no such deployment or service
|
||||
except WindowsAzureError as e:
|
||||
module.fail_json(msg="failed to find the deployment, error was: %s" % str(e))
|
||||
|
||||
# Delete deployment
|
||||
if deployment:
|
||||
changed = True
|
||||
try:
|
||||
# gather disk info
|
||||
results = []
|
||||
for role in deployment.role_list:
|
||||
role_props = azure.get_role(name, deployment.name, role.role_name)
|
||||
if role_props.os_virtual_hard_disk.disk_name not in disk_names:
|
||||
disk_names.append(role_props.os_virtual_hard_disk.disk_name)
|
||||
|
||||
result = azure.delete_deployment(name, deployment.name)
|
||||
_wait_for_completion(azure, result, wait_timeout, "delete_deployment")
|
||||
|
||||
for disk_name in disk_names:
|
||||
azure.delete_disk(disk_name, True)
|
||||
|
||||
# Now that the vm is deleted, remove the cloud service
|
||||
result = azure.delete_hosted_service(service_name=name)
|
||||
_wait_for_completion(azure, result, wait_timeout, "delete_hosted_service")
|
||||
except WindowsAzureError as e:
|
||||
module.fail_json(msg="failed to delete the service %s, error was: %s" % (name, str(e)))
|
||||
public_dns_name = urlparse(deployment.url).hostname
|
||||
|
||||
return changed, public_dns_name, deployment
|
||||
|
||||
|
||||
def get_azure_creds(module):
|
||||
# Check modul args for credentials, then check environment vars
|
||||
subscription_id = module.params.get('subscription_id')
|
||||
if not subscription_id:
|
||||
subscription_id = os.environ.get('AZURE_SUBSCRIPTION_ID', None)
|
||||
if not subscription_id:
|
||||
module.fail_json(msg="No subscription_id provided. Please set 'AZURE_SUBSCRIPTION_ID' or use the 'subscription_id' parameter")
|
||||
|
||||
management_cert_path = module.params.get('management_cert_path')
|
||||
if not management_cert_path:
|
||||
management_cert_path = os.environ.get('AZURE_CERT_PATH', None)
|
||||
if not management_cert_path:
|
||||
module.fail_json(msg="No management_cert_path provided. Please set 'AZURE_CERT_PATH' or use the 'management_cert_path' parameter")
|
||||
|
||||
return subscription_id, management_cert_path
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
ssh_cert_path=dict(),
|
||||
name=dict(),
|
||||
hostname=dict(),
|
||||
location=dict(choices=AZURE_LOCATIONS),
|
||||
role_size=dict(choices=AZURE_ROLE_SIZES),
|
||||
subscription_id=dict(no_log=True),
|
||||
storage_account=dict(),
|
||||
management_cert_path=dict(),
|
||||
endpoints=dict(default='22'),
|
||||
user=dict(),
|
||||
password=dict(),
|
||||
image=dict(),
|
||||
virtual_network_name=dict(default=None),
|
||||
state=dict(default='present'),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(default=600),
|
||||
wait_timeout_redirects=dict(default=300)
|
||||
)
|
||||
)
|
||||
# create azure ServiceManagementService object
|
||||
subscription_id, management_cert_path = get_azure_creds(module)
|
||||
|
||||
wait_timeout_redirects = int(module.params.get('wait_timeout_redirects'))
|
||||
if LooseVersion(windows_azure.__version__) <= "0.8.0":
|
||||
# wrapper for handling redirects which the sdk <= 0.8.0 is not following
|
||||
azure = Wrapper(ServiceManagementService(subscription_id, management_cert_path), wait_timeout_redirects)
|
||||
else:
|
||||
azure = ServiceManagementService(subscription_id, management_cert_path)
|
||||
|
||||
cloud_service_raw = None
|
||||
if module.params.get('state') == 'absent':
|
||||
(changed, public_dns_name, deployment) = terminate_virtual_machine(module, azure)
|
||||
|
||||
elif module.params.get('state') == 'present':
|
||||
# Changed is always set to true when provisioning new instances
|
||||
if not module.params.get('name'):
|
||||
module.fail_json(msg='name parameter is required for new instance')
|
||||
if not module.params.get('image'):
|
||||
module.fail_json(msg='image parameter is required for new instance')
|
||||
if not module.params.get('user'):
|
||||
module.fail_json(msg='user parameter is required for new instance')
|
||||
if not module.params.get('location'):
|
||||
module.fail_json(msg='location parameter is required for new instance')
|
||||
if not module.params.get('storage_account'):
|
||||
module.fail_json(msg='storage_account parameter is required for new instance')
|
||||
(changed, public_dns_name, deployment) = create_virtual_machine(module, azure)
|
||||
|
||||
module.exit_json(changed=changed, public_dns_name=public_dns_name, deployment=json.loads(json.dumps(deployment, default=lambda o: o.__dict__)))
|
||||
|
||||
|
||||
class Wrapper(object):
|
||||
def __init__(self, obj, wait_timeout):
|
||||
self.other = obj
|
||||
self.wait_timeout = wait_timeout
|
||||
|
||||
def __getattr__(self, name):
|
||||
if hasattr(self.other, name):
|
||||
func = getattr(self.other, name)
|
||||
return lambda *args, **kwargs: self._wrap(func, args, kwargs)
|
||||
raise AttributeError(name)
|
||||
|
||||
def _wrap(self, func, args, kwargs):
|
||||
if type(func) == MethodType:
|
||||
result = self._handle_temporary_redirects(lambda: func(*args, **kwargs))
|
||||
else:
|
||||
result = self._handle_temporary_redirects(lambda: func(self.other, *args, **kwargs))
|
||||
return result
|
||||
|
||||
def _handle_temporary_redirects(self, f):
|
||||
wait_timeout = time.time() + self.wait_timeout
|
||||
while wait_timeout > time.time():
|
||||
try:
|
||||
return f()
|
||||
except WindowsAzureError as e:
|
||||
if not str(e).lower().find("temporary redirect") == -1:
|
||||
time.sleep(5)
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,313 +0,0 @@
|
||||
#!/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: cloudformation
|
||||
short_description: create a AWS CloudFormation stack
|
||||
description:
|
||||
- Launches an AWS CloudFormation stack and waits for it complete.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
stack_name:
|
||||
description:
|
||||
- name of the cloudformation stack
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
disable_rollback:
|
||||
description:
|
||||
- If a stacks fails to form, rollback will remove the stack
|
||||
required: false
|
||||
default: "false"
|
||||
choices: [ "true", "false" ]
|
||||
aliases: []
|
||||
template_parameters:
|
||||
description:
|
||||
- a list of hashes of all the template variables for the stack
|
||||
required: false
|
||||
default: {}
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: true
|
||||
default: null
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
state:
|
||||
description:
|
||||
- If state is "present", stack will be created. If state is "present" and if stack exists and template has changed, it will be updated.
|
||||
If state is absent, stack will be removed.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
template:
|
||||
description:
|
||||
- the path of the cloudformation template
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
tags:
|
||||
description:
|
||||
- Dictionary of tags to associate with stack and it's resources during stack creation. Cannot be updated later.
|
||||
Requires at least Boto version 2.6.0.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.4"
|
||||
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' ]
|
||||
version_added: "1.5"
|
||||
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' ]
|
||||
version_added: "1.5"
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
version_added: "1.5"
|
||||
|
||||
requirements: [ "boto" ]
|
||||
author: James S. Martin
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic task example
|
||||
tasks:
|
||||
- name: launch ansible cloudformation example
|
||||
action: cloudformation >
|
||||
stack_name="ansible-cloudformation" state=present
|
||||
region=us-east-1 disable_rollback=true
|
||||
template=files/cloudformation-example.json
|
||||
args:
|
||||
template_parameters:
|
||||
KeyName: jmartin
|
||||
DiskType: ephemeral
|
||||
InstanceType: m1.small
|
||||
ClusterSize: 3
|
||||
tags:
|
||||
Stack: ansible-cloudformation
|
||||
'''
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto
|
||||
import boto.cloudformation.connection
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class Region:
|
||||
def __init__(self, region):
|
||||
'''connects boto to the region specified in the cloudformation template'''
|
||||
self.name = region
|
||||
self.endpoint = 'cloudformation.%s.amazonaws.com' % region
|
||||
|
||||
|
||||
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 boto_version_required(version_tuple):
|
||||
parts = boto.Version.split('.')
|
||||
boto_version = []
|
||||
try:
|
||||
for part in parts:
|
||||
boto_version.append(int(part))
|
||||
except:
|
||||
boto_version.append(-1)
|
||||
return tuple(boto_version) >= tuple(version_tuple)
|
||||
|
||||
|
||||
def stack_operation(cfn, stack_name, operation):
|
||||
'''gets the status of a stack while it is created/updated/deleted'''
|
||||
existed = []
|
||||
result = {}
|
||||
operation_complete = False
|
||||
while operation_complete == False:
|
||||
try:
|
||||
stack = cfn.describe_stacks(stack_name)[0]
|
||||
existed.append('yes')
|
||||
except:
|
||||
if 'yes' in existed:
|
||||
result = dict(changed=True,
|
||||
output='Stack Deleted',
|
||||
events=map(str, list(stack.describe_events())))
|
||||
else:
|
||||
result = dict(changed= True, output='Stack Not Found')
|
||||
break
|
||||
if '%s_COMPLETE' % operation == stack.stack_status:
|
||||
result = dict(changed=True,
|
||||
events = map(str, list(stack.describe_events())),
|
||||
output = 'Stack %s complete' % operation)
|
||||
break
|
||||
if 'ROLLBACK_COMPLETE' == stack.stack_status or '%s_ROLLBACK_COMPLETE' % operation == stack.stack_status:
|
||||
result = dict(changed=True, failed=True,
|
||||
events = map(str, list(stack.describe_events())),
|
||||
output = 'Problem with %s. Rollback complete' % operation)
|
||||
break
|
||||
elif '%s_FAILED' % operation == stack.stack_status:
|
||||
result = dict(changed=True, failed=True,
|
||||
events = map(str, list(stack.describe_events())),
|
||||
output = 'Stack %s failed' % operation)
|
||||
break
|
||||
else:
|
||||
time.sleep(5)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
stack_name=dict(required=True),
|
||||
template_parameters=dict(required=False, type='dict', default={}),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
template=dict(default=None, required=True),
|
||||
disable_rollback=dict(default=False, type='bool'),
|
||||
tags=dict(default=None)
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
stack_name = module.params['stack_name']
|
||||
template_body = open(module.params['template'], 'r').read()
|
||||
disable_rollback = module.params['disable_rollback']
|
||||
template_parameters = module.params['template_parameters']
|
||||
tags = module.params['tags']
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
kwargs = dict()
|
||||
if tags is not None:
|
||||
if not boto_version_required((2,6,0)):
|
||||
module.fail_json(msg='Module parameter "tags" requires at least Boto version 2.6.0')
|
||||
kwargs['tags'] = tags
|
||||
|
||||
|
||||
# convert the template parameters ansible passes into a tuple for boto
|
||||
template_parameters_tup = [(k, v) for k, v in template_parameters.items()]
|
||||
stack_outputs = {}
|
||||
|
||||
try:
|
||||
cf_region = Region(region)
|
||||
cfn = boto.cloudformation.connection.CloudFormationConnection(
|
||||
aws_access_key_id=aws_access_key,
|
||||
aws_secret_access_key=aws_secret_key,
|
||||
region=cf_region,
|
||||
)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
update = False
|
||||
result = {}
|
||||
operation = None
|
||||
|
||||
# if state is present we are going to ensure that the stack is either
|
||||
# created or updated
|
||||
if state == 'present':
|
||||
try:
|
||||
cfn.create_stack(stack_name, parameters=template_parameters_tup,
|
||||
template_body=template_body,
|
||||
disable_rollback=disable_rollback,
|
||||
capabilities=['CAPABILITY_IAM'],
|
||||
**kwargs)
|
||||
operation = 'CREATE'
|
||||
except Exception, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'AlreadyExistsException' in error_msg or 'already exists' in error_msg:
|
||||
update = True
|
||||
else:
|
||||
module.fail_json(msg=error_msg)
|
||||
if not update:
|
||||
result = stack_operation(cfn, stack_name, operation)
|
||||
|
||||
# if the state is present and the stack already exists, we try to update it
|
||||
# AWS will tell us if the stack template and parameters are the same and
|
||||
# don't need to be updated.
|
||||
if update:
|
||||
try:
|
||||
cfn.update_stack(stack_name, parameters=template_parameters_tup,
|
||||
template_body=template_body,
|
||||
disable_rollback=disable_rollback,
|
||||
capabilities=['CAPABILITY_IAM'])
|
||||
operation = 'UPDATE'
|
||||
except Exception, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'No updates are to be performed.' in error_msg:
|
||||
result = dict(changed=False, output='Stack is already up-to-date.')
|
||||
else:
|
||||
module.fail_json(msg=error_msg)
|
||||
|
||||
if operation == 'UPDATE':
|
||||
result = stack_operation(cfn, stack_name, operation)
|
||||
|
||||
# check the status of the stack while we are creating/updating it.
|
||||
# and get the outputs of the stack
|
||||
|
||||
if state == 'present' or update:
|
||||
stack = cfn.describe_stacks(stack_name)[0]
|
||||
for output in stack.outputs:
|
||||
stack_outputs[output.key] = output.value
|
||||
result['stack_outputs'] = stack_outputs
|
||||
|
||||
# absent state is different because of the way delete_stack works.
|
||||
# problem is it it doesn't give an error if stack isn't found
|
||||
# so must describe the stack first
|
||||
|
||||
if state == 'absent':
|
||||
try:
|
||||
cfn.describe_stacks(stack_name)
|
||||
operation = 'DELETE'
|
||||
except Exception, err:
|
||||
error_msg = boto_exception(err)
|
||||
if 'Stack:%s does not exist' % stack_name in error_msg:
|
||||
result = dict(changed=False, output='Stack not found.')
|
||||
else:
|
||||
module.fail_json(msg=error_msg)
|
||||
if operation == 'DELETE':
|
||||
cfn.delete_stack(stack_name)
|
||||
result = stack_operation(cfn, stack_name, operation)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,434 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: digital_ocean
|
||||
short_description: Create/delete a droplet/SSH_key in DigitalOcean
|
||||
description:
|
||||
- Create/delete a droplet in DigitalOcean and optionally wait for it to be 'running', or deploy an SSH key.
|
||||
version_added: "1.3"
|
||||
options:
|
||||
command:
|
||||
description:
|
||||
- Which target you want to operate on.
|
||||
default: droplet
|
||||
choices: ['droplet', 'ssh']
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the target.
|
||||
default: present
|
||||
choices: ['present', 'active', 'absent', 'deleted']
|
||||
client_id:
|
||||
description:
|
||||
- DigitalOcean manager id.
|
||||
api_key:
|
||||
description:
|
||||
- DigitalOcean api key.
|
||||
id:
|
||||
description:
|
||||
- Numeric, the droplet id you want to operate on.
|
||||
name:
|
||||
description:
|
||||
- String, this is the name of the droplet - must be formatted by hostname rules, or the name of a SSH key.
|
||||
unique_name:
|
||||
description:
|
||||
- Bool, require unique hostnames. By default, DigitalOcean allows multiple hosts with the same name. Setting this to "yes" allows only one host per name. Useful for idempotence.
|
||||
version_added: "1.4"
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
size_id:
|
||||
description:
|
||||
- Numeric, this is the id of the size you would like the droplet created with.
|
||||
image_id:
|
||||
description:
|
||||
- Numeric, this is the id of the image you would like the droplet created with.
|
||||
region_id:
|
||||
description:
|
||||
- "Numeric, this is the id of the region you would like your server to be created in."
|
||||
ssh_key_ids:
|
||||
description:
|
||||
- Optional, comma separated list of ssh_key_ids that you would like to be added to the server.
|
||||
virtio:
|
||||
description:
|
||||
- "Bool, turn on virtio driver in droplet for improved network and storage I/O."
|
||||
version_added: "1.4"
|
||||
default: "yes"
|
||||
choices: [ "yes", "no" ]
|
||||
private_networking:
|
||||
description:
|
||||
- "Bool, add an additional, private network interface to droplet for inter-droplet communication."
|
||||
version_added: "1.4"
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
backups_enabled:
|
||||
description:
|
||||
- Optional, Boolean, enables backups for your droplet.
|
||||
version_added: "1.6"
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
wait:
|
||||
description:
|
||||
- Wait for the droplet to be in state 'running' before returning. If wait is "no" an ip_address may not be returned.
|
||||
default: "yes"
|
||||
choices: [ "yes", "no" ]
|
||||
wait_timeout:
|
||||
description:
|
||||
- How long before wait gives up, in seconds.
|
||||
default: 300
|
||||
ssh_pub_key:
|
||||
description:
|
||||
- The public SSH key you want to add to your account.
|
||||
|
||||
notes:
|
||||
- Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY.
|
||||
requirements: [ dopy ]
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
# Ensure a SSH key is present
|
||||
# If a key matches this name, will return the ssh key id and changed = False
|
||||
# If no existing key matches this name, a new key is created, the ssh key id is returned and changed = False
|
||||
|
||||
- digital_ocean: >
|
||||
state=present
|
||||
command=ssh
|
||||
name=my_ssh_key
|
||||
ssh_pub_key='ssh-rsa AAAA...'
|
||||
client_id=XXX
|
||||
api_key=XXX
|
||||
|
||||
# Create a new Droplet
|
||||
# Will return the droplet details including the droplet id (used for idempotence)
|
||||
|
||||
- digital_ocean: >
|
||||
state=present
|
||||
command=droplet
|
||||
name=mydroplet
|
||||
client_id=XXX
|
||||
api_key=XXX
|
||||
size_id=1
|
||||
region_id=2
|
||||
image_id=3
|
||||
wait_timeout=500
|
||||
register: my_droplet
|
||||
- debug: msg="ID is {{ my_droplet.droplet.id }}"
|
||||
- debug: msg="IP is {{ my_droplet.droplet.ip_address }}"
|
||||
|
||||
# Ensure a droplet is present
|
||||
# If droplet id already exist, will return the droplet details and changed = False
|
||||
# If no droplet matches the id, a new droplet will be created and the droplet details (including the new id) are returned, changed = True.
|
||||
|
||||
- digital_ocean: >
|
||||
state=present
|
||||
command=droplet
|
||||
id=123
|
||||
name=mydroplet
|
||||
client_id=XXX
|
||||
api_key=XXX
|
||||
size_id=1
|
||||
region_id=2
|
||||
image_id=3
|
||||
wait_timeout=500
|
||||
|
||||
# Create a droplet with ssh key
|
||||
# The ssh key id can be passed as argument at the creation of a droplet (see ssh_key_ids).
|
||||
# Several keys can be added to ssh_key_ids as id1,id2,id3
|
||||
# The keys are used to connect as root to the droplet.
|
||||
|
||||
- digital_ocean: >
|
||||
state=present
|
||||
ssh_key_ids=id1,id2
|
||||
name=mydroplet
|
||||
client_id=XXX
|
||||
api_key=XXX
|
||||
size_id=1
|
||||
region_id=2
|
||||
image_id=3
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
import dopy
|
||||
from dopy.manager import DoError, DoManager
|
||||
except ImportError, e:
|
||||
print "failed=True msg='dopy >= 0.2.3 required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
if dopy.__version__ < '0.2.3':
|
||||
print "failed=True msg='dopy >= 0.2.3 required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
class TimeoutError(DoError):
|
||||
def __init__(self, msg, id):
|
||||
super(TimeoutError, self).__init__(msg)
|
||||
self.id = id
|
||||
|
||||
class JsonfyMixIn(object):
|
||||
def to_json(self):
|
||||
return self.__dict__
|
||||
|
||||
class Droplet(JsonfyMixIn):
|
||||
manager = None
|
||||
|
||||
def __init__(self, droplet_json):
|
||||
self.status = 'new'
|
||||
self.__dict__.update(droplet_json)
|
||||
|
||||
def is_powered_on(self):
|
||||
return self.status == 'active'
|
||||
|
||||
def update_attr(self, attrs=None):
|
||||
if attrs:
|
||||
for k, v in attrs.iteritems():
|
||||
setattr(self, k, v)
|
||||
else:
|
||||
json = self.manager.show_droplet(self.id)
|
||||
if json['ip_address']:
|
||||
self.update_attr(json)
|
||||
|
||||
def power_on(self):
|
||||
assert self.status == 'off', 'Can only power on a closed one.'
|
||||
json = self.manager.power_on_droplet(self.id)
|
||||
self.update_attr(json)
|
||||
|
||||
def ensure_powered_on(self, wait=True, wait_timeout=300):
|
||||
if self.is_powered_on():
|
||||
return
|
||||
if self.status == 'off': # powered off
|
||||
self.power_on()
|
||||
|
||||
if wait:
|
||||
end_time = time.time() + wait_timeout
|
||||
while time.time() < end_time:
|
||||
time.sleep(min(20, end_time - time.time()))
|
||||
self.update_attr()
|
||||
if self.is_powered_on():
|
||||
if not self.ip_address:
|
||||
raise TimeoutError('No ip is found.', self.id)
|
||||
return
|
||||
raise TimeoutError('Wait for droplet running timeout', self.id)
|
||||
|
||||
def destroy(self):
|
||||
return self.manager.destroy_droplet(self.id, scrub_data=True)
|
||||
|
||||
@classmethod
|
||||
def setup(cls, client_id, api_key):
|
||||
cls.manager = DoManager(client_id, api_key)
|
||||
|
||||
@classmethod
|
||||
def add(cls, name, size_id, image_id, region_id, ssh_key_ids=None, virtio=True, private_networking=False, backups_enabled=False):
|
||||
json = cls.manager.new_droplet(name, size_id, image_id, region_id, ssh_key_ids, virtio, private_networking, backups_enabled)
|
||||
droplet = cls(json)
|
||||
return droplet
|
||||
|
||||
@classmethod
|
||||
def find(cls, id=None, name=None):
|
||||
if not id and not name:
|
||||
return False
|
||||
|
||||
droplets = cls.list_all()
|
||||
|
||||
# Check first by id. digital ocean requires that it be unique
|
||||
for droplet in droplets:
|
||||
if droplet.id == id:
|
||||
return droplet
|
||||
|
||||
# Failing that, check by hostname.
|
||||
for droplet in droplets:
|
||||
if droplet.name == name:
|
||||
return droplet
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def list_all(cls):
|
||||
json = cls.manager.all_active_droplets()
|
||||
return map(cls, json)
|
||||
|
||||
class SSH(JsonfyMixIn):
|
||||
manager = None
|
||||
|
||||
def __init__(self, ssh_key_json):
|
||||
self.__dict__.update(ssh_key_json)
|
||||
update_attr = __init__
|
||||
|
||||
def destroy(self):
|
||||
self.manager.destroy_ssh_key(self.id)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def setup(cls, client_id, api_key):
|
||||
cls.manager = DoManager(client_id, api_key)
|
||||
|
||||
@classmethod
|
||||
def find(cls, name):
|
||||
if not name:
|
||||
return False
|
||||
keys = cls.list_all()
|
||||
for key in keys:
|
||||
if key.name == name:
|
||||
return key
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def list_all(cls):
|
||||
json = cls.manager.all_ssh_keys()
|
||||
return map(cls, json)
|
||||
|
||||
@classmethod
|
||||
def add(cls, name, key_pub):
|
||||
json = cls.manager.new_ssh_key(name, key_pub)
|
||||
return cls(json)
|
||||
|
||||
def core(module):
|
||||
def getkeyordie(k):
|
||||
v = module.params[k]
|
||||
if v is None:
|
||||
module.fail_json(msg='Unable to load %s' % k)
|
||||
return v
|
||||
|
||||
try:
|
||||
# params['client_id'] will be None even if client_id is not passed in
|
||||
client_id = module.params['client_id'] or os.environ['DO_CLIENT_ID']
|
||||
api_key = module.params['api_key'] or os.environ['DO_API_KEY']
|
||||
except KeyError, e:
|
||||
module.fail_json(msg='Unable to load %s' % e.message)
|
||||
|
||||
changed = True
|
||||
command = module.params['command']
|
||||
state = module.params['state']
|
||||
|
||||
if command == 'droplet':
|
||||
Droplet.setup(client_id, api_key)
|
||||
if state in ('active', 'present'):
|
||||
|
||||
# First, try to find a droplet by id.
|
||||
droplet = Droplet.find(id=module.params['id'])
|
||||
|
||||
# If we couldn't find the droplet and the user is allowing unique
|
||||
# hostnames, then check to see if a droplet with the specified
|
||||
# hostname already exists.
|
||||
if not droplet and module.params['unique_name']:
|
||||
droplet = Droplet.find(name=getkeyordie('name'))
|
||||
|
||||
# If both of those attempts failed, then create a new droplet.
|
||||
if not droplet:
|
||||
droplet = Droplet.add(
|
||||
name=getkeyordie('name'),
|
||||
size_id=getkeyordie('size_id'),
|
||||
image_id=getkeyordie('image_id'),
|
||||
region_id=getkeyordie('region_id'),
|
||||
ssh_key_ids=module.params['ssh_key_ids'],
|
||||
virtio=module.params['virtio'],
|
||||
private_networking=module.params['private_networking'],
|
||||
backups_enabled=module.params['backups_enabled'],
|
||||
)
|
||||
|
||||
if droplet.is_powered_on():
|
||||
changed = False
|
||||
|
||||
droplet.ensure_powered_on(
|
||||
wait=getkeyordie('wait'),
|
||||
wait_timeout=getkeyordie('wait_timeout')
|
||||
)
|
||||
|
||||
module.exit_json(changed=changed, droplet=droplet.to_json())
|
||||
|
||||
elif state in ('absent', 'deleted'):
|
||||
# First, try to find a droplet by id.
|
||||
droplet = Droplet.find(module.params['id'])
|
||||
|
||||
# If we couldn't find the droplet and the user is allowing unique
|
||||
# hostnames, then check to see if a droplet with the specified
|
||||
# hostname already exists.
|
||||
if not droplet and module.params['unique_name']:
|
||||
droplet = Droplet.find(name=getkeyordie('name'))
|
||||
|
||||
if not droplet:
|
||||
module.exit_json(changed=False, msg='The droplet is not found.')
|
||||
|
||||
event_json = droplet.destroy()
|
||||
module.exit_json(changed=True, event_id=event_json['event_id'])
|
||||
|
||||
elif command == 'ssh':
|
||||
SSH.setup(client_id, api_key)
|
||||
name = getkeyordie('name')
|
||||
if state in ('active', 'present'):
|
||||
key = SSH.find(name)
|
||||
if key:
|
||||
module.exit_json(changed=False, ssh_key=key.to_json())
|
||||
key = SSH.add(name, getkeyordie('ssh_pub_key'))
|
||||
module.exit_json(changed=True, ssh_key=key.to_json())
|
||||
|
||||
elif state in ('absent', 'deleted'):
|
||||
key = SSH.find(name)
|
||||
if not key:
|
||||
module.exit_json(changed=False, msg='SSH key with the name of %s is not found.' % name)
|
||||
key.destroy()
|
||||
module.exit_json(changed=True)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
command = dict(choices=['droplet', 'ssh'], default='droplet'),
|
||||
state = dict(choices=['active', 'present', 'absent', 'deleted'], default='present'),
|
||||
client_id = dict(aliases=['CLIENT_ID'], no_log=True),
|
||||
api_key = dict(aliases=['API_KEY'], no_log=True),
|
||||
name = dict(type='str'),
|
||||
size_id = dict(type='int'),
|
||||
image_id = dict(type='int'),
|
||||
region_id = dict(type='int'),
|
||||
ssh_key_ids = dict(default=''),
|
||||
virtio = dict(type='bool', default='yes'),
|
||||
private_networking = dict(type='bool', default='no'),
|
||||
backups_enabled = dict(type='bool', default='no'),
|
||||
id = dict(aliases=['droplet_id'], type='int'),
|
||||
unique_name = dict(type='bool', default='no'),
|
||||
wait = dict(type='bool', default=True),
|
||||
wait_timeout = dict(default=300, type='int'),
|
||||
ssh_pub_key = dict(type='str'),
|
||||
),
|
||||
required_together = (
|
||||
['size_id', 'image_id', 'region_id'],
|
||||
),
|
||||
mutually_exclusive = (
|
||||
['size_id', 'ssh_pub_key'],
|
||||
['image_id', 'ssh_pub_key'],
|
||||
['region_id', 'ssh_pub_key'],
|
||||
),
|
||||
required_one_of = (
|
||||
['id', 'name'],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
core(module)
|
||||
except TimeoutError, e:
|
||||
module.fail_json(msg=str(e), id=e.id)
|
||||
except (DoError, Exception), e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,242 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: digital_ocean_domain
|
||||
short_description: Create/delete a DNS record in DigitalOcean
|
||||
description:
|
||||
- Create/delete a DNS record in DigitalOcean.
|
||||
version_added: "1.6"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the target.
|
||||
default: present
|
||||
choices: ['present', 'active', 'absent', 'deleted']
|
||||
client_id:
|
||||
description:
|
||||
- DigitalOcean manager id.
|
||||
api_key:
|
||||
description:
|
||||
- DigitalOcean api key.
|
||||
id:
|
||||
description:
|
||||
- Numeric, the droplet id you want to operate on.
|
||||
name:
|
||||
description:
|
||||
- String, this is the name of the droplet - must be formatted by hostname rules, or the name of a SSH key, or the name of a domain.
|
||||
ip:
|
||||
description:
|
||||
- The IP address to point a domain at.
|
||||
|
||||
notes:
|
||||
- Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY.
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a domain record
|
||||
|
||||
- digital_ocean_domain: >
|
||||
state=present
|
||||
name=my.digitalocean.domain
|
||||
ip=127.0.0.1
|
||||
|
||||
# Create a droplet and a corresponding domain record
|
||||
|
||||
- digital_ocean: >
|
||||
state=present
|
||||
name=test_droplet
|
||||
size_id=1
|
||||
region_id=2
|
||||
image_id=3
|
||||
register: test_droplet
|
||||
|
||||
- digital_ocean_domain: >
|
||||
state=present
|
||||
name={{ test_droplet.droplet.name }}.my.domain
|
||||
ip={{ test_droplet.droplet.ip_address }}
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
from dopy.manager import DoError, DoManager
|
||||
except ImportError as e:
|
||||
print "failed=True msg='dopy required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
class TimeoutError(DoError):
|
||||
def __init__(self, msg, id):
|
||||
super(TimeoutError, self).__init__(msg)
|
||||
self.id = id
|
||||
|
||||
class JsonfyMixIn(object):
|
||||
def to_json(self):
|
||||
return self.__dict__
|
||||
|
||||
class DomainRecord(JsonfyMixIn):
|
||||
manager = None
|
||||
|
||||
def __init__(self, json):
|
||||
self.__dict__.update(json)
|
||||
update_attr = __init__
|
||||
|
||||
def update(self, data = None, record_type = None):
|
||||
json = self.manager.edit_domain_record(self.domain_id,
|
||||
self.id,
|
||||
record_type if record_type is not None else self.record_type,
|
||||
data if data is not None else self.data)
|
||||
self.__dict__.update(json)
|
||||
return self
|
||||
|
||||
def destroy(self):
|
||||
json = self.manager.destroy_domain_record(self.domain_id, self.id)
|
||||
return json
|
||||
|
||||
class Domain(JsonfyMixIn):
|
||||
manager = None
|
||||
|
||||
def __init__(self, domain_json):
|
||||
self.__dict__.update(domain_json)
|
||||
|
||||
def destroy(self):
|
||||
self.manager.destroy_domain(self.id)
|
||||
|
||||
def records(self):
|
||||
json = self.manager.all_domain_records(self.id)
|
||||
return map(DomainRecord, json)
|
||||
|
||||
@classmethod
|
||||
def add(cls, name, ip):
|
||||
json = cls.manager.new_domain(name, ip)
|
||||
return cls(json)
|
||||
|
||||
@classmethod
|
||||
def setup(cls, client_id, api_key):
|
||||
cls.manager = DoManager(client_id, api_key)
|
||||
DomainRecord.manager = cls.manager
|
||||
|
||||
@classmethod
|
||||
def list_all(cls):
|
||||
domains = cls.manager.all_domains()
|
||||
return map(cls, domains)
|
||||
|
||||
@classmethod
|
||||
def find(cls, name=None, id=None):
|
||||
if name is None and id is None:
|
||||
return False
|
||||
|
||||
domains = Domain.list_all()
|
||||
|
||||
if id is not None:
|
||||
for domain in domains:
|
||||
if domain.id == id:
|
||||
return domain
|
||||
|
||||
if name is not None:
|
||||
for domain in domains:
|
||||
if domain.name == name:
|
||||
return domain
|
||||
|
||||
return False
|
||||
|
||||
def core(module):
|
||||
def getkeyordie(k):
|
||||
v = module.params[k]
|
||||
if v is None:
|
||||
module.fail_json(msg='Unable to load %s' % k)
|
||||
return v
|
||||
|
||||
try:
|
||||
# params['client_id'] will be None even if client_id is not passed in
|
||||
client_id = module.params['client_id'] or os.environ['DO_CLIENT_ID']
|
||||
api_key = module.params['api_key'] or os.environ['DO_API_KEY']
|
||||
except KeyError, e:
|
||||
module.fail_json(msg='Unable to load %s' % e.message)
|
||||
|
||||
changed = True
|
||||
state = module.params['state']
|
||||
|
||||
Domain.setup(client_id, api_key)
|
||||
if state in ('present'):
|
||||
domain = Domain.find(id=module.params["id"])
|
||||
|
||||
if not domain:
|
||||
domain = Domain.find(name=getkeyordie("name"))
|
||||
|
||||
if not domain:
|
||||
domain = Domain.add(getkeyordie("name"),
|
||||
getkeyordie("ip"))
|
||||
module.exit_json(changed=True, domain=domain.to_json())
|
||||
else:
|
||||
records = domain.records()
|
||||
at_record = None
|
||||
for record in records:
|
||||
if record.name == "@":
|
||||
at_record = record
|
||||
|
||||
if not at_record.data == getkeyordie("ip"):
|
||||
record.update(data=getkeyordie("ip"), record_type='A')
|
||||
module.exit_json(changed=True, domain=Domain.find(id=record.domain_id).to_json())
|
||||
|
||||
module.exit_json(changed=False, domain=domain.to_json())
|
||||
|
||||
elif state in ('absent'):
|
||||
domain = None
|
||||
if "id" in module.params:
|
||||
domain = Domain.find(id=module.params["id"])
|
||||
|
||||
if not domain and "name" in module.params:
|
||||
domain = Domain.find(name=module.params["name"])
|
||||
|
||||
if not domain:
|
||||
module.exit_json(changed=False, msg="Domain not found.")
|
||||
|
||||
event_json = domain.destroy()
|
||||
module.exit_json(changed=True, event=event_json)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(choices=['active', 'present', 'absent', 'deleted'], default='present'),
|
||||
client_id = dict(aliases=['CLIENT_ID'], no_log=True),
|
||||
api_key = dict(aliases=['API_KEY'], no_log=True),
|
||||
name = dict(type='str'),
|
||||
id = dict(aliases=['droplet_id'], type='int'),
|
||||
ip = dict(type='str'),
|
||||
),
|
||||
required_one_of = (
|
||||
['id', 'name'],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
core(module)
|
||||
except TimeoutError as e:
|
||||
module.fail_json(msg=str(e), id=e.id)
|
||||
except (DoError, Exception) as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,178 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: digital_ocean_sshkey
|
||||
short_description: Create/delete an SSH key in DigitalOcean
|
||||
description:
|
||||
- Create/delete an SSH key.
|
||||
version_added: "1.6"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the target.
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
client_id:
|
||||
description:
|
||||
- DigitalOcean manager id.
|
||||
api_key:
|
||||
description:
|
||||
- DigitalOcean api key.
|
||||
id:
|
||||
description:
|
||||
- Numeric, the SSH key id you want to operate on.
|
||||
name:
|
||||
description:
|
||||
- String, this is the name of an SSH key to create or destroy.
|
||||
ssh_pub_key:
|
||||
description:
|
||||
- The public SSH key you want to add to your account.
|
||||
|
||||
notes:
|
||||
- Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY.
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
# Ensure a SSH key is present
|
||||
# If a key matches this name, will return the ssh key id and changed = False
|
||||
# If no existing key matches this name, a new key is created, the ssh key id is returned and changed = False
|
||||
|
||||
- digital_ocean_sshkey: >
|
||||
state=present
|
||||
name=my_ssh_key
|
||||
ssh_pub_key='ssh-rsa AAAA...'
|
||||
client_id=XXX
|
||||
api_key=XXX
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
from dopy.manager import DoError, DoManager
|
||||
except ImportError as e:
|
||||
print "failed=True msg='dopy required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
class TimeoutError(DoError):
|
||||
def __init__(self, msg, id):
|
||||
super(TimeoutError, self).__init__(msg)
|
||||
self.id = id
|
||||
|
||||
class JsonfyMixIn(object):
|
||||
def to_json(self):
|
||||
return self.__dict__
|
||||
|
||||
class SSH(JsonfyMixIn):
|
||||
manager = None
|
||||
|
||||
def __init__(self, ssh_key_json):
|
||||
self.__dict__.update(ssh_key_json)
|
||||
update_attr = __init__
|
||||
|
||||
def destroy(self):
|
||||
self.manager.destroy_ssh_key(self.id)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def setup(cls, client_id, api_key):
|
||||
cls.manager = DoManager(client_id, api_key)
|
||||
|
||||
@classmethod
|
||||
def find(cls, name):
|
||||
if not name:
|
||||
return False
|
||||
keys = cls.list_all()
|
||||
for key in keys:
|
||||
if key.name == name:
|
||||
return key
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def list_all(cls):
|
||||
json = cls.manager.all_ssh_keys()
|
||||
return map(cls, json)
|
||||
|
||||
@classmethod
|
||||
def add(cls, name, key_pub):
|
||||
json = cls.manager.new_ssh_key(name, key_pub)
|
||||
return cls(json)
|
||||
|
||||
def core(module):
|
||||
def getkeyordie(k):
|
||||
v = module.params[k]
|
||||
if v is None:
|
||||
module.fail_json(msg='Unable to load %s' % k)
|
||||
return v
|
||||
|
||||
try:
|
||||
# params['client_id'] will be None even if client_id is not passed in
|
||||
client_id = module.params['client_id'] or os.environ['DO_CLIENT_ID']
|
||||
api_key = module.params['api_key'] or os.environ['DO_API_KEY']
|
||||
except KeyError, e:
|
||||
module.fail_json(msg='Unable to load %s' % e.message)
|
||||
|
||||
changed = True
|
||||
state = module.params['state']
|
||||
|
||||
SSH.setup(client_id, api_key)
|
||||
name = getkeyordie('name')
|
||||
if state in ('present'):
|
||||
key = SSH.find(name)
|
||||
if key:
|
||||
module.exit_json(changed=False, ssh_key=key.to_json())
|
||||
key = SSH.add(name, getkeyordie('ssh_pub_key'))
|
||||
module.exit_json(changed=True, ssh_key=key.to_json())
|
||||
|
||||
elif state in ('absent'):
|
||||
key = SSH.find(name)
|
||||
if not key:
|
||||
module.exit_json(changed=False, msg='SSH key with the name of %s is not found.' % name)
|
||||
key.destroy()
|
||||
module.exit_json(changed=True)
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(choices=['present', 'absent'], default='present'),
|
||||
client_id = dict(aliases=['CLIENT_ID'], no_log=True),
|
||||
api_key = dict(aliases=['API_KEY'], no_log=True),
|
||||
name = dict(type='str'),
|
||||
id = dict(aliases=['droplet_id'], type='int'),
|
||||
ssh_pub_key = dict(type='str'),
|
||||
),
|
||||
required_one_of = (
|
||||
['id', 'name'],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
core(module)
|
||||
except TimeoutError as e:
|
||||
module.fail_json(msg=str(e), id=e.id)
|
||||
except (DoError, Exception) as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,854 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Cove Schneider
|
||||
# (c) 2014, Joshua Conner <joshua.conner@gmail.com>
|
||||
# (c) 2014, Pavel Antonov <antonov@adwz.ru>
|
||||
#
|
||||
# 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: docker
|
||||
version_added: "1.4"
|
||||
short_description: manage docker containers
|
||||
description:
|
||||
- Manage the life cycle of docker containers.
|
||||
options:
|
||||
count:
|
||||
description:
|
||||
- Set number of containers to run
|
||||
required: False
|
||||
default: 1
|
||||
aliases: []
|
||||
image:
|
||||
description:
|
||||
- Set container image to use
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
command:
|
||||
description:
|
||||
- Set command to run in a container on startup
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- Set name for container (used to find single container or to provide links)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
ports:
|
||||
description:
|
||||
- Set private to public port mapping specification using docker CLI-style syntax [([<host_interface>:[host_port]])|(<host_port>):]<container_port>[/udp]
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
expose:
|
||||
description:
|
||||
- Set container ports to expose for port mappings or links. (If the port is already exposed using EXPOSE in a Dockerfile, you don't need to expose it again.)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
publish_all_ports:
|
||||
description:
|
||||
- Publish all exposed ports to the host interfaces
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
volumes:
|
||||
description:
|
||||
- Set volume(s) to mount on the container
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
volumes_from:
|
||||
description:
|
||||
- Set shared volume(s) from another container
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
links:
|
||||
description:
|
||||
- Link container(s) to other container(s) (e.g. links=redis,postgresql:db)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
memory_limit:
|
||||
description:
|
||||
- Set RAM allocated to container
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
default: 256MB
|
||||
docker_url:
|
||||
description:
|
||||
- URL of docker host to issue commands to
|
||||
required: false
|
||||
default: unix://var/run/docker.sock
|
||||
aliases: []
|
||||
docker_api_version:
|
||||
description:
|
||||
- Remote API version to use. This defaults to the current default as specified by docker-py.
|
||||
required: false
|
||||
default: docker-py default remote API version
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
username:
|
||||
description:
|
||||
- Set remote API username
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
password:
|
||||
description:
|
||||
- Set remote API password
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
hostname:
|
||||
description:
|
||||
- Set container hostname
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
env:
|
||||
description:
|
||||
- Set environment variables (e.g. env="PASSWORD=sEcRe7,WORKERS=4")
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
dns:
|
||||
description:
|
||||
- Set custom DNS servers for the container
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
detach:
|
||||
description:
|
||||
- Enable detached mode on start up, leaves container running in background
|
||||
required: false
|
||||
default: true
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Set the state of the container
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "running", "stopped", "absent", "killed", "restarted" ]
|
||||
aliases: []
|
||||
privileged:
|
||||
description:
|
||||
- Set whether the container should run in privileged mode
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
lxc_conf:
|
||||
description:
|
||||
- LXC config parameters, e.g. lxc.aa_profile:unconfined
|
||||
required: false
|
||||
default:
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- Set the name of the container (cannot use with count)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
stdin_open:
|
||||
description:
|
||||
- Keep stdin open
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
tty:
|
||||
description:
|
||||
- Allocate a pseudo-tty
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
net:
|
||||
description:
|
||||
- Set Network mode for the container (bridge, none, container:<name|id>, host). Requires docker >= 0.11.
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
registry:
|
||||
description:
|
||||
- The remote registry URL to use for pulling images.
|
||||
required: false
|
||||
default: ''
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
|
||||
author: Cove Schneider, Joshua Conner, Pavel Antonov
|
||||
requirements: [ "docker-py >= 0.3.0", "docker >= 0.10.0" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
Start one docker container running tomcat in each host of the web group and bind tomcat's listening port to 8080
|
||||
on the host:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" ports=8080
|
||||
|
||||
The tomcat server's port is NAT'ed to a dynamic port on the host, but you can determine which port the server was
|
||||
mapped to using docker_containers:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" ports=8080 count=5
|
||||
- name: Display IP address and port mappings for containers
|
||||
debug: msg={{inventory_hostname}}:{{item['HostConfig']['PortBindings']['8080/tcp'][0]['HostPort']}}
|
||||
with_items: docker_containers
|
||||
|
||||
Just as in the previous example, but iterates over the list of docker containers with a sequence:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
vars:
|
||||
start_containers_count: 5
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" ports=8080 count={{start_containers_count}}
|
||||
- name: Display IP address and port mappings for containers
|
||||
debug: msg="{{inventory_hostname}}:{{docker_containers[{{item}}]['HostConfig']['PortBindings']['8080/tcp'][0]['HostPort']}}"
|
||||
with_sequence: start=0 end={{start_containers_count - 1}}
|
||||
|
||||
Stop, remove all of the running tomcat containers and list the exit code from the stopped containers:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: stop tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" state=absent
|
||||
- name: Display return codes from stopped containers
|
||||
debug: msg="Returned {{inventory_hostname}}:{{item}}"
|
||||
with_items: docker_containers
|
||||
|
||||
Create a named container:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat server
|
||||
docker: image=centos name=tomcat command="service tomcat6 start" ports=8080
|
||||
|
||||
Create multiple named containers:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos name={{item}} command="service tomcat6 start" ports=8080
|
||||
with_items:
|
||||
- crookshank
|
||||
- snowbell
|
||||
- heathcliff
|
||||
- felix
|
||||
- sylvester
|
||||
|
||||
Create containers named in a sequence:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos name={{item}} command="service tomcat6 start" ports=8080
|
||||
with_sequence: start=1 end=5 format=tomcat_%d.example.com
|
||||
|
||||
Create two linked containers:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: ensure redis container is running
|
||||
docker: image=crosbymichael/redis name=redis
|
||||
|
||||
- name: ensure redis_ambassador container is running
|
||||
docker: image=svendowideit/ambassador ports=6379:6379 links=redis:redis name=redis_ambassador_ansible
|
||||
|
||||
Create containers with options specified as key-value pairs and lists:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- docker:
|
||||
image: namespace/image_name
|
||||
links:
|
||||
- postgresql:db
|
||||
- redis:redis
|
||||
|
||||
|
||||
Create containers with options specified as strings and lists as comma-separated strings:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
docker: image=namespace/image_name links=postgresql:db,redis:redis
|
||||
|
||||
Create a container with no networking:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
docker: image=namespace/image_name net=none
|
||||
|
||||
'''
|
||||
|
||||
HAS_DOCKER_PY = True
|
||||
|
||||
import sys
|
||||
from urlparse import urlparse
|
||||
try:
|
||||
import docker.client
|
||||
import docker.utils
|
||||
from requests.exceptions import *
|
||||
except ImportError, e:
|
||||
HAS_DOCKER_PY = False
|
||||
|
||||
try:
|
||||
from docker.errors import APIError as DockerAPIError
|
||||
except ImportError:
|
||||
from docker.client import APIError as DockerAPIError
|
||||
|
||||
|
||||
def _human_to_bytes(number):
|
||||
suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
|
||||
if isinstance(number, int):
|
||||
return number
|
||||
if number[-1] == suffixes[0] and number[-2].isdigit():
|
||||
return number[:-1]
|
||||
|
||||
i = 1
|
||||
for each in suffixes[1:]:
|
||||
if number[-len(each):] == suffixes[i]:
|
||||
return int(number[:-len(each)]) * (1024 ** i)
|
||||
i = i + 1
|
||||
|
||||
print "failed=True msg='Could not convert %s to integer'" % (number)
|
||||
sys.exit(1)
|
||||
|
||||
def _ansible_facts(container_list):
|
||||
return {"docker_containers": container_list}
|
||||
|
||||
def _docker_id_quirk(inspect):
|
||||
# XXX: some quirk in docker
|
||||
if 'ID' in inspect:
|
||||
inspect['Id'] = inspect['ID']
|
||||
del inspect['ID']
|
||||
return inspect
|
||||
|
||||
class DockerManager:
|
||||
|
||||
counters = {'created':0, 'started':0, 'stopped':0, 'killed':0, 'removed':0, 'restarted':0, 'pull':0}
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.binds = None
|
||||
self.volumes = None
|
||||
if self.module.params.get('volumes'):
|
||||
self.binds = {}
|
||||
self.volumes = {}
|
||||
vols = self.module.params.get('volumes')
|
||||
for vol in vols:
|
||||
parts = vol.split(":")
|
||||
# host mount (e.g. /mnt:/tmp, bind mounts host's /tmp to /mnt in the container)
|
||||
if len(parts) == 2:
|
||||
self.volumes[parts[1]] = {}
|
||||
self.binds[parts[0]] = parts[1]
|
||||
# with bind mode
|
||||
elif len(parts) == 3:
|
||||
if parts[2] not in ['ro', 'rw']:
|
||||
self.module.fail_json(msg='bind mode needs to either be "ro" or "rw"')
|
||||
ro = parts[2] == 'ro'
|
||||
self.volumes[parts[1]] = {}
|
||||
self.binds[parts[0]] = {'bind': parts[1], 'ro': ro}
|
||||
# docker mount (e.g. /www, mounts a docker volume /www on the container at the same location)
|
||||
else:
|
||||
self.volumes[parts[0]] = {}
|
||||
|
||||
self.lxc_conf = None
|
||||
if self.module.params.get('lxc_conf'):
|
||||
self.lxc_conf = []
|
||||
options = self.module.params.get('lxc_conf')
|
||||
for option in options:
|
||||
parts = option.split(':')
|
||||
self.lxc_conf.append({"Key": parts[0], "Value": parts[1]})
|
||||
|
||||
self.exposed_ports = None
|
||||
if self.module.params.get('expose'):
|
||||
self.exposed_ports = self.get_exposed_ports(self.module.params.get('expose'))
|
||||
|
||||
self.port_bindings = None
|
||||
if self.module.params.get('ports'):
|
||||
self.port_bindings = self.get_port_bindings(self.module.params.get('ports'))
|
||||
|
||||
self.links = None
|
||||
if self.module.params.get('links'):
|
||||
self.links = self.get_links(self.module.params.get('links'))
|
||||
|
||||
self.env = self.module.params.get('env', None)
|
||||
|
||||
# connect to docker server
|
||||
docker_url = urlparse(module.params.get('docker_url'))
|
||||
docker_api_version = module.params.get('docker_api_version')
|
||||
self.client = docker.Client(base_url=docker_url.geturl(), version=docker_api_version)
|
||||
|
||||
|
||||
def get_links(self, links):
|
||||
"""
|
||||
Parse the links passed, if a link is specified without an alias then just create the alias of the same name as the link
|
||||
"""
|
||||
processed_links = {}
|
||||
|
||||
for link in links:
|
||||
parsed_link = link.split(':', 1)
|
||||
if(len(parsed_link) == 2):
|
||||
processed_links[parsed_link[0]] = parsed_link[1]
|
||||
else:
|
||||
processed_links[parsed_link[0]] = parsed_link[0]
|
||||
|
||||
return processed_links
|
||||
|
||||
|
||||
def get_exposed_ports(self, expose_list):
|
||||
"""
|
||||
Parse the ports and protocols (TCP/UDP) to expose in the docker-py `create_container` call from the docker CLI-style syntax.
|
||||
"""
|
||||
if expose_list:
|
||||
exposed = []
|
||||
for port in expose_list:
|
||||
port = str(port).strip()
|
||||
if port.endswith('/tcp') or port.endswith('/udp'):
|
||||
port_with_proto = tuple(port.split('/'))
|
||||
else:
|
||||
# assume tcp protocol if not specified
|
||||
port_with_proto = (port, 'tcp')
|
||||
exposed.append(port_with_proto)
|
||||
return exposed
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_port_bindings(self, ports):
|
||||
"""
|
||||
Parse the `ports` string into a port bindings dict for the `start_container` call.
|
||||
"""
|
||||
binds = {}
|
||||
for port in ports:
|
||||
# ports could potentially be an array like [80, 443], so we make sure they're strings
|
||||
# before splitting
|
||||
parts = str(port).split(':')
|
||||
container_port = parts[-1]
|
||||
if '/' not in container_port:
|
||||
container_port = int(parts[-1])
|
||||
|
||||
p_len = len(parts)
|
||||
if p_len == 1:
|
||||
# Bind `container_port` of the container to a dynamically
|
||||
# allocated TCP port on all available interfaces of the host
|
||||
# machine.
|
||||
bind = ('0.0.0.0',)
|
||||
elif p_len == 2:
|
||||
# Bind `container_port` of the container to port `parts[0]` on
|
||||
# all available interfaces of the host machine.
|
||||
bind = ('0.0.0.0', int(parts[0]))
|
||||
elif p_len == 3:
|
||||
# Bind `container_port` of the container to port `parts[1]` on
|
||||
# IP `parts[0]` of the host machine. If `parts[1]` empty bind
|
||||
# to a dynamically allocacted port of IP `parts[0]`.
|
||||
bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],)
|
||||
|
||||
if container_port in binds:
|
||||
old_bind = binds[container_port]
|
||||
if isinstance(old_bind, list):
|
||||
# append to list if it already exists
|
||||
old_bind.append(bind)
|
||||
else:
|
||||
# otherwise create list that contains the old and new binds
|
||||
binds[container_port] = [binds[container_port], bind]
|
||||
else:
|
||||
binds[container_port] = bind
|
||||
|
||||
return binds
|
||||
|
||||
|
||||
def get_split_image_tag(self, image):
|
||||
# If image contains a host or org name, omit that from our check
|
||||
if '/' in image:
|
||||
registry, resource = image.rsplit('/', 1)
|
||||
else:
|
||||
registry, resource = None, image
|
||||
|
||||
# now we can determine if image has a tag
|
||||
if ':' in resource:
|
||||
resource, tag = resource.split(':', 1)
|
||||
if registry:
|
||||
resource = '/'.join((registry, resource))
|
||||
else:
|
||||
tag = "latest"
|
||||
resource = image
|
||||
|
||||
return resource, tag
|
||||
|
||||
def get_summary_counters_msg(self):
|
||||
msg = ""
|
||||
for k, v in self.counters.iteritems():
|
||||
msg = msg + "%s %d " % (k, v)
|
||||
|
||||
return msg
|
||||
|
||||
def increment_counter(self, name):
|
||||
self.counters[name] = self.counters[name] + 1
|
||||
|
||||
def has_changed(self):
|
||||
for k, v in self.counters.iteritems():
|
||||
if v > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_inspect_containers(self, containers):
|
||||
inspect = []
|
||||
for i in containers:
|
||||
details = self.client.inspect_container(i['Id'])
|
||||
details = _docker_id_quirk(details)
|
||||
inspect.append(details)
|
||||
|
||||
return inspect
|
||||
|
||||
def get_deployed_containers(self):
|
||||
"""determine which images/commands are running already"""
|
||||
image = self.module.params.get('image')
|
||||
command = self.module.params.get('command')
|
||||
if command:
|
||||
command = command.strip()
|
||||
name = self.module.params.get('name')
|
||||
if name and not name.startswith('/'):
|
||||
name = '/' + name
|
||||
deployed = []
|
||||
|
||||
# if we weren't given a tag with the image, we need to only compare on the image name, as that
|
||||
# docker will give us back the full image name including a tag in the container list if one exists.
|
||||
image, tag = self.get_split_image_tag(image)
|
||||
|
||||
for i in self.client.containers(all=True):
|
||||
running_image, running_tag = self.get_split_image_tag(i['Image'])
|
||||
running_command = i['Command'].strip()
|
||||
|
||||
name_matches = False
|
||||
if i["Names"]:
|
||||
name_matches = (name and name in i['Names'])
|
||||
image_matches = (running_image == image)
|
||||
tag_matches = (not tag or running_tag == tag)
|
||||
# if a container has an entrypoint, `command` will actually equal
|
||||
# '{} {}'.format(entrypoint, command)
|
||||
command_matches = (not command or running_command.endswith(command))
|
||||
|
||||
if name_matches or (name is None and image_matches and tag_matches and command_matches):
|
||||
details = self.client.inspect_container(i['Id'])
|
||||
details = _docker_id_quirk(details)
|
||||
deployed.append(details)
|
||||
|
||||
return deployed
|
||||
|
||||
def get_running_containers(self):
|
||||
running = []
|
||||
for i in self.get_deployed_containers():
|
||||
if i['State']['Running'] == True and i['State'].get('Ghost', False) == False:
|
||||
running.append(i)
|
||||
|
||||
return running
|
||||
|
||||
def create_containers(self, count=1):
|
||||
params = {'image': self.module.params.get('image'),
|
||||
'command': self.module.params.get('command'),
|
||||
'ports': self.exposed_ports,
|
||||
'volumes': self.volumes,
|
||||
'mem_limit': _human_to_bytes(self.module.params.get('memory_limit')),
|
||||
'environment': self.env,
|
||||
'hostname': self.module.params.get('hostname'),
|
||||
'detach': self.module.params.get('detach'),
|
||||
'name': self.module.params.get('name'),
|
||||
'stdin_open': self.module.params.get('stdin_open'),
|
||||
'tty': self.module.params.get('tty'),
|
||||
}
|
||||
|
||||
if docker.utils.compare_version('1.10', self.client.version()['ApiVersion']) < 0:
|
||||
params['dns'] = self.module.params.get('dns')
|
||||
params['volumes_from'] = self.module.params.get('volumes_from')
|
||||
|
||||
def do_create(count, params):
|
||||
results = []
|
||||
for _ in range(count):
|
||||
result = self.client.create_container(**params)
|
||||
self.increment_counter('created')
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
try:
|
||||
containers = do_create(count, params)
|
||||
except:
|
||||
resource = self.module.params.get('image')
|
||||
image, tag = self.get_split_image_tag(resource)
|
||||
if self.module.params.get('username'):
|
||||
try:
|
||||
self.client.login(
|
||||
self.module.params.get('username'),
|
||||
password=self.module.params.get('password'),
|
||||
email=self.module.params.get('email'),
|
||||
registry=self.module.params.get('registry')
|
||||
)
|
||||
except:
|
||||
self.module.fail_json(msg="failed to login to the remote registry, check your username/password.")
|
||||
try:
|
||||
self.client.pull(image, tag=tag)
|
||||
except:
|
||||
self.module.fail_json(msg="failed to pull the specified image: %s" % resource)
|
||||
self.increment_counter('pull')
|
||||
containers = do_create(count, params)
|
||||
|
||||
return containers
|
||||
|
||||
def start_containers(self, containers):
|
||||
params = {
|
||||
'lxc_conf': self.lxc_conf,
|
||||
'binds': self.binds,
|
||||
'port_bindings': self.port_bindings,
|
||||
'publish_all_ports': self.module.params.get('publish_all_ports'),
|
||||
'privileged': self.module.params.get('privileged'),
|
||||
'links': self.links,
|
||||
'network_mode': self.module.params.get('net'),
|
||||
}
|
||||
if docker.utils.compare_version('1.10', self.client.version()['ApiVersion']) >= 0 and hasattr(docker, '__version__') and docker.__version__ > '0.3.0':
|
||||
params['dns'] = self.module.params.get('dns')
|
||||
params['volumes_from'] = self.module.params.get('volumes_from')
|
||||
|
||||
for i in containers:
|
||||
self.client.start(i['Id'], **params)
|
||||
self.increment_counter('started')
|
||||
|
||||
def stop_containers(self, containers):
|
||||
for i in containers:
|
||||
self.client.stop(i['Id'])
|
||||
self.increment_counter('stopped')
|
||||
|
||||
return [self.client.wait(i['Id']) for i in containers]
|
||||
|
||||
def remove_containers(self, containers):
|
||||
for i in containers:
|
||||
self.client.remove_container(i['Id'])
|
||||
self.increment_counter('removed')
|
||||
|
||||
def kill_containers(self, containers):
|
||||
for i in containers:
|
||||
self.client.kill(i['Id'])
|
||||
self.increment_counter('killed')
|
||||
|
||||
def restart_containers(self, containers):
|
||||
for i in containers:
|
||||
self.client.restart(i['Id'])
|
||||
self.increment_counter('restarted')
|
||||
|
||||
|
||||
def check_dependencies(module):
|
||||
"""
|
||||
Ensure `docker-py` >= 0.3.0 is installed, and call module.fail_json with a
|
||||
helpful error message if it isn't.
|
||||
"""
|
||||
if not HAS_DOCKER_PY:
|
||||
module.fail_json(msg="`docker-py` doesn't seem to be installed, but is required for the Ansible Docker module.")
|
||||
else:
|
||||
HAS_NEW_ENOUGH_DOCKER_PY = False
|
||||
if hasattr(docker, '__version__'):
|
||||
# a '__version__' attribute was added to the module but not until
|
||||
# after 0.3.0 was added pushed to pip. If it's there, use it.
|
||||
if docker.__version__ >= '0.3.0':
|
||||
HAS_NEW_ENOUGH_DOCKER_PY = True
|
||||
else:
|
||||
# HACK: if '__version__' isn't there, we check for the existence of
|
||||
# `_get_raw_response_socket` in the docker.Client class, which was
|
||||
# added in 0.3.0
|
||||
if hasattr(docker.Client, '_get_raw_response_socket'):
|
||||
HAS_NEW_ENOUGH_DOCKER_PY = True
|
||||
|
||||
if not HAS_NEW_ENOUGH_DOCKER_PY:
|
||||
module.fail_json(msg="The Ansible Docker module requires `docker-py` >= 0.3.0.")
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
count = dict(default=1),
|
||||
image = dict(required=True),
|
||||
command = dict(required=False, default=None),
|
||||
expose = dict(required=False, default=None, type='list'),
|
||||
ports = dict(required=False, default=None, type='list'),
|
||||
publish_all_ports = dict(default=False, type='bool'),
|
||||
volumes = dict(default=None, type='list'),
|
||||
volumes_from = dict(default=None),
|
||||
links = dict(default=None, type='list'),
|
||||
memory_limit = dict(default=0),
|
||||
memory_swap = dict(default=0),
|
||||
docker_url = dict(default='unix://var/run/docker.sock'),
|
||||
docker_api_version = dict(default=docker.client.DEFAULT_DOCKER_API_VERSION),
|
||||
username = dict(default=None),
|
||||
password = dict(),
|
||||
email = dict(),
|
||||
registry = dict(),
|
||||
hostname = dict(default=None),
|
||||
env = dict(type='dict'),
|
||||
dns = dict(),
|
||||
detach = dict(default=True, type='bool'),
|
||||
state = dict(default='running', choices=['absent', 'present', 'running', 'stopped', 'killed', 'restarted']),
|
||||
debug = dict(default=False, type='bool'),
|
||||
privileged = dict(default=False, type='bool'),
|
||||
stdin_open = dict(default=False, type='bool'),
|
||||
tty = dict(default=False, type='bool'),
|
||||
lxc_conf = dict(default=None, type='list'),
|
||||
name = dict(default=None),
|
||||
net = dict(default=None)
|
||||
)
|
||||
)
|
||||
|
||||
check_dependencies(module)
|
||||
|
||||
try:
|
||||
manager = DockerManager(module)
|
||||
state = module.params.get('state')
|
||||
count = int(module.params.get('count'))
|
||||
name = module.params.get('name')
|
||||
image = module.params.get('image')
|
||||
|
||||
if count < 0:
|
||||
module.fail_json(msg="Count must be greater than zero")
|
||||
if count > 1 and name:
|
||||
module.fail_json(msg="Count and name must not be used together")
|
||||
|
||||
running_containers = manager.get_running_containers()
|
||||
running_count = len(running_containers)
|
||||
delta = count - running_count
|
||||
deployed_containers = manager.get_deployed_containers()
|
||||
facts = None
|
||||
failed = False
|
||||
changed = False
|
||||
|
||||
# start/stop containers
|
||||
if state in [ "running", "present" ]:
|
||||
|
||||
# make sure a container with `name` exists, if not create and start it
|
||||
if name:
|
||||
# first determine if a container with this name exists
|
||||
existing_container = None
|
||||
for deployed_container in deployed_containers:
|
||||
if deployed_container.get('Name') == '/%s' % name:
|
||||
existing_container = deployed_container
|
||||
break
|
||||
|
||||
# the named container is running, but with a
|
||||
# different image or tag, so we stop it first
|
||||
if existing_container and existing_container.get('Config', dict()).get('Image') != image:
|
||||
manager.stop_containers([existing_container])
|
||||
manager.remove_containers([existing_container])
|
||||
running_containers = manager.get_running_containers()
|
||||
deployed_containers = manager.get_deployed_containers()
|
||||
existing_container = None
|
||||
|
||||
# if the container isn't running (or if we stopped the
|
||||
# old version above), create and (maybe) start it up now
|
||||
if not existing_container:
|
||||
containers = manager.create_containers(1)
|
||||
if state == "present": # otherwise it get (re)started later anyways..
|
||||
manager.start_containers(containers)
|
||||
running_containers = manager.get_running_containers()
|
||||
deployed_containers = manager.get_deployed_containers()
|
||||
|
||||
if state == "running":
|
||||
# make sure a container with `name` is running
|
||||
if name and "/" + name not in map(lambda x: x.get('Name'), running_containers):
|
||||
manager.start_containers(deployed_containers)
|
||||
|
||||
# start more containers if we don't have enough
|
||||
elif delta > 0:
|
||||
containers = manager.create_containers(delta)
|
||||
manager.start_containers(containers)
|
||||
|
||||
# stop containers if we have too many
|
||||
elif delta < 0:
|
||||
containers_to_stop = running_containers[0:abs(delta)]
|
||||
containers = manager.stop_containers(containers_to_stop)
|
||||
manager.remove_containers(containers_to_stop)
|
||||
|
||||
facts = manager.get_running_containers()
|
||||
else:
|
||||
facts = manager.get_deployed_containers()
|
||||
|
||||
# stop and remove containers
|
||||
elif state == "absent":
|
||||
facts = manager.stop_containers(deployed_containers)
|
||||
manager.remove_containers(deployed_containers)
|
||||
|
||||
# stop containers
|
||||
elif state == "stopped":
|
||||
facts = manager.stop_containers(running_containers)
|
||||
|
||||
# kill containers
|
||||
elif state == "killed":
|
||||
manager.kill_containers(running_containers)
|
||||
|
||||
# restart containers
|
||||
elif state == "restarted":
|
||||
manager.restart_containers(running_containers)
|
||||
facts = manager.get_inspect_containers(running_containers)
|
||||
|
||||
msg = "%s container(s) running image %s with command %s" % \
|
||||
(manager.get_summary_counters_msg(), module.params.get('image'), module.params.get('command'))
|
||||
changed = manager.has_changed()
|
||||
|
||||
module.exit_json(failed=failed, changed=changed, msg=msg, ansible_facts=_ansible_facts(facts))
|
||||
|
||||
except DockerAPIError, e:
|
||||
changed = manager.has_changed()
|
||||
module.exit_json(failed=True, changed=changed, msg="Docker API error: " + e.explanation)
|
||||
|
||||
except RequestException, e:
|
||||
changed = manager.has_changed()
|
||||
module.exit_json(failed=True, changed=changed, msg=repr(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,252 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
|
||||
# (c) 2014, Pavel Antonov <antonov@adwz.ru>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
######################################################################
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: docker_image
|
||||
author: Pavel Antonov
|
||||
version_added: "1.5"
|
||||
short_description: manage docker images
|
||||
description:
|
||||
- Create, check and remove docker images
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Path to directory with Dockerfile
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- Image name to work with
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
tag:
|
||||
description:
|
||||
- Image tag to work with
|
||||
required: false
|
||||
default: "latest"
|
||||
aliases: []
|
||||
nocache:
|
||||
description:
|
||||
- Do not use cache with building
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
docker_url:
|
||||
description:
|
||||
- URL of docker host to issue commands to
|
||||
required: false
|
||||
default: unix://var/run/docker.sock
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Set the state of the image
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent", "build" ]
|
||||
aliases: []
|
||||
timeout:
|
||||
description:
|
||||
- Set image operation timeout
|
||||
required: false
|
||||
default: 600
|
||||
aliases: []
|
||||
requirements: [ "docker-py" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
Build docker image if required. Path should contains Dockerfile to build image:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: check or build image
|
||||
docker_image: path="/path/to/build/dir" name="my/app" state=present
|
||||
|
||||
Build new version of image:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: check or build image
|
||||
docker_image: path="/path/to/build/dir" name="my/app" state=build
|
||||
|
||||
Remove image from local docker storage:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: remove image
|
||||
docker_image: name="my/app" state=absent
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import docker.client
|
||||
from requests.exceptions import *
|
||||
from urlparse import urlparse
|
||||
except ImportError, e:
|
||||
print "failed=True msg='failed to import python module: %s'" % e
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from docker.errors import APIError as DockerAPIError
|
||||
except ImportError:
|
||||
from docker.client import APIError as DockerAPIError
|
||||
|
||||
class DockerImageManager:
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.path = self.module.params.get('path')
|
||||
self.name = self.module.params.get('name')
|
||||
self.tag = self.module.params.get('tag')
|
||||
self.nocache = self.module.params.get('nocache')
|
||||
docker_url = urlparse(module.params.get('docker_url'))
|
||||
self.client = docker.Client(base_url=docker_url.geturl(), timeout=module.params.get('timeout'))
|
||||
self.changed = False
|
||||
self.log = []
|
||||
self.error_msg = None
|
||||
|
||||
def get_log(self, as_string=True):
|
||||
return "".join(self.log) if as_string else self.log
|
||||
|
||||
def build(self):
|
||||
stream = self.client.build(self.path, tag=':'.join([self.name, self.tag]), nocache=self.nocache, rm=True, stream=True)
|
||||
success_search = r'Successfully built ([0-9a-f]+)'
|
||||
image_id = None
|
||||
self.changed = True
|
||||
|
||||
for chunk in stream:
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
try:
|
||||
chunk_json = json.loads(chunk)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if 'error' in chunk_json:
|
||||
self.error_msg = chunk_json['error']
|
||||
return None
|
||||
|
||||
if 'stream' in chunk_json:
|
||||
output = chunk_json['stream']
|
||||
self.log.append(output)
|
||||
match = re.search(success_search, output)
|
||||
if match:
|
||||
image_id = match.group(1)
|
||||
|
||||
# Just in case we skipped evaluating the JSON returned from build
|
||||
# during every iteration, add an error if the image_id was never
|
||||
# populated
|
||||
if not image_id:
|
||||
self.error_msg = 'Unknown error encountered'
|
||||
|
||||
return image_id
|
||||
|
||||
def has_changed(self):
|
||||
return self.changed
|
||||
|
||||
def get_images(self):
|
||||
filtered_images = []
|
||||
images = self.client.images()
|
||||
for i in images:
|
||||
# Docker-py version >= 0.3 (Docker API >= 1.8)
|
||||
if 'RepoTags' in i:
|
||||
repotag = ':'.join([self.name, self.tag])
|
||||
if not self.name or repotag in i['RepoTags']:
|
||||
filtered_images.append(i)
|
||||
# Docker-py version < 0.3 (Docker API < 1.8)
|
||||
elif (not self.name or self.name == i['Repository']) and (not self.tag or self.tag == i['Tag']):
|
||||
filtered_images.append(i)
|
||||
return filtered_images
|
||||
|
||||
def remove_images(self):
|
||||
images = self.get_images()
|
||||
for i in images:
|
||||
try:
|
||||
self.client.remove_image(i['Id'])
|
||||
self.changed = True
|
||||
except DockerAPIError as e:
|
||||
# image can be removed by docker if not used
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
path = dict(required=False, default=None),
|
||||
name = dict(required=True),
|
||||
tag = dict(required=False, default="latest"),
|
||||
nocache = dict(default=False, type='bool'),
|
||||
state = dict(default='present', choices=['absent', 'present', 'build']),
|
||||
docker_url = dict(default='unix://var/run/docker.sock'),
|
||||
timeout = dict(default=600, type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
manager = DockerImageManager(module)
|
||||
state = module.params.get('state')
|
||||
failed = False
|
||||
image_id = None
|
||||
msg = ''
|
||||
do_build = False
|
||||
|
||||
# build image if not exists
|
||||
if state == "present":
|
||||
images = manager.get_images()
|
||||
if len(images) == 0:
|
||||
do_build = True
|
||||
# build image
|
||||
elif state == "build":
|
||||
do_build = True
|
||||
# remove image or images
|
||||
elif state == "absent":
|
||||
manager.remove_images()
|
||||
|
||||
if do_build:
|
||||
image_id = manager.build()
|
||||
if image_id:
|
||||
msg = "Image built: %s" % image_id
|
||||
else:
|
||||
failed = True
|
||||
msg = "Error: %s\nLog:%s" % (manager.error_msg, manager.get_log())
|
||||
|
||||
module.exit_json(failed=failed, changed=manager.has_changed(), msg=msg, image_id=image_id)
|
||||
|
||||
except DockerAPIError as e:
|
||||
module.exit_json(failed=True, changed=manager.has_changed(), msg="Docker API error: " + e.explanation)
|
||||
|
||||
except RequestException as e:
|
||||
module.exit_json(failed=True, changed=manager.has_changed(), msg=repr(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
File diff suppressed because it is too large
Load Diff
@ -1,273 +0,0 @@
|
||||
#!/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
|
||||
version_added: "1.3"
|
||||
short_description: create or destroy an image in ec2, return imageid
|
||||
description:
|
||||
- Creates or deletes ec2 images. This module has a dependency on python-boto >= 2.5
|
||||
options:
|
||||
instance_id:
|
||||
description:
|
||||
- instance id of the image to create
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- The name of the new image to create
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
wait:
|
||||
description:
|
||||
- wait for the AMI to be in state 'available' before returning.
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- create or deregister/delete image
|
||||
required: false
|
||||
default: 'present'
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ 'aws_region', 'ec2_region' ]
|
||||
description:
|
||||
description:
|
||||
- An optional human-readable string describing the contents and purpose of the AMI.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
no_reboot:
|
||||
description:
|
||||
- An optional flag indicating that the bundling process should not attempt to shutdown the instance before bundling. If this flag is True, the responsibility of maintaining file system integrity is left to the owner of the instance. The default choice is "no".
|
||||
required: false
|
||||
default: no
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
image_id:
|
||||
description:
|
||||
- Image ID to be deregistered.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
delete_snapshot:
|
||||
description:
|
||||
- Whether or not to deleted an AMI while deregistering it.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
|
||||
author: Evan Duffield <eduffield@iacquire.com>
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
# Thank you to iAcquire for sponsoring development of this module.
|
||||
#
|
||||
# See http://alestic.com/2011/06/ec2-ami-security for more information about ensuring the security of your AMI.
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic AMI Creation
|
||||
- local_action:
|
||||
module: ec2_ami
|
||||
aws_access_key: xxxxxxxxxxxxxxxxxxxxxxx
|
||||
aws_secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
instance_id: i-xxxxxx
|
||||
wait: yes
|
||||
name: newtest
|
||||
register: instance
|
||||
|
||||
# Basic AMI Creation, without waiting
|
||||
- local_action:
|
||||
module: ec2_ami
|
||||
aws_access_key: xxxxxxxxxxxxxxxxxxxxxxx
|
||||
aws_secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
region: xxxxxx
|
||||
instance_id: i-xxxxxx
|
||||
wait: no
|
||||
name: newtest
|
||||
register: instance
|
||||
|
||||
# Deregister/Delete AMI
|
||||
- local_action:
|
||||
module: ec2_ami
|
||||
aws_access_key: xxxxxxxxxxxxxxxxxxxxxxx
|
||||
aws_secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
region: xxxxxx
|
||||
image_id: ${instance.image_id}
|
||||
delete_snapshot: True
|
||||
state: absent
|
||||
|
||||
# Deregister AMI
|
||||
- local_action:
|
||||
module: ec2_ami
|
||||
aws_access_key: xxxxxxxxxxxxxxxxxxxxxxx
|
||||
aws_secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
region: xxxxxx
|
||||
image_id: ${instance.image_id}
|
||||
delete_snapshot: False
|
||||
state: absent
|
||||
|
||||
'''
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def create_image(module, ec2):
|
||||
"""
|
||||
Creates new AMI
|
||||
|
||||
module : AnsibleModule object
|
||||
ec2: authenticated ec2 connection object
|
||||
"""
|
||||
|
||||
instance_id = module.params.get('instance_id')
|
||||
name = module.params.get('name')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
description = module.params.get('description')
|
||||
no_reboot = module.params.get('no_reboot')
|
||||
|
||||
try:
|
||||
params = {'instance_id': instance_id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'no_reboot': no_reboot}
|
||||
|
||||
image_id = ec2.create_image(**params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
# Wait until the image is recognized. EC2 API has eventual consistency,
|
||||
# such that a successful CreateImage API call doesn't guarantee the success
|
||||
# of subsequent DescribeImages API call using the new image id returned.
|
||||
for i in range(wait_timeout):
|
||||
try:
|
||||
img = ec2.get_image(image_id)
|
||||
break
|
||||
except boto.exception.EC2ResponseError, e:
|
||||
if 'InvalidAMIID.NotFound' in e.error_code and wait:
|
||||
time.sleep(1)
|
||||
else:
|
||||
module.fail_json(msg="Error while trying to find the new image. Using wait=yes and/or a longer wait_timeout may help.")
|
||||
else:
|
||||
module.fail_json(msg="timed out waiting for image to be recognized")
|
||||
|
||||
# wait here until the image is created
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait and wait_timeout > time.time() and (img is None or img.state != 'available'):
|
||||
img = ec2.get_image(image_id)
|
||||
time.sleep(3)
|
||||
if wait and wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "timed out waiting for image to be created")
|
||||
|
||||
module.exit_json(msg="AMI creation operation complete", image_id=image_id, state=img.state, changed=True)
|
||||
|
||||
|
||||
def deregister_image(module, ec2):
|
||||
"""
|
||||
Deregisters AMI
|
||||
"""
|
||||
|
||||
image_id = module.params.get('image_id')
|
||||
delete_snapshot = module.params.get('delete_snapshot')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
img = ec2.get_image(image_id)
|
||||
if img == None:
|
||||
module.fail_json(msg = "Image %s does not exist" % image_id, changed=False)
|
||||
|
||||
try:
|
||||
params = {'image_id': image_id,
|
||||
'delete_snapshot': delete_snapshot}
|
||||
|
||||
res = ec2.deregister_image(**params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
# wait here until the image is gone
|
||||
img = ec2.get_image(image_id)
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait and wait_timeout > time.time() and img is not None:
|
||||
img = ec2.get_image(image_id)
|
||||
time.sleep(3)
|
||||
if wait and wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "timed out waiting for image to be reregistered/deleted")
|
||||
|
||||
module.exit_json(msg="AMI deregister/delete operation complete", changed=True)
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
instance_id = dict(),
|
||||
image_id = dict(),
|
||||
delete_snapshot = dict(),
|
||||
name = dict(),
|
||||
wait = dict(type="bool", default=False),
|
||||
wait_timeout = dict(default=900),
|
||||
description = dict(default=""),
|
||||
no_reboot = dict(default=False, type="bool"),
|
||||
state = dict(default='present'),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
if module.params.get('state') == 'absent':
|
||||
if not module.params.get('image_id'):
|
||||
module.fail_json(msg='image_id needs to be an ami image to registered/delete')
|
||||
|
||||
deregister_image(module, ec2)
|
||||
|
||||
elif module.params.get('state') == 'present':
|
||||
# Changed is always set to true when provisioning new AMI
|
||||
if not module.params.get('instance_id'):
|
||||
module.fail_json(msg='instance_id parameter is required for new image')
|
||||
if not module.params.get('name'):
|
||||
module.fail_json(msg='name parameter is required for new image')
|
||||
create_image(module, ec2)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
||||
|
@ -1,196 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2013, Nimbis Services
|
||||
#
|
||||
# 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_search
|
||||
short_description: Retrieve AWS AMI for a given operating system.
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- Look up the most recent AMI on AWS for a given operating system.
|
||||
- Returns C(ami), C(aki), C(ari), C(serial), C(tag)
|
||||
- If there is no AKI or ARI associated with an image, these will be C(null).
|
||||
- Only supports images from cloud-images.ubuntu.com
|
||||
- 'Example output: C({"ami": "ami-69f5a900", "changed": false, "aki": "aki-88aa75e1", "tag": "release", "ari": null, "serial": "20131024"})'
|
||||
version_added: "1.6"
|
||||
options:
|
||||
distro:
|
||||
description: Linux distribution (e.g., C(ubuntu))
|
||||
required: true
|
||||
choices: ["ubuntu"]
|
||||
release:
|
||||
description: short name of the release (e.g., C(precise))
|
||||
required: true
|
||||
stream:
|
||||
description: Type of release.
|
||||
required: false
|
||||
default: "server"
|
||||
choices: ["server", "desktop"]
|
||||
store:
|
||||
description: Back-end store for instance
|
||||
required: false
|
||||
default: "ebs"
|
||||
choices: ["ebs", "ebs-io1", "ebs-ssd", "instance-store"]
|
||||
arch:
|
||||
description: CPU architecture
|
||||
required: false
|
||||
default: "amd64"
|
||||
choices: ["i386", "amd64"]
|
||||
region:
|
||||
description: EC2 region
|
||||
required: false
|
||||
default: us-east-1
|
||||
choices: ["ap-northeast-1", "ap-southeast-1", "ap-southeast-2",
|
||||
"eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1"]
|
||||
virt:
|
||||
description: virutalization type
|
||||
required: false
|
||||
default: paravirtual
|
||||
choices: ["paravirtual", "hvm"]
|
||||
|
||||
author: Lorin Hochstein
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Launch an Ubuntu 12.04 (Precise Pangolin) EC2 instance
|
||||
hosts: 127.0.0.1
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Get the Ubuntu precise AMI
|
||||
ec2_ami_search: distro=ubuntu release=precise region=us-west-1 store=instance-store
|
||||
register: ubuntu_image
|
||||
- name: Start the EC2 instance
|
||||
ec2: image={{ ubuntu_image.ami }} instance_type=m1.small key_name=mykey
|
||||
'''
|
||||
|
||||
import csv
|
||||
import json
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
SUPPORTED_DISTROS = ['ubuntu']
|
||||
|
||||
AWS_REGIONS = ['ap-northeast-1',
|
||||
'ap-southeast-1',
|
||||
'ap-southeast-2',
|
||||
'eu-west-1',
|
||||
'sa-east-1',
|
||||
'us-east-1',
|
||||
'us-west-1',
|
||||
'us-west-2']
|
||||
|
||||
|
||||
def get_url(module, url):
|
||||
""" Get url and return response """
|
||||
try:
|
||||
r = urllib2.urlopen(url)
|
||||
except (urllib2.HTTPError, urllib2.URLError), e:
|
||||
code = getattr(e, 'code', -1)
|
||||
module.fail_json(msg="Request failed: %s" % str(e), status_code=code)
|
||||
return r
|
||||
|
||||
|
||||
def ubuntu(module):
|
||||
""" Get the ami for ubuntu """
|
||||
|
||||
release = module.params['release']
|
||||
stream = module.params['stream']
|
||||
store = module.params['store']
|
||||
arch = module.params['arch']
|
||||
region = module.params['region']
|
||||
virt = module.params['virt']
|
||||
|
||||
url = get_ubuntu_url(release, stream)
|
||||
|
||||
req = get_url(module, url)
|
||||
reader = csv.reader(req, delimiter='\t')
|
||||
try:
|
||||
ami, aki, ari, tag, serial = lookup_ubuntu_ami(reader, release, stream,
|
||||
store, arch, region, virt)
|
||||
module.exit_json(changed=False, ami=ami, aki=aki, ari=ari, tag=tag,
|
||||
serial=serial)
|
||||
except KeyError:
|
||||
module.fail_json(msg="No matching AMI found")
|
||||
|
||||
|
||||
def lookup_ubuntu_ami(table, release, stream, store, arch, region, virt):
|
||||
""" Look up the Ubuntu AMI that matches query given a table of AMIs
|
||||
|
||||
table: an iterable that returns a row of
|
||||
(release, stream, tag, serial, region, ami, aki, ari, virt)
|
||||
release: ubuntu release name
|
||||
stream: 'server' or 'desktop'
|
||||
store: 'ebs', 'ebs-io1', 'ebs-ssd' or 'instance-store'
|
||||
arch: 'i386' or 'amd64'
|
||||
region: EC2 region
|
||||
virt: 'paravirtual' or 'hvm'
|
||||
|
||||
Returns (ami, aki, ari, tag, serial)"""
|
||||
expected = (release, stream, store, arch, region, virt)
|
||||
|
||||
for row in table:
|
||||
(actual_release, actual_stream, tag, serial,
|
||||
actual_store, actual_arch, actual_region, ami, aki, ari,
|
||||
actual_virt) = row
|
||||
actual = (actual_release, actual_stream, actual_store, actual_arch,
|
||||
actual_region, actual_virt)
|
||||
if actual == expected:
|
||||
# aki and ari are sometimes blank
|
||||
if aki == '':
|
||||
aki = None
|
||||
if ari == '':
|
||||
ari = None
|
||||
return (ami, aki, ari, tag, serial)
|
||||
|
||||
raise KeyError()
|
||||
|
||||
|
||||
def get_ubuntu_url(release, stream):
|
||||
url = "https://cloud-images.ubuntu.com/query/%s/%s/released.current.txt"
|
||||
return url % (release, stream)
|
||||
|
||||
|
||||
def main():
|
||||
arg_spec = dict(
|
||||
distro=dict(required=True, choices=SUPPORTED_DISTROS),
|
||||
release=dict(required=True),
|
||||
stream=dict(required=False, default='server',
|
||||
choices=['desktop', 'server']),
|
||||
store=dict(required=False, default='ebs',
|
||||
choices=['ebs', 'ebs-io1', 'ebs-ssd', 'instance-store']),
|
||||
arch=dict(required=False, default='amd64',
|
||||
choices=['i386', 'amd64']),
|
||||
region=dict(required=False, default='us-east-1', choices=AWS_REGIONS),
|
||||
virt=dict(required=False, default='paravirtual',
|
||||
choices=['paravirtual', 'hvm'])
|
||||
)
|
||||
module = AnsibleModule(argument_spec=arg_spec)
|
||||
distro = module.params['distro']
|
||||
|
||||
if distro == 'ubuntu':
|
||||
ubuntu(module)
|
||||
else:
|
||||
module.fail_json(msg="Unsupported distro: %s" % distro)
|
||||
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,608 +0,0 @@
|
||||
#!/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_asg
|
||||
short_description: Create or delete AWS Autoscaling Groups
|
||||
description:
|
||||
- Can create or delete AWS Autoscaling Groups
|
||||
- Works with the ec2_lc module to manage Launch Configurations
|
||||
version_added: "1.6"
|
||||
author: Gareth Rushgrove
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the instance
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
name:
|
||||
description:
|
||||
- Unique name for group to be created or deleted
|
||||
required: true
|
||||
load_balancers:
|
||||
description:
|
||||
- List of ELB names to use for the group
|
||||
required: false
|
||||
availability_zones:
|
||||
description:
|
||||
- List of availability zone names in which to create the group. Defaults to all the availability zones in the region if vpc_zone_identifier is not set.
|
||||
required: false
|
||||
launch_config_name:
|
||||
description:
|
||||
- Name of the Launch configuration to use for the group. See the ec2_lc module for managing these.
|
||||
required: false
|
||||
min_size:
|
||||
description:
|
||||
- Minimum number of instances in group
|
||||
required: false
|
||||
max_size:
|
||||
description:
|
||||
- Maximum number of instances in group
|
||||
required: false
|
||||
desired_capacity:
|
||||
description:
|
||||
- Desired number of instances in group
|
||||
required: false
|
||||
replace_all_instances:
|
||||
description:
|
||||
- In a rolling fashion, replace all instances with an old launch configuration with one from the current launch configuraiton.
|
||||
required: false
|
||||
version_added: "1.8"
|
||||
default: False
|
||||
replace_batch_size:
|
||||
description:
|
||||
- Number of instances you'd like to replace at a time. Used with replace_all_instances.
|
||||
required: false
|
||||
version_added: "1.8"
|
||||
default: 1
|
||||
replace_instances:
|
||||
description:
|
||||
- List of instance_ids belonging to the named ASG that you would like to terminate and be replaced with instances matching the current launch configuration.
|
||||
required: false
|
||||
version_added: "1.8"
|
||||
default: None
|
||||
lc_check:
|
||||
description:
|
||||
- Check to make sure instances that are being replaced with replace_instances do not aready have the current launch_config.
|
||||
required: false
|
||||
version_added: "1.8"
|
||||
default: True
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
vpc_zone_identifier:
|
||||
description:
|
||||
- List of VPC subnets to use
|
||||
required: false
|
||||
default: None
|
||||
tags:
|
||||
description:
|
||||
- A list of tags to add to the Auto Scale Group. Optional key is 'propagate_at_launch', which defaults to true.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.7"
|
||||
health_check_period:
|
||||
description:
|
||||
- Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health.
|
||||
required: false
|
||||
default: 500 seconds
|
||||
version_added: "1.7"
|
||||
health_check_type:
|
||||
description:
|
||||
- The service you want the health status from, Amazon EC2 or Elastic Load Balancer.
|
||||
required: false
|
||||
default: EC2
|
||||
version_added: "1.7"
|
||||
choices: ['EC2', 'ELB']
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait instances to become viable when replaced. Used in concjunction with instance_ids option.
|
||||
default: 300
|
||||
version_added: "1.8"
|
||||
extends_documentation_fragment: aws
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
A basic example of configuration:
|
||||
|
||||
- ec2_asg:
|
||||
name: special
|
||||
load_balancers: 'lb1,lb2'
|
||||
availability_zones: 'eu-west-1a,eu-west-1b'
|
||||
launch_config_name: 'lc-1'
|
||||
min_size: 1
|
||||
max_size: 10
|
||||
desired_capacity: 5
|
||||
vpc_zone_identifier: 'subnet-abcd1234,subnet-1a2b3c4d'
|
||||
tags:
|
||||
- environment: production
|
||||
propagate_at_launch: no
|
||||
|
||||
Below is an example of how to assign a new launch config to an ASG and terminate old instances.
|
||||
|
||||
All instances in "myasg" that do not have the launch configuration named "my_new_lc" will be terminated in
|
||||
a rolling fashion with instances using the current launch configuration, "my_new_lc".
|
||||
|
||||
This could also be considered a rolling deploy of a pre-baked AMI.
|
||||
|
||||
If this is a newly created group, the instances will not be replaced since all instances
|
||||
will have the current launch configuration.
|
||||
|
||||
- name: create launch config
|
||||
ec2_lc:
|
||||
name: my_new_lc
|
||||
image_id: ami-lkajsf
|
||||
key_name: mykey
|
||||
region: us-east-1
|
||||
security_groups: sg-23423
|
||||
instance_type: m1.small
|
||||
assign_public_ip: yes
|
||||
|
||||
- ec2_asg:
|
||||
name: myasg
|
||||
launch_config_name: my_new_lc
|
||||
health_check_period: 60
|
||||
health_check_type: ELB
|
||||
replace_all_instances: yes
|
||||
min_size: 5
|
||||
max_size: 5
|
||||
desired_capacity: 5
|
||||
region: us-east-1
|
||||
|
||||
To only replace a couple of instances instead of all of them, supply a list
|
||||
to "replace_instances":
|
||||
|
||||
- ec2_asg:
|
||||
name: myasg
|
||||
launch_config_name: my_new_lc
|
||||
health_check_period: 60
|
||||
health_check_type: ELB
|
||||
replace_instances:
|
||||
- i-b345231
|
||||
- i-24c2931
|
||||
min_size: 5
|
||||
max_size: 5
|
||||
desired_capacity: 5
|
||||
region: us-east-1
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
try:
|
||||
import boto.ec2.autoscale
|
||||
from boto.ec2.autoscale import AutoScaleConnection, AutoScalingGroup, Tag
|
||||
from boto.exception import BotoServerError
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
ASG_ATTRIBUTES = ('availability_zones', 'default_cooldown', 'desired_capacity',
|
||||
'health_check_period', 'health_check_type', 'launch_config_name',
|
||||
'load_balancers', 'max_size', 'min_size', 'name', 'placement_group',
|
||||
'tags', 'termination_policies', 'vpc_zone_identifier')
|
||||
|
||||
INSTANCE_ATTRIBUTES = ('instance_id', 'health_status', 'lifecycle_state', 'launch_config_name')
|
||||
|
||||
def enforce_required_arguments(module):
|
||||
''' As many arguments are not required for autoscale group deletion
|
||||
they cannot be mandatory arguments for the module, so we enforce
|
||||
them here '''
|
||||
missing_args = []
|
||||
for arg in ('min_size', 'max_size', 'launch_config_name'):
|
||||
if module.params[arg] is None:
|
||||
missing_args.append(arg)
|
||||
if missing_args:
|
||||
module.fail_json(msg="Missing required arguments for autoscaling group create/update: %s" % ",".join(missing_args))
|
||||
|
||||
|
||||
def get_properties(autoscaling_group):
|
||||
properties = dict((attr, getattr(autoscaling_group, attr)) for attr in ASG_ATTRIBUTES)
|
||||
properties['healthy_instances'] = 0
|
||||
properties['in_service_instances'] = 0
|
||||
properties['unhealthy_instances'] = 0
|
||||
properties['pending_instances'] = 0
|
||||
properties['viable_instances'] = 0
|
||||
properties['terminating_instances'] = 0
|
||||
|
||||
if autoscaling_group.instances:
|
||||
properties['instances'] = [i.instance_id for i in autoscaling_group.instances]
|
||||
instance_facts = {}
|
||||
for i in autoscaling_group.instances:
|
||||
instance_facts[i.instance_id] = {'health_status': i.health_status,
|
||||
'lifecycle_state': i.lifecycle_state,
|
||||
'launch_config_name': i.launch_config_name }
|
||||
if i.health_status == 'Healthy' and i.lifecycle_state == 'InService':
|
||||
properties['viable_instances'] += 1
|
||||
if i.health_status == 'Healthy':
|
||||
properties['healthy_instances'] += 1
|
||||
else:
|
||||
properties['unhealthy_instances'] += 1
|
||||
if i.lifecycle_state == 'InService':
|
||||
properties['in_service_instances'] += 1
|
||||
if i.lifecycle_state == 'Terminating':
|
||||
properties['terminating_instances'] += 1
|
||||
if i.lifecycle_state == 'Pending':
|
||||
properties['pending_instances'] += 1
|
||||
properties['instance_facts'] = instance_facts
|
||||
properties['load_balancers'] = autoscaling_group.load_balancers
|
||||
return properties
|
||||
|
||||
|
||||
def create_autoscaling_group(connection, module):
|
||||
|
||||
group_name = module.params.get('name')
|
||||
load_balancers = module.params['load_balancers']
|
||||
availability_zones = module.params['availability_zones']
|
||||
launch_config_name = module.params.get('launch_config_name')
|
||||
min_size = module.params['min_size']
|
||||
max_size = module.params['max_size']
|
||||
desired_capacity = module.params.get('desired_capacity')
|
||||
vpc_zone_identifier = module.params.get('vpc_zone_identifier')
|
||||
set_tags = module.params.get('tags')
|
||||
health_check_period = module.params.get('health_check_period')
|
||||
health_check_type = module.params.get('health_check_type')
|
||||
|
||||
as_groups = connection.get_all_groups(names=[group_name])
|
||||
|
||||
if not vpc_zone_identifier and not availability_zones:
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
try:
|
||||
ec2_connection = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
asg_tags = []
|
||||
for tag in set_tags:
|
||||
if tag.has_key('key') and tag.has_key('value'): # this block is to support depricated form
|
||||
asg_tags.append(Tag(key=tag.get('key'),
|
||||
value=tag.get('value'),
|
||||
propagate_at_launch=bool(tag.get('propagate_at_launch', True)),
|
||||
resource_id=group_name))
|
||||
else:
|
||||
for k,v in tag.iteritems():
|
||||
if k !='propagate_at_launch':
|
||||
asg_tags.append(Tag(key=k,
|
||||
value=v,
|
||||
propagate_at_launch=bool(tag.get('propagate_at_launch', True)),
|
||||
resource_id=group_name))
|
||||
|
||||
if not as_groups:
|
||||
if not vpc_zone_identifier and not availability_zones:
|
||||
availability_zones = module.params['availability_zones'] = [zone.name for zone in ec2_connection.get_all_zones()]
|
||||
enforce_required_arguments(module)
|
||||
launch_configs = connection.get_all_launch_configurations(names=[launch_config_name])
|
||||
ag = AutoScalingGroup(
|
||||
group_name=group_name,
|
||||
load_balancers=load_balancers,
|
||||
availability_zones=availability_zones,
|
||||
launch_config=launch_configs[0],
|
||||
min_size=min_size,
|
||||
max_size=max_size,
|
||||
desired_capacity=desired_capacity,
|
||||
vpc_zone_identifier=vpc_zone_identifier,
|
||||
connection=connection,
|
||||
tags=asg_tags,
|
||||
health_check_period=health_check_period,
|
||||
health_check_type=health_check_type)
|
||||
|
||||
try:
|
||||
connection.create_auto_scaling_group(ag)
|
||||
asg_properties = get_properties(ag)
|
||||
changed = True
|
||||
return(changed, asg_properties)
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
else:
|
||||
as_group = as_groups[0]
|
||||
changed = False
|
||||
for attr in ASG_ATTRIBUTES:
|
||||
if module.params.get(attr):
|
||||
module_attr = module.params.get(attr)
|
||||
group_attr = getattr(as_group, attr)
|
||||
# we do this because AWS and the module may return the same list
|
||||
# sorted differently
|
||||
try:
|
||||
module_attr.sort()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
group_attr.sort()
|
||||
except:
|
||||
pass
|
||||
if group_attr != module_attr:
|
||||
changed = True
|
||||
setattr(as_group, attr, module_attr)
|
||||
|
||||
if len(set_tags) > 0:
|
||||
existing_tags = as_group.tags
|
||||
existing_tag_map = dict((tag.key, tag) for tag in existing_tags)
|
||||
for tag in set_tags:
|
||||
if tag.has_key('key') and tag.has_key('value'): # this is to support deprecated method
|
||||
if 'key' not in tag:
|
||||
continue
|
||||
if ( not tag['key'] in existing_tag_map or
|
||||
existing_tag_map[tag['key']].value != tag['value'] or
|
||||
('propagate_at_launch' in tag and
|
||||
existing_tag_map[tag['key']].propagate_at_launch != tag['propagate_at_launch']) ):
|
||||
changed = True
|
||||
continue
|
||||
else:
|
||||
for k,v in tag.iteritems():
|
||||
if k !='propagate_at_launch':
|
||||
if ( not k in existing_tag_map or
|
||||
existing_tag_map[k].value != v or
|
||||
('propagate_at_launch' in tag and
|
||||
existing_tag_map[k].propagate_at_launch != tag['propagate_at_launch']) ):
|
||||
changed = True
|
||||
continue
|
||||
if changed:
|
||||
connection.create_or_update_tags(asg_tags)
|
||||
|
||||
# handle loadbalancers separately because None != []
|
||||
load_balancers = module.params.get('load_balancers') or []
|
||||
if load_balancers and as_group.load_balancers != load_balancers:
|
||||
changed = True
|
||||
as_group.load_balancers = module.params.get('load_balancers')
|
||||
|
||||
try:
|
||||
if changed:
|
||||
as_group.update()
|
||||
asg_properties = get_properties(as_group)
|
||||
return(changed, asg_properties)
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
result = as_groups[0]
|
||||
module.exit_json(changed=changed, name=result.name,
|
||||
autoscaling_group_arn=result.autoscaling_group_arn,
|
||||
availability_zones=result.availability_zones,
|
||||
created_time=str(result.created_time),
|
||||
default_cooldown=result.default_cooldown,
|
||||
health_check_period=result.health_check_period,
|
||||
health_check_type=result.health_check_type,
|
||||
instance_id=result.instance_id,
|
||||
instances=[instance.instance_id for instance in result.instances],
|
||||
launch_config_name=result.launch_config_name,
|
||||
load_balancers=result.load_balancers,
|
||||
min_size=result.min_size, max_size=result.max_size,
|
||||
placement_group=result.placement_group,
|
||||
wait_timeout = dict(default=300),
|
||||
tags=result.tags,
|
||||
termination_policies=result.termination_policies,
|
||||
vpc_zone_identifier=result.vpc_zone_identifier)
|
||||
|
||||
|
||||
def delete_autoscaling_group(connection, module):
|
||||
group_name = module.params.get('name')
|
||||
groups = connection.get_all_groups(names=[group_name])
|
||||
if groups:
|
||||
group = groups[0]
|
||||
group.max_size = 0
|
||||
group.min_size = 0
|
||||
group.desired_capacity = 0
|
||||
group.update()
|
||||
instances = True
|
||||
while instances:
|
||||
tmp_groups = connection.get_all_groups(names=[group_name])
|
||||
if tmp_groups:
|
||||
tmp_group = tmp_groups[0]
|
||||
if not tmp_group.instances:
|
||||
instances = False
|
||||
time.sleep(10)
|
||||
|
||||
group.delete()
|
||||
changed=True
|
||||
return changed
|
||||
else:
|
||||
changed=False
|
||||
return changed
|
||||
|
||||
def get_chunks(l, n):
|
||||
for i in xrange(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
|
||||
def replace(connection, module):
|
||||
|
||||
batch_size = module.params.get('replace_batch_size')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
group_name = module.params.get('group_name')
|
||||
max_size = module.params.get('max_size')
|
||||
min_size = module.params.get('min_size')
|
||||
desired_capacity = module.params.get('desired_capacity')
|
||||
replace_instances = module.params.get('replace_instances')
|
||||
|
||||
|
||||
# wait for instance list to be populated on a newly provisioned ASG
|
||||
instance_wait = time.time() + 30
|
||||
while instance_wait > time.time():
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
props = get_properties(as_group)
|
||||
if props.has_key('instances'):
|
||||
instances = props['instances']
|
||||
break
|
||||
time.sleep(10)
|
||||
if instance_wait <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "Waited too for instances to appear. %s" % time.asctime())
|
||||
# determine if we need to continue
|
||||
replaceable = 0
|
||||
if replace_instances:
|
||||
instances = replace_instances
|
||||
for k in props['instance_facts'].keys():
|
||||
if k in instances:
|
||||
if props['instance_facts'][k]['launch_config_name'] != props['launch_config_name']:
|
||||
replaceable += 1
|
||||
if replaceable == 0:
|
||||
changed = False
|
||||
return(changed, props)
|
||||
|
||||
# set temporary settings and wait for them to be reached
|
||||
as_group.max_size = max_size + batch_size
|
||||
as_group.min_size = min_size + batch_size
|
||||
as_group.desired_capacity = desired_capacity + batch_size
|
||||
as_group.update()
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait_timeout > time.time() and min_size + batch_size > props['viable_instances']:
|
||||
time.sleep(10)
|
||||
as_groups = connection.get_all_groups(names=[group_name])
|
||||
as_group = as_groups[0]
|
||||
props = get_properties(as_group)
|
||||
if wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "Waited too for instances to appear. %s" % time.asctime())
|
||||
instances = props['instances']
|
||||
if replace_instances:
|
||||
instances = replace_instances
|
||||
for i in get_chunks(instances, batch_size):
|
||||
replace_batch(connection, module, i)
|
||||
# return settings to normal
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
as_group.max_size = max_size
|
||||
as_group.min_size = min_size
|
||||
as_group.desired_capacity = desired_capacity
|
||||
as_group.update()
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
asg_properties = get_properties(as_group)
|
||||
changed=True
|
||||
return(changed, asg_properties)
|
||||
|
||||
def replace_batch(connection, module, replace_instances):
|
||||
|
||||
|
||||
group_name = module.params.get('group_name')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
lc_check = module.params.get('lc_check')
|
||||
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
props = get_properties(as_group)
|
||||
|
||||
# check to make sure instances given are actually in the given ASG
|
||||
# and they have a non-current launch config
|
||||
old_instances = []
|
||||
instances = ( inst_id for inst_id in replace_instances if inst_id in props['instances'])
|
||||
|
||||
if lc_check:
|
||||
for i in instances:
|
||||
if props['instance_facts'][i]['launch_config_name'] != props['launch_config_name']:
|
||||
old_instances.append(i)
|
||||
else:
|
||||
old_instances = instances
|
||||
|
||||
# set all instances given to unhealthy
|
||||
for instance_id in old_instances:
|
||||
connection.set_instance_health(instance_id,'Unhealthy')
|
||||
|
||||
# we wait to make sure the machines we marked as Unhealthy are
|
||||
# no longer in the list
|
||||
|
||||
count = 1
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait_timeout > time.time() and count > 0:
|
||||
count = 0
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
props = get_properties(as_group)
|
||||
instance_facts = props['instance_facts']
|
||||
instances = ( i for i in instance_facts if i in old_instances)
|
||||
for i in instances:
|
||||
if ( instance_facts[i]['lifecycle_state'] == 'Terminating'
|
||||
or instance_facts[i]['health_status'] == 'Unhealthy' ):
|
||||
count += 1
|
||||
time.sleep(10)
|
||||
|
||||
if wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "Waited too long for old instances to terminate. %s" % time.asctime())
|
||||
|
||||
# make sure we have the latest stats after that last loop.
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
props = get_properties(as_group)
|
||||
|
||||
# now we make sure that we have enough instances in a viable state
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait_timeout > time.time() and props['min_size'] > props['viable_instances']:
|
||||
time.sleep(10)
|
||||
as_groups = connection.get_all_groups(names=[group_name])
|
||||
as_group = as_groups[0]
|
||||
props = get_properties(as_group)
|
||||
|
||||
if wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "Waited too long for new instances to become viable. %s" % time.asctime())
|
||||
|
||||
# collect final stats info
|
||||
as_group = connection.get_all_groups(names=[group_name])[0]
|
||||
asg_properties = get_properties(as_group)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(required=True, type='str'),
|
||||
load_balancers=dict(type='list'),
|
||||
availability_zones=dict(type='list'),
|
||||
launch_config_name=dict(type='str'),
|
||||
min_size=dict(type='int'),
|
||||
max_size=dict(type='int'),
|
||||
desired_capacity=dict(type='int'),
|
||||
vpc_zone_identifier=dict(type='str'),
|
||||
replace_batch_size=dict(type='int', default=1),
|
||||
replace_all_instances=dict(type='bool', default=False),
|
||||
replace_instances=dict(type='list', default=[]),
|
||||
lc_check=dict(type='bool', default=True),
|
||||
wait_timeout=dict(type='int', default=300),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
tags=dict(type='list', default=[]),
|
||||
health_check_period=dict(type='int', default=300),
|
||||
health_check_type=dict(default='EC2', choices=['EC2', 'ELB']),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
state = module.params.get('state')
|
||||
replace_instances = module.params.get('replace_instances')
|
||||
replace_all_instances = module.params.get('replace_all_instances')
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
try:
|
||||
connection = connect_to_aws(boto.ec2.autoscale, region, **aws_connect_params)
|
||||
if not connection:
|
||||
module.fail_json(msg="failed to connect to AWS for the given region: %s" % str(region))
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
changed = False
|
||||
if replace_all_instances and replace_instances:
|
||||
module.fail_json(msg="You can't use replace_instances and replace_all_instances in the same task.")
|
||||
if state == 'present':
|
||||
create_changed, asg_properties=create_autoscaling_group(connection, module)
|
||||
if replace_all_instances or replace_instances:
|
||||
replace_changed, asg_properties=replace(connection, module)
|
||||
elif state == 'absent':
|
||||
changed = delete_autoscaling_group(connection, module)
|
||||
module.exit_json( changed = changed )
|
||||
if create_changed or replace_changed:
|
||||
changed = True
|
||||
module.exit_json( changed = changed, **asg_properties )
|
||||
|
||||
main()
|
@ -1,305 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ec2_eip
|
||||
short_description: associate an EC2 elastic IP with an instance.
|
||||
description:
|
||||
- This module associates AWS EC2 elastic IP addresses with instances
|
||||
version_added: 1.4
|
||||
options:
|
||||
instance_id:
|
||||
description:
|
||||
- The EC2 instance id
|
||||
required: false
|
||||
public_ip:
|
||||
description:
|
||||
- The elastic IP address to associate with the instance.
|
||||
- If absent, allocate a new address
|
||||
required: false
|
||||
state:
|
||||
description:
|
||||
- If present, associate the IP with the instance.
|
||||
- If absent, disassociate the IP with the instance.
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
region:
|
||||
description:
|
||||
- the EC2 region to use
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ ec2_region ]
|
||||
in_vpc:
|
||||
description:
|
||||
- allocate an EIP inside a VPC or not
|
||||
required: false
|
||||
default: false
|
||||
version_added: "1.4"
|
||||
reuse_existing_ip_allowed:
|
||||
description:
|
||||
- Reuse an EIP that is not associated to an instance (when available), instead of allocating a new one.
|
||||
required: false
|
||||
default: false
|
||||
version_added: "1.6"
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long to wait in seconds for newly provisioned EIPs to become available
|
||||
default: 300
|
||||
version_added: "1.7"
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
author: Lorin Hochstein <lorin@nimbisservices.com>
|
||||
notes:
|
||||
- This module will return C(public_ip) on success, which will contain the
|
||||
public IP address associated with the instance.
|
||||
- There may be a delay between the time the Elastic IP is assigned and when
|
||||
the cloud instance is reachable via the new address. Use wait_for and pause
|
||||
to delay further playbook execution until the instance is reachable, if
|
||||
necessary.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: associate an elastic IP with an instance
|
||||
ec2_eip: instance_id=i-1212f003 ip=93.184.216.119
|
||||
|
||||
- name: disassociate an elastic IP from an instance
|
||||
ec2_eip: instance_id=i-1212f003 ip=93.184.216.119 state=absent
|
||||
|
||||
- name: allocate a new elastic IP and associate it with an instance
|
||||
ec2_eip: instance_id=i-1212f003
|
||||
|
||||
- name: allocate a new elastic IP without associating it to anything
|
||||
ec2_eip:
|
||||
register: eip
|
||||
- name: output the IP
|
||||
debug: msg="Allocated IP is {{ eip.public_ip }}"
|
||||
|
||||
- name: provision new instances with ec2
|
||||
ec2: keypair=mykey instance_type=c1.medium image=emi-40603AD1 wait=yes group=webserver count=3
|
||||
register: ec2
|
||||
- name: associate new elastic IPs with each of the instances
|
||||
ec2_eip: "instance_id={{ item }}"
|
||||
with_items: ec2.instance_ids
|
||||
|
||||
- name: allocate a new elastic IP inside a VPC in us-west-2
|
||||
ec2_eip: region=us-west-2 in_vpc=yes
|
||||
register: eip
|
||||
- name: output the IP
|
||||
debug: msg="Allocated IP inside a VPC is {{ eip.public_ip }}"
|
||||
'''
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
boto_found = False
|
||||
else:
|
||||
boto_found = True
|
||||
|
||||
|
||||
wait_timeout = 0
|
||||
|
||||
def associate_ip_and_instance(ec2, address, instance_id, module):
|
||||
if ip_is_associated_with_instance(ec2, address.public_ip, instance_id, module):
|
||||
module.exit_json(changed=False, public_ip=address.public_ip)
|
||||
|
||||
# If we're in check mode, nothing else to do
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
try:
|
||||
if address.domain == "vpc":
|
||||
res = ec2.associate_address(instance_id, allocation_id=address.allocation_id)
|
||||
else:
|
||||
res = ec2.associate_address(instance_id, public_ip=address.public_ip)
|
||||
except boto.exception.EC2ResponseError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if res:
|
||||
module.exit_json(changed=True, public_ip=address.public_ip)
|
||||
else:
|
||||
module.fail_json(msg="association failed")
|
||||
|
||||
|
||||
def disassociate_ip_and_instance(ec2, address, instance_id, module):
|
||||
if not ip_is_associated_with_instance(ec2, address.public_ip, instance_id, module):
|
||||
module.exit_json(changed=False, public_ip=address.public_ip)
|
||||
|
||||
# If we're in check mode, nothing else to do
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
try:
|
||||
if address.domain == "vpc":
|
||||
res = ec2.disassociate_address(association_id=address.association_id)
|
||||
else:
|
||||
res = ec2.disassociate_address(public_ip=address.public_ip)
|
||||
except boto.exception.EC2ResponseError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if res:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.fail_json(msg="disassociation failed")
|
||||
|
||||
|
||||
def find_address(ec2, public_ip, module):
|
||||
""" Find an existing Elastic IP address """
|
||||
if wait_timeout != 0:
|
||||
timeout = time.time() + wait_timeout
|
||||
while timeout > time.time():
|
||||
try:
|
||||
addresses = ec2.get_all_addresses([public_ip])
|
||||
break
|
||||
except boto.exception.EC2ResponseError, e:
|
||||
if "Address '%s' not found." % public_ip in e.message :
|
||||
pass
|
||||
else:
|
||||
module.fail_json(msg=str(e.message))
|
||||
time.sleep(5)
|
||||
|
||||
if timeout <= time.time():
|
||||
module.fail_json(msg = "wait for EIPs timeout on %s" % time.asctime())
|
||||
else:
|
||||
try:
|
||||
addresses = ec2.get_all_addresses([public_ip])
|
||||
except boto.exception.EC2ResponseError, e:
|
||||
module.fail_json(msg=str(e.message))
|
||||
|
||||
return addresses[0]
|
||||
|
||||
|
||||
def ip_is_associated_with_instance(ec2, public_ip, instance_id, module):
|
||||
""" Check if the elastic IP is currently associated with the instance """
|
||||
address = find_address(ec2, public_ip, module)
|
||||
if address:
|
||||
return address.instance_id == instance_id
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def allocate_address(ec2, domain, module, reuse_existing_ip_allowed):
|
||||
""" Allocate a new elastic IP address (when needed) and return it """
|
||||
# If we're in check mode, nothing else to do
|
||||
if module.check_mode:
|
||||
module.exit_json(change=True)
|
||||
|
||||
if reuse_existing_ip_allowed:
|
||||
if domain:
|
||||
domain_filter = { 'domain' : domain }
|
||||
else:
|
||||
domain_filter = { 'domain' : 'standard' }
|
||||
all_addresses = ec2.get_all_addresses(filters=domain_filter)
|
||||
|
||||
unassociated_addresses = filter(lambda a: a.instance_id == "", all_addresses)
|
||||
if unassociated_addresses:
|
||||
address = unassociated_addresses[0];
|
||||
else:
|
||||
address = ec2.allocate_address(domain=domain)
|
||||
else:
|
||||
address = ec2.allocate_address(domain=domain)
|
||||
|
||||
return address
|
||||
|
||||
|
||||
def release_address(ec2, public_ip, module):
|
||||
""" Release a previously allocated elastic IP address """
|
||||
|
||||
address = find_address(ec2, public_ip, module)
|
||||
|
||||
# If we're in check mode, nothing else to do
|
||||
if module.check_mode:
|
||||
module.exit_json(change=True)
|
||||
|
||||
res = address.release()
|
||||
if res:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.fail_json(msg="release failed")
|
||||
|
||||
|
||||
def find_instance(ec2, instance_id, module):
|
||||
""" Attempt to find the EC2 instance and return it """
|
||||
|
||||
try:
|
||||
reservations = ec2.get_all_reservations(instance_ids=[instance_id])
|
||||
except boto.exception.EC2ResponseError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if len(reservations) == 1:
|
||||
instances = reservations[0].instances
|
||||
if len(instances) == 1:
|
||||
return instances[0]
|
||||
|
||||
module.fail_json(msg="could not find instance" + instance_id)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
instance_id = dict(required=False),
|
||||
public_ip = dict(required=False, aliases= ['ip']),
|
||||
state = dict(required=False, default='present',
|
||||
choices=['present', 'absent']),
|
||||
in_vpc = dict(required=False, type='bool', default=False),
|
||||
reuse_existing_ip_allowed = dict(required=False, type='bool', default=False),
|
||||
wait_timeout = dict(default=300),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not boto_found:
|
||||
module.fail_json(msg="boto is required")
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
instance_id = module.params.get('instance_id')
|
||||
public_ip = module.params.get('public_ip')
|
||||
state = module.params.get('state')
|
||||
in_vpc = module.params.get('in_vpc')
|
||||
domain = "vpc" if in_vpc else None
|
||||
reuse_existing_ip_allowed = module.params.get('reuse_existing_ip_allowed')
|
||||
new_eip_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
if state == 'present':
|
||||
# Allocate an EIP and exit
|
||||
if not instance_id and not public_ip:
|
||||
address = allocate_address(ec2, domain, module, reuse_existing_ip_allowed)
|
||||
module.exit_json(changed=True, public_ip=address.public_ip)
|
||||
|
||||
# Return the EIP object since we've been given a public IP
|
||||
if public_ip:
|
||||
address = find_address(ec2, public_ip, module)
|
||||
|
||||
# Allocate an IP for instance since no public_ip was provided
|
||||
if instance_id and not public_ip:
|
||||
instance = find_instance(ec2, instance_id, module)
|
||||
if instance.vpc_id:
|
||||
domain = "vpc"
|
||||
address = allocate_address(ec2, domain, module, reuse_existing_ip_allowed)
|
||||
# overriding the timeout since this is a a newly provisioned ip
|
||||
global wait_timeout
|
||||
wait_timeout = new_eip_timeout
|
||||
|
||||
# Associate address object (provided or allocated) with instance
|
||||
associate_ip_and_instance(ec2, address, instance_id, module)
|
||||
|
||||
else:
|
||||
#disassociating address from instance
|
||||
if instance_id:
|
||||
address = find_address(ec2, public_ip, module)
|
||||
disassociate_ip_and_instance(ec2, address, instance_id, module)
|
||||
#releasing address
|
||||
else:
|
||||
release_address(ec2, public_ip, module)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,339 +0,0 @@
|
||||
#!/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_elb
|
||||
short_description: De-registers or registers instances from EC2 ELBs
|
||||
description:
|
||||
- This module de-registers or registers an AWS EC2 instance from the ELBs
|
||||
that it belongs to.
|
||||
- Returns fact "ec2_elbs" which is a list of elbs attached to the instance
|
||||
if state=absent is passed as an argument.
|
||||
- Will be marked changed when called only if there are ELBs found to operate on.
|
||||
version_added: "1.2"
|
||||
author: John Jarvis
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the instance
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
instance_id:
|
||||
description:
|
||||
- EC2 Instance ID
|
||||
required: true
|
||||
ec2_elbs:
|
||||
description:
|
||||
- List of ELB names, required for registration. The ec2_elbs fact should be used if there was a previous de-register.
|
||||
required: false
|
||||
default: None
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
enable_availability_zone:
|
||||
description:
|
||||
- Whether to enable the availability zone of the instance on the target ELB if the availability zone has not already
|
||||
been enabled. If set to no, the task will fail if the availability zone is not enabled on the ELB.
|
||||
required: false
|
||||
default: yes
|
||||
choices: [ "yes", "no" ]
|
||||
wait:
|
||||
description:
|
||||
- Wait for instance registration or deregistration to complete successfully before returning.
|
||||
required: false
|
||||
default: yes
|
||||
choices: [ "yes", "no" ]
|
||||
validate_certs:
|
||||
description:
|
||||
- When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0.
|
||||
required: false
|
||||
default: "yes"
|
||||
choices: ["yes", "no"]
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
wait_timeout:
|
||||
description:
|
||||
- Number of seconds to wait for an instance to change state. If 0 then this module may return an error if a transient error occurs. If non-zero then any transient errors are ignored until the timeout is reached. Ignored when wait=no.
|
||||
required: false
|
||||
default: 0
|
||||
version_added: "1.6"
|
||||
extends_documentation_fragment: aws
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# basic pre_task and post_task example
|
||||
pre_tasks:
|
||||
- name: Gathering ec2 facts
|
||||
ec2_facts:
|
||||
- name: Instance De-register
|
||||
local_action: ec2_elb
|
||||
args:
|
||||
instance_id: "{{ ansible_ec2_instance_id }}"
|
||||
state: 'absent'
|
||||
roles:
|
||||
- myrole
|
||||
post_tasks:
|
||||
- name: Instance Register
|
||||
local_action: ec2_elb
|
||||
args:
|
||||
instance_id: "{{ ansible_ec2_instance_id }}"
|
||||
ec2_elbs: "{{ item }}"
|
||||
state: 'present'
|
||||
with_items: ec2_elbs
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
import boto
|
||||
import boto.ec2
|
||||
import boto.ec2.elb
|
||||
from boto.regioninfo import RegionInfo
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
class ElbManager:
|
||||
"""Handles EC2 instance ELB registration and de-registration"""
|
||||
|
||||
def __init__(self, module, instance_id=None, ec2_elbs=None,
|
||||
region=None, **aws_connect_params):
|
||||
self.module = module
|
||||
self.instance_id = instance_id
|
||||
self.region = region
|
||||
self.aws_connect_params = aws_connect_params
|
||||
self.lbs = self._get_instance_lbs(ec2_elbs)
|
||||
self.changed = False
|
||||
|
||||
def deregister(self, wait, timeout):
|
||||
"""De-register the instance from all ELBs and wait for the ELB
|
||||
to report it out-of-service"""
|
||||
|
||||
for lb in self.lbs:
|
||||
initial_state = self._get_instance_health(lb)
|
||||
if initial_state is None:
|
||||
# The instance isn't registered with this ELB so just
|
||||
# return unchanged
|
||||
return
|
||||
|
||||
lb.deregister_instances([self.instance_id])
|
||||
|
||||
# The ELB is changing state in some way. Either an instance that's
|
||||
# InService is moving to OutOfService, or an instance that's
|
||||
# already OutOfService is being deregistered.
|
||||
self.changed = True
|
||||
|
||||
if wait:
|
||||
self._await_elb_instance_state(lb, 'OutOfService', initial_state, timeout)
|
||||
|
||||
def register(self, wait, enable_availability_zone, timeout):
|
||||
"""Register the instance for all ELBs and wait for the ELB
|
||||
to report the instance in-service"""
|
||||
for lb in self.lbs:
|
||||
initial_state = self._get_instance_health(lb)
|
||||
|
||||
if enable_availability_zone:
|
||||
self._enable_availailability_zone(lb)
|
||||
|
||||
lb.register_instances([self.instance_id])
|
||||
|
||||
if wait:
|
||||
self._await_elb_instance_state(lb, 'InService', initial_state, timeout)
|
||||
else:
|
||||
# We cannot assume no change was made if we don't wait
|
||||
# to find out
|
||||
self.changed = True
|
||||
|
||||
def exists(self, lbtest):
|
||||
""" Verify that the named ELB actually exists """
|
||||
|
||||
found = False
|
||||
for lb in self.lbs:
|
||||
if lb.name == lbtest:
|
||||
found=True
|
||||
break
|
||||
return found
|
||||
|
||||
def _enable_availailability_zone(self, lb):
|
||||
"""Enable the current instance's availability zone in the provided lb.
|
||||
Returns True if the zone was enabled or False if no change was made.
|
||||
lb: load balancer"""
|
||||
instance = self._get_instance()
|
||||
if instance.placement in lb.availability_zones:
|
||||
return False
|
||||
|
||||
lb.enable_zones(zones=instance.placement)
|
||||
|
||||
# If successful, the new zone will have been added to
|
||||
# lb.availability_zones
|
||||
return instance.placement in lb.availability_zones
|
||||
|
||||
def _await_elb_instance_state(self, lb, awaited_state, initial_state, timeout):
|
||||
"""Wait for an ELB to change state
|
||||
lb: load balancer
|
||||
awaited_state : state to poll for (string)"""
|
||||
|
||||
wait_timeout = time.time() + timeout
|
||||
while True:
|
||||
instance_state = self._get_instance_health(lb)
|
||||
|
||||
if not instance_state:
|
||||
msg = ("The instance %s could not be put in service on %s."
|
||||
" Reason: Invalid Instance")
|
||||
self.module.fail_json(msg=msg % (self.instance_id, lb))
|
||||
|
||||
if instance_state.state == awaited_state:
|
||||
# Check the current state against the initial state, and only set
|
||||
# changed if they are different.
|
||||
if (initial_state is None) or (instance_state.state != initial_state.state):
|
||||
self.changed = True
|
||||
break
|
||||
elif self._is_instance_state_pending(instance_state):
|
||||
# If it's pending, we'll skip further checks andd continue waiting
|
||||
pass
|
||||
elif (awaited_state == 'InService'
|
||||
and instance_state.reason_code == "Instance"
|
||||
and time.time() >= wait_timeout):
|
||||
# If the reason_code for the instance being out of service is
|
||||
# "Instance" this indicates a failure state, e.g. the instance
|
||||
# has failed a health check or the ELB does not have the
|
||||
# instance's availabilty zone enabled. The exact reason why is
|
||||
# described in InstantState.description.
|
||||
msg = ("The instance %s could not be put in service on %s."
|
||||
" Reason: %s")
|
||||
self.module.fail_json(msg=msg % (self.instance_id,
|
||||
lb,
|
||||
instance_state.description))
|
||||
time.sleep(1)
|
||||
|
||||
def _is_instance_state_pending(self, instance_state):
|
||||
"""
|
||||
Determines whether the instance_state is "pending", meaning there is
|
||||
an operation under way to bring it in service.
|
||||
"""
|
||||
# This is messy, because AWS provides no way to distinguish between
|
||||
# an instance that is is OutOfService because it's pending vs. OutOfService
|
||||
# because it's failing health checks. So we're forced to analyze the
|
||||
# description, which is likely to be brittle.
|
||||
return (instance_state and 'pending' in instance_state.description)
|
||||
|
||||
def _get_instance_health(self, lb):
|
||||
"""
|
||||
Check instance health, should return status object or None under
|
||||
certain error conditions.
|
||||
"""
|
||||
try:
|
||||
status = lb.get_instance_health([self.instance_id])[0]
|
||||
except boto.exception.BotoServerError, e:
|
||||
if e.error_code == 'InvalidInstance':
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
return status
|
||||
|
||||
def _get_instance_lbs(self, ec2_elbs=None):
|
||||
"""Returns a list of ELBs attached to self.instance_id
|
||||
ec2_elbs: an optional list of elb names that will be used
|
||||
for elb lookup instead of returning what elbs
|
||||
are attached to self.instance_id"""
|
||||
|
||||
try:
|
||||
elb = connect_to_aws(boto.ec2.elb, self.region,
|
||||
**self.aws_connect_params)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
self.module.fail_json(msg=str(e))
|
||||
|
||||
elbs = elb.get_all_load_balancers()
|
||||
|
||||
if ec2_elbs:
|
||||
lbs = sorted(lb for lb in elbs if lb.name in ec2_elbs)
|
||||
else:
|
||||
lbs = []
|
||||
for lb in elbs:
|
||||
for info in lb.instances:
|
||||
if self.instance_id == info.id:
|
||||
lbs.append(lb)
|
||||
return lbs
|
||||
|
||||
def _get_instance(self):
|
||||
"""Returns a boto.ec2.InstanceObject for self.instance_id"""
|
||||
try:
|
||||
ec2 = connect_to_aws(boto.ec2, self.region,
|
||||
**self.aws_connect_params)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
self.module.fail_json(msg=str(e))
|
||||
return ec2.get_only_instances(instance_ids=[self.instance_id])[0]
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state={'required': True},
|
||||
instance_id={'required': True},
|
||||
ec2_elbs={'default': None, 'required': False, 'type':'list'},
|
||||
enable_availability_zone={'default': True, 'required': False, 'type': 'bool'},
|
||||
wait={'required': False, 'default': True, 'type': 'bool'},
|
||||
wait_timeout={'requred': False, 'default': 0, 'type': 'int'}
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
|
||||
if not region:
|
||||
module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file")
|
||||
|
||||
ec2_elbs = module.params['ec2_elbs']
|
||||
wait = module.params['wait']
|
||||
enable_availability_zone = module.params['enable_availability_zone']
|
||||
timeout = module.params['wait_timeout']
|
||||
|
||||
if module.params['state'] == 'present' and 'ec2_elbs' not in module.params:
|
||||
module.fail_json(msg="ELBs are required for registration")
|
||||
|
||||
instance_id = module.params['instance_id']
|
||||
elb_man = ElbManager(module, instance_id, ec2_elbs,
|
||||
region=region, **aws_connect_params)
|
||||
|
||||
if ec2_elbs is not None:
|
||||
for elb in ec2_elbs:
|
||||
if not elb_man.exists(elb):
|
||||
msg="ELB %s does not exist" % elb
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
elb_man.register(wait, enable_availability_zone, timeout)
|
||||
elif module.params['state'] == 'absent':
|
||||
elb_man.deregister(wait, timeout)
|
||||
|
||||
ansible_facts = {'ec2_elbs': [lb.name for lb in elb_man.lbs]}
|
||||
ec2_facts_result = dict(changed=elb_man.changed, ansible_facts=ansible_facts)
|
||||
|
||||
module.exit_json(**ec2_facts_result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,698 +0,0 @@
|
||||
#!/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_elb_lb
|
||||
description:
|
||||
- Returns information about the load balancer.
|
||||
- Will be marked changed when called only if state is changed.
|
||||
short_description: Creates or destroys Amazon ELB.
|
||||
version_added: "1.5"
|
||||
author: Jim Dalton
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Create or destroy the ELB
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- The name of the ELB
|
||||
required: true
|
||||
listeners:
|
||||
description:
|
||||
- List of ports/protocols for this ELB to listen on (see example)
|
||||
required: false
|
||||
purge_listeners:
|
||||
description:
|
||||
- Purge existing listeners on ELB that are not found in listeners
|
||||
required: false
|
||||
default: true
|
||||
zones:
|
||||
description:
|
||||
- List of availability zones to enable on this ELB
|
||||
required: false
|
||||
purge_zones:
|
||||
description:
|
||||
- Purge existing availability zones on ELB that are not found in zones
|
||||
required: false
|
||||
default: false
|
||||
security_group_ids:
|
||||
description:
|
||||
- A list of security groups to apply to the elb
|
||||
require: false
|
||||
default: None
|
||||
version_added: "1.6"
|
||||
health_check:
|
||||
description:
|
||||
- An associative array of health check configuration settigs (see example)
|
||||
require: false
|
||||
default: None
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
subnets:
|
||||
description:
|
||||
- A list of VPC subnets to use when creating ELB. Zones should be empty if using this.
|
||||
required: false
|
||||
default: None
|
||||
aliases: []
|
||||
version_added: "1.7"
|
||||
purge_subnets:
|
||||
description:
|
||||
- Purge existing subnet on ELB that are not found in subnets
|
||||
required: false
|
||||
default: false
|
||||
version_added: "1.7"
|
||||
scheme:
|
||||
description:
|
||||
- The scheme to use when creating the ELB. For a private VPC-visible ELB use 'internal'.
|
||||
required: false
|
||||
default: 'internet-facing'
|
||||
version_added: "1.7"
|
||||
validate_certs:
|
||||
description:
|
||||
- When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0.
|
||||
required: false
|
||||
default: "yes"
|
||||
choices: ["yes", "no"]
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
connection_draining_timeout:
|
||||
description:
|
||||
- Wait a specified timeout allowing connections to drain before terminating an instance
|
||||
required: false
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
cross_az_load_balancing:
|
||||
description:
|
||||
- Distribute load across all configured Availablity Zones
|
||||
required: false
|
||||
default: "no"
|
||||
choices: ["yes", "no"]
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Note: None of these examples set aws_access_key, aws_secret_key, or region.
|
||||
# It is assumed that their matching environment variables are set.
|
||||
|
||||
# Basic provisioning example
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "test-please-delete"
|
||||
state: present
|
||||
zones:
|
||||
- us-east-1a
|
||||
- us-east-1d
|
||||
listeners:
|
||||
- protocol: http # options are http, https, ssl, tcp
|
||||
load_balancer_port: 80
|
||||
instance_port: 80
|
||||
- protocol: https
|
||||
load_balancer_port: 443
|
||||
instance_protocol: http # optional, defaults to value of protocol setting
|
||||
instance_port: 80
|
||||
# ssl certificate required for https or ssl
|
||||
ssl_certificate_id: "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert"
|
||||
|
||||
|
||||
# Basic VPC provisioning example
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "test-vpc"
|
||||
scheme: internal
|
||||
state: present
|
||||
subnets:
|
||||
- subnet-abcd1234
|
||||
- subnet-1a2b3c4d
|
||||
listeners:
|
||||
- protocol: http # options are http, https, ssl, tcp
|
||||
load_balancer_port: 80
|
||||
instance_port: 80
|
||||
|
||||
# Configure a health check
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "test-please-delete"
|
||||
state: present
|
||||
zones:
|
||||
- us-east-1d
|
||||
listeners:
|
||||
- protocol: http
|
||||
load_balancer_port: 80
|
||||
instance_port: 80
|
||||
health_check:
|
||||
ping_protocol: http # options are http, https, ssl, tcp
|
||||
ping_port: 80
|
||||
ping_path: "/index.html" # not required for tcp or ssl
|
||||
response_timeout: 5 # seconds
|
||||
interval: 30 # seconds
|
||||
unhealthy_threshold: 2
|
||||
healthy_threshold: 10
|
||||
|
||||
# Ensure ELB is gone
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "test-please-delete"
|
||||
state: absent
|
||||
|
||||
# Normally, this module will purge any listeners that exist on the ELB
|
||||
# but aren't specified in the listeners parameter. If purge_listeners is
|
||||
# false it leaves them alone
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "test-please-delete"
|
||||
state: present
|
||||
zones:
|
||||
- us-east-1a
|
||||
- us-east-1d
|
||||
listeners:
|
||||
- protocol: http
|
||||
load_balancer_port: 80
|
||||
instance_port: 80
|
||||
purge_listeners: no
|
||||
|
||||
# Normally, this module will leave availability zones that are enabled
|
||||
# on the ELB alone. If purge_zones is true, then any extreneous zones
|
||||
# will be removed
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "test-please-delete"
|
||||
state: present
|
||||
zones:
|
||||
- us-east-1a
|
||||
- us-east-1d
|
||||
listeners:
|
||||
- protocol: http
|
||||
load_balancer_port: 80
|
||||
instance_port: 80
|
||||
purge_zones: yes
|
||||
|
||||
# Creates a ELB and assigns a list of subnets to it.
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
state: present
|
||||
name: 'New ELB'
|
||||
security_group_ids: 'sg-123456, sg-67890'
|
||||
region: us-west-2
|
||||
subnets: 'subnet-123456, subnet-67890'
|
||||
purge_subnets: yes
|
||||
listeners:
|
||||
- protocol: http
|
||||
load_balancer_port: 80
|
||||
instance_port: 80
|
||||
|
||||
# Create an ELB with connection draining and cross availability
|
||||
# zone load balancing
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
name: "New ELB"
|
||||
state: present
|
||||
connection_draining_timeout: 60
|
||||
cross_az_load_balancing: "yes"
|
||||
region: us-east-1
|
||||
zones:
|
||||
- us-east-1a
|
||||
- us-east-1d
|
||||
listeners:
|
||||
- protocols: http
|
||||
- load_balancer_port: 80
|
||||
- instance_port: 80
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
import boto
|
||||
import boto.ec2.elb
|
||||
import boto.ec2.elb.attributes
|
||||
from boto.ec2.elb.healthcheck import HealthCheck
|
||||
from boto.regioninfo import RegionInfo
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ElbManager(object):
|
||||
"""Handles ELB creation and destruction"""
|
||||
|
||||
def __init__(self, module, name, listeners=None, purge_listeners=None,
|
||||
zones=None, purge_zones=None, security_group_ids=None,
|
||||
health_check=None, subnets=None, purge_subnets=None,
|
||||
scheme="internet-facing", connection_draining_timeout=None,
|
||||
cross_az_load_balancing=None, region=None, **aws_connect_params):
|
||||
|
||||
self.module = module
|
||||
self.name = name
|
||||
self.listeners = listeners
|
||||
self.purge_listeners = purge_listeners
|
||||
self.zones = zones
|
||||
self.purge_zones = purge_zones
|
||||
self.security_group_ids = security_group_ids
|
||||
self.health_check = health_check
|
||||
self.subnets = subnets
|
||||
self.purge_subnets = purge_subnets
|
||||
self.scheme = scheme
|
||||
self.connection_draining_timeout = connection_draining_timeout
|
||||
self.cross_az_load_balancing = cross_az_load_balancing
|
||||
|
||||
self.aws_connect_params = aws_connect_params
|
||||
self.region = region
|
||||
|
||||
self.changed = False
|
||||
self.status = 'gone'
|
||||
self.elb_conn = self._get_elb_connection()
|
||||
self.elb = self._get_elb()
|
||||
|
||||
def ensure_ok(self):
|
||||
"""Create the ELB"""
|
||||
if not self.elb:
|
||||
# Zones and listeners will be added at creation
|
||||
self._create_elb()
|
||||
else:
|
||||
self._set_zones()
|
||||
self._set_security_groups()
|
||||
self._set_elb_listeners()
|
||||
self._set_subnets()
|
||||
self._set_health_check()
|
||||
# boto has introduced support for some ELB attributes in
|
||||
# different versions, so we check first before trying to
|
||||
# set them to avoid errors
|
||||
if self._check_attribute_support('connection_draining'):
|
||||
self._set_connection_draining_timeout()
|
||||
if self._check_attribute_support('cross_zone_load_balancing'):
|
||||
self._set_cross_az_load_balancing()
|
||||
|
||||
def ensure_gone(self):
|
||||
"""Destroy the ELB"""
|
||||
if self.elb:
|
||||
self._delete_elb()
|
||||
|
||||
def get_info(self):
|
||||
try:
|
||||
check_elb = self.elb_conn.get_all_load_balancers(self.name)[0]
|
||||
except:
|
||||
check_elb = None
|
||||
|
||||
if not check_elb:
|
||||
info = {
|
||||
'name': self.name,
|
||||
'status': self.status
|
||||
}
|
||||
else:
|
||||
info = {
|
||||
'name': check_elb.name,
|
||||
'dns_name': check_elb.dns_name,
|
||||
'zones': check_elb.availability_zones,
|
||||
'security_group_ids': check_elb.security_groups,
|
||||
'status': self.status,
|
||||
'subnets': self.subnets,
|
||||
'scheme': check_elb.scheme
|
||||
}
|
||||
|
||||
if check_elb.health_check:
|
||||
info['health_check'] = {
|
||||
'target': check_elb.health_check.target,
|
||||
'interval': check_elb.health_check.interval,
|
||||
'timeout': check_elb.health_check.timeout,
|
||||
'healthy_threshold': check_elb.health_check.healthy_threshold,
|
||||
'unhealthy_threshold': check_elb.health_check.unhealthy_threshold,
|
||||
}
|
||||
|
||||
if check_elb.listeners:
|
||||
info['listeners'] = [l.get_complex_tuple()
|
||||
for l in check_elb.listeners]
|
||||
elif self.status == 'created':
|
||||
# When creating a new ELB, listeners don't show in the
|
||||
# immediately returned result, so just include the
|
||||
# ones that were added
|
||||
info['listeners'] = [self._listener_as_tuple(l)
|
||||
for l in self.listeners]
|
||||
else:
|
||||
info['listeners'] = []
|
||||
|
||||
if self._check_attribute_support('connection_draining'):
|
||||
info['connection_draining_timeout'] = self.elb_conn.get_lb_attribute(self.name, 'ConnectionDraining').timeout
|
||||
|
||||
if self._check_attribute_support('cross_zone_load_balancing'):
|
||||
is_cross_az_lb_enabled = self.elb_conn.get_lb_attribute(self.name, 'CrossZoneLoadBalancing')
|
||||
if is_cross_az_lb_enabled:
|
||||
info['cross_az_load_balancing'] = 'yes'
|
||||
else:
|
||||
info['cross_az_load_balancing'] = 'no'
|
||||
|
||||
return info
|
||||
|
||||
def _get_elb(self):
|
||||
elbs = self.elb_conn.get_all_load_balancers()
|
||||
for elb in elbs:
|
||||
if self.name == elb.name:
|
||||
self.status = 'ok'
|
||||
return elb
|
||||
|
||||
def _get_elb_connection(self):
|
||||
try:
|
||||
return connect_to_aws(boto.ec2.elb, self.region,
|
||||
**self.aws_connect_params)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
self.module.fail_json(msg=str(e))
|
||||
|
||||
def _delete_elb(self):
|
||||
# True if succeeds, exception raised if not
|
||||
result = self.elb_conn.delete_load_balancer(name=self.name)
|
||||
if result:
|
||||
self.changed = True
|
||||
self.status = 'deleted'
|
||||
|
||||
def _create_elb(self):
|
||||
listeners = [self._listener_as_tuple(l) for l in self.listeners]
|
||||
self.elb = self.elb_conn.create_load_balancer(name=self.name,
|
||||
zones=self.zones,
|
||||
security_groups=self.security_group_ids,
|
||||
complex_listeners=listeners,
|
||||
subnets=self.subnets,
|
||||
scheme=self.scheme)
|
||||
if self.elb:
|
||||
self.changed = True
|
||||
self.status = 'created'
|
||||
|
||||
def _create_elb_listeners(self, listeners):
|
||||
"""Takes a list of listener tuples and creates them"""
|
||||
# True if succeeds, exception raised if not
|
||||
self.changed = self.elb_conn.create_load_balancer_listeners(self.name,
|
||||
complex_listeners=listeners)
|
||||
|
||||
def _delete_elb_listeners(self, listeners):
|
||||
"""Takes a list of listener tuples and deletes them from the elb"""
|
||||
ports = [l[0] for l in listeners]
|
||||
|
||||
# True if succeeds, exception raised if not
|
||||
self.changed = self.elb_conn.delete_load_balancer_listeners(self.name,
|
||||
ports)
|
||||
|
||||
def _set_elb_listeners(self):
|
||||
"""
|
||||
Creates listeners specified by self.listeners; overwrites existing
|
||||
listeners on these ports; removes extraneous listeners
|
||||
"""
|
||||
listeners_to_add = []
|
||||
listeners_to_remove = []
|
||||
listeners_to_keep = []
|
||||
|
||||
# Check for any listeners we need to create or overwrite
|
||||
for listener in self.listeners:
|
||||
listener_as_tuple = self._listener_as_tuple(listener)
|
||||
|
||||
# First we loop through existing listeners to see if one is
|
||||
# already specified for this port
|
||||
existing_listener_found = None
|
||||
for existing_listener in self.elb.listeners:
|
||||
# Since ELB allows only one listener on each incoming port, a
|
||||
# single match on the incomping port is all we're looking for
|
||||
if existing_listener[0] == listener['load_balancer_port']:
|
||||
existing_listener_found = existing_listener.get_complex_tuple()
|
||||
break
|
||||
|
||||
if existing_listener_found:
|
||||
# Does it match exactly?
|
||||
if listener_as_tuple != existing_listener_found:
|
||||
# The ports are the same but something else is different,
|
||||
# so we'll remove the exsiting one and add the new one
|
||||
listeners_to_remove.append(existing_listener_found)
|
||||
listeners_to_add.append(listener_as_tuple)
|
||||
else:
|
||||
# We already have this listener, so we're going to keep it
|
||||
listeners_to_keep.append(existing_listener_found)
|
||||
else:
|
||||
# We didn't find an existing listener, so just add the new one
|
||||
listeners_to_add.append(listener_as_tuple)
|
||||
|
||||
# Check for any extraneous listeners we need to remove, if desired
|
||||
if self.purge_listeners:
|
||||
for existing_listener in self.elb.listeners:
|
||||
existing_listener_tuple = existing_listener.get_complex_tuple()
|
||||
if existing_listener_tuple in listeners_to_remove:
|
||||
# Already queued for removal
|
||||
continue
|
||||
if existing_listener_tuple in listeners_to_keep:
|
||||
# Keep this one around
|
||||
continue
|
||||
# Since we're not already removing it and we don't need to keep
|
||||
# it, let's get rid of it
|
||||
listeners_to_remove.append(existing_listener_tuple)
|
||||
|
||||
if listeners_to_remove:
|
||||
self._delete_elb_listeners(listeners_to_remove)
|
||||
|
||||
if listeners_to_add:
|
||||
self._create_elb_listeners(listeners_to_add)
|
||||
|
||||
def _listener_as_tuple(self, listener):
|
||||
"""Formats listener as a 4- or 5-tuples, in the order specified by the
|
||||
ELB API"""
|
||||
# N.B. string manipulations on protocols below (str(), upper()) is to
|
||||
# ensure format matches output from ELB API
|
||||
listener_list = [
|
||||
listener['load_balancer_port'],
|
||||
listener['instance_port'],
|
||||
str(listener['protocol'].upper()),
|
||||
]
|
||||
|
||||
# Instance protocol is not required by ELB API; it defaults to match
|
||||
# load balancer protocol. We'll mimic that behavior here
|
||||
if 'instance_protocol' in listener:
|
||||
listener_list.append(str(listener['instance_protocol'].upper()))
|
||||
else:
|
||||
listener_list.append(str(listener['protocol'].upper()))
|
||||
|
||||
if 'ssl_certificate_id' in listener:
|
||||
listener_list.append(str(listener['ssl_certificate_id']))
|
||||
|
||||
return tuple(listener_list)
|
||||
|
||||
def _enable_zones(self, zones):
|
||||
try:
|
||||
self.elb.enable_zones(zones)
|
||||
except boto.exception.BotoServerError, e:
|
||||
if "Invalid Availability Zone" in e.error_message:
|
||||
self.module.fail_json(msg=e.error_message)
|
||||
else:
|
||||
self.module.fail_json(msg="an unknown server error occurred, please try again later")
|
||||
self.changed = True
|
||||
|
||||
def _disable_zones(self, zones):
|
||||
try:
|
||||
self.elb.disable_zones(zones)
|
||||
except boto.exception.BotoServerError, e:
|
||||
if "Invalid Availability Zone" in e.error_message:
|
||||
self.module.fail_json(msg=e.error_message)
|
||||
else:
|
||||
self.module.fail_json(msg="an unknown server error occurred, please try again later")
|
||||
self.changed = True
|
||||
|
||||
def _attach_subnets(self, subnets):
|
||||
self.elb_conn.attach_lb_to_subnets(self.name, subnets)
|
||||
self.changed = True
|
||||
|
||||
def _detach_subnets(self, subnets):
|
||||
self.elb_conn.detach_lb_from_subnets(self.name, subnets)
|
||||
self.changed = True
|
||||
|
||||
def _set_subnets(self):
|
||||
"""Determine which subnets need to be attached or detached on the ELB"""
|
||||
if self.subnets:
|
||||
if self.purge_subnets:
|
||||
subnets_to_detach = list(set(self.elb.subnets) - set(self.subnets))
|
||||
subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets))
|
||||
else:
|
||||
subnets_to_detach = None
|
||||
subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets))
|
||||
|
||||
if subnets_to_attach:
|
||||
self._attach_subnets(subnets_to_attach)
|
||||
if subnets_to_detach:
|
||||
self._detach_subnets(subnets_to_detach)
|
||||
|
||||
def _set_zones(self):
|
||||
"""Determine which zones need to be enabled or disabled on the ELB"""
|
||||
if self.zones:
|
||||
if self.purge_zones:
|
||||
zones_to_disable = list(set(self.elb.availability_zones) -
|
||||
set(self.zones))
|
||||
zones_to_enable = list(set(self.zones) -
|
||||
set(self.elb.availability_zones))
|
||||
else:
|
||||
zones_to_disable = None
|
||||
zones_to_enable = list(set(self.zones) -
|
||||
set(self.elb.availability_zones))
|
||||
if zones_to_enable:
|
||||
self._enable_zones(zones_to_enable)
|
||||
# N.B. This must come second, in case it would have removed all zones
|
||||
if zones_to_disable:
|
||||
self._disable_zones(zones_to_disable)
|
||||
|
||||
def _set_security_groups(self):
|
||||
if self.security_group_ids != None and set(self.elb.security_groups) != set(self.security_group_ids):
|
||||
self.elb_conn.apply_security_groups_to_lb(self.name, self.security_group_ids)
|
||||
self.Changed = True
|
||||
|
||||
def _set_health_check(self):
|
||||
"""Set health check values on ELB as needed"""
|
||||
if self.health_check:
|
||||
# This just makes it easier to compare each of the attributes
|
||||
# and look for changes. Keys are attributes of the current
|
||||
# health_check; values are desired values of new health_check
|
||||
health_check_config = {
|
||||
"target": self._get_health_check_target(),
|
||||
"timeout": self.health_check['response_timeout'],
|
||||
"interval": self.health_check['interval'],
|
||||
"unhealthy_threshold": self.health_check['unhealthy_threshold'],
|
||||
"healthy_threshold": self.health_check['healthy_threshold'],
|
||||
}
|
||||
|
||||
update_health_check = False
|
||||
|
||||
# The health_check attribute is *not* set on newly created
|
||||
# ELBs! So we have to create our own.
|
||||
if not self.elb.health_check:
|
||||
self.elb.health_check = HealthCheck()
|
||||
|
||||
for attr, desired_value in health_check_config.iteritems():
|
||||
if getattr(self.elb.health_check, attr) != desired_value:
|
||||
setattr(self.elb.health_check, attr, desired_value)
|
||||
update_health_check = True
|
||||
|
||||
if update_health_check:
|
||||
self.elb.configure_health_check(self.elb.health_check)
|
||||
self.changed = True
|
||||
|
||||
def _check_attribute_support(self, attr):
|
||||
return hasattr(boto.ec2.elb.attributes.LbAttributes(), attr)
|
||||
|
||||
def _set_cross_az_load_balancing(self):
|
||||
attributes = self.elb.get_attributes()
|
||||
if self.cross_az_load_balancing:
|
||||
attributes.cross_zone_load_balancing.enabled = True
|
||||
else:
|
||||
attributes.cross_zone_load_balancing.enabled = False
|
||||
self.elb_conn.modify_lb_attribute(self.name, 'CrossZoneLoadBalancing',
|
||||
attributes.cross_zone_load_balancing.enabled)
|
||||
|
||||
def _set_connection_draining_timeout(self):
|
||||
attributes = self.elb.get_attributes()
|
||||
if self.connection_draining_timeout is not None:
|
||||
attributes.connection_draining.enabled = True
|
||||
attributes.connection_draining.timeout = self.connection_draining_timeout
|
||||
self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining)
|
||||
else:
|
||||
attributes.connection_draining.enabled = False
|
||||
self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining)
|
||||
|
||||
def _get_health_check_target(self):
|
||||
"""Compose target string from healthcheck parameters"""
|
||||
protocol = self.health_check['ping_protocol'].upper()
|
||||
path = ""
|
||||
|
||||
if protocol in ['HTTP', 'HTTPS'] and 'ping_path' in self.health_check:
|
||||
path = self.health_check['ping_path']
|
||||
|
||||
return "%s:%s%s" % (protocol, self.health_check['ping_port'], path)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state={'required': True, 'choices': ['present', 'absent']},
|
||||
name={'required': True},
|
||||
listeners={'default': None, 'required': False, 'type': 'list'},
|
||||
purge_listeners={'default': True, 'required': False, 'type': 'bool'},
|
||||
zones={'default': None, 'required': False, 'type': 'list'},
|
||||
purge_zones={'default': False, 'required': False, 'type': 'bool'},
|
||||
security_group_ids={'default': None, 'required': False, 'type': 'list'},
|
||||
health_check={'default': None, 'required': False, 'type': 'dict'},
|
||||
subnets={'default': None, 'required': False, 'type': 'list'},
|
||||
purge_subnets={'default': False, 'required': False, 'type': 'bool'},
|
||||
scheme={'default': 'internet-facing', 'required': False},
|
||||
connection_draining_timeout={'default': None, 'required': False},
|
||||
cross_az_load_balancing={'default': None, 'required': False}
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
if not region:
|
||||
module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file")
|
||||
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
listeners = module.params['listeners']
|
||||
purge_listeners = module.params['purge_listeners']
|
||||
zones = module.params['zones']
|
||||
purge_zones = module.params['purge_zones']
|
||||
security_group_ids = module.params['security_group_ids']
|
||||
health_check = module.params['health_check']
|
||||
subnets = module.params['subnets']
|
||||
purge_subnets = module.params['purge_subnets']
|
||||
scheme = module.params['scheme']
|
||||
connection_draining_timeout = module.params['connection_draining_timeout']
|
||||
cross_az_load_balancing = module.params['cross_az_load_balancing']
|
||||
|
||||
if state == 'present' and not listeners:
|
||||
module.fail_json(msg="At least one port is required for ELB creation")
|
||||
|
||||
if state == 'present' and not (zones or subnets):
|
||||
module.fail_json(msg="At least one availability zone or subnet is required for ELB creation")
|
||||
|
||||
elb_man = ElbManager(module, name, listeners, purge_listeners, zones,
|
||||
purge_zones, security_group_ids, health_check,
|
||||
subnets, purge_subnets, scheme,
|
||||
connection_draining_timeout, cross_az_load_balancing,
|
||||
region=region, **aws_connect_params)
|
||||
|
||||
# check for unsupported attributes for this version of boto
|
||||
if cross_az_load_balancing and not elb_man._check_attribute_support('cross_zone_load_balancing'):
|
||||
module.fail_json(msg="You must install boto >= 2.18.0 to use the cross_az_load_balancing attribute")
|
||||
|
||||
if connection_draining_timeout and not elb_man._check_attribute_support('connection_draining'):
|
||||
module.fail_json(msg="You must install boto >= 2.28.0 to use the connection_draining_timeout attribute")
|
||||
|
||||
if state == 'present':
|
||||
elb_man.ensure_ok()
|
||||
elif state == 'absent':
|
||||
elb_man.ensure_gone()
|
||||
|
||||
ansible_facts = {'ec2_elb': 'info'}
|
||||
ec2_facts_result = dict(changed=elb_man.changed,
|
||||
elb=elb_man.get_info(),
|
||||
ansible_facts=ansible_facts)
|
||||
|
||||
module.exit_json(**ec2_facts_result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,182 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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_facts
|
||||
short_description: Gathers facts about remote hosts within ec2 (aws)
|
||||
version_added: "1.0"
|
||||
options:
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used
|
||||
on personally controlled sites using self-signed certificates.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
version_added: 1.5.1
|
||||
description:
|
||||
- This module fetches data from the metadata servers in ec2 (aws) as per
|
||||
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html.
|
||||
The module must be called from within the EC2 instance itself.
|
||||
Eucalyptus cloud provides a similar service and this module should
|
||||
work with this cloud provider as well.
|
||||
notes:
|
||||
- Parameters to filter on ec2_facts may be added later.
|
||||
author: "Silviu Dicu <silviudicu@gmail.com>"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Conditional example
|
||||
- name: Gather facts
|
||||
action: ec2_facts
|
||||
|
||||
- name: Conditional
|
||||
action: debug msg="This instance is a t1.micro"
|
||||
when: ansible_ec2_instance_type == "t1.micro"
|
||||
'''
|
||||
|
||||
import socket
|
||||
import re
|
||||
|
||||
socket.setdefaulttimeout(5)
|
||||
|
||||
class Ec2Metadata(object):
|
||||
|
||||
ec2_metadata_uri = 'http://169.254.169.254/latest/meta-data/'
|
||||
ec2_sshdata_uri = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key'
|
||||
ec2_userdata_uri = 'http://169.254.169.254/latest/user-data/'
|
||||
|
||||
AWS_REGIONS = ('ap-northeast-1',
|
||||
'ap-southeast-1',
|
||||
'ap-southeast-2',
|
||||
'eu-west-1',
|
||||
'sa-east-1',
|
||||
'us-east-1',
|
||||
'us-west-1',
|
||||
'us-west-2',
|
||||
'us-gov-west-1'
|
||||
)
|
||||
|
||||
def __init__(self, module, ec2_metadata_uri=None, ec2_sshdata_uri=None, ec2_userdata_uri=None):
|
||||
self.module = module
|
||||
self.uri_meta = ec2_metadata_uri or self.ec2_metadata_uri
|
||||
self.uri_user = ec2_userdata_uri or self.ec2_userdata_uri
|
||||
self.uri_ssh = ec2_sshdata_uri or self.ec2_sshdata_uri
|
||||
self._data = {}
|
||||
self._prefix = 'ansible_ec2_%s'
|
||||
|
||||
def _fetch(self, url):
|
||||
(response, info) = fetch_url(self.module, url, force=True)
|
||||
if response:
|
||||
data = response.read()
|
||||
else:
|
||||
data = None
|
||||
return data
|
||||
|
||||
def _mangle_fields(self, fields, uri, filter_patterns=['public-keys-0']):
|
||||
new_fields = {}
|
||||
for key, value in fields.iteritems():
|
||||
split_fields = key[len(uri):].split('/')
|
||||
if len(split_fields) > 1 and split_fields[1]:
|
||||
new_key = "-".join(split_fields)
|
||||
new_fields[self._prefix % new_key] = value
|
||||
else:
|
||||
new_key = "".join(split_fields)
|
||||
new_fields[self._prefix % new_key] = value
|
||||
for pattern in filter_patterns:
|
||||
for key in new_fields.keys():
|
||||
match = re.search(pattern, key)
|
||||
if match:
|
||||
new_fields.pop(key)
|
||||
return new_fields
|
||||
|
||||
def fetch(self, uri, recurse=True):
|
||||
raw_subfields = self._fetch(uri)
|
||||
if not raw_subfields:
|
||||
return
|
||||
subfields = raw_subfields.split('\n')
|
||||
for field in subfields:
|
||||
if field.endswith('/') and recurse:
|
||||
self.fetch(uri + field)
|
||||
if uri.endswith('/'):
|
||||
new_uri = uri + field
|
||||
else:
|
||||
new_uri = uri + '/' + field
|
||||
if new_uri not in self._data and not new_uri.endswith('/'):
|
||||
content = self._fetch(new_uri)
|
||||
if field == 'security-groups':
|
||||
sg_fields = ",".join(content.split('\n'))
|
||||
self._data['%s' % (new_uri)] = sg_fields
|
||||
else:
|
||||
self._data['%s' % (new_uri)] = content
|
||||
|
||||
def fix_invalid_varnames(self, data):
|
||||
"""Change ':'' and '-' to '_' to ensure valid template variable names"""
|
||||
for (key, value) in data.items():
|
||||
if ':' in key or '-' in key:
|
||||
newkey = key.replace(':','_').replace('-','_')
|
||||
del data[key]
|
||||
data[newkey] = value
|
||||
|
||||
def add_ec2_region(self, data):
|
||||
"""Use the 'ansible_ec2_placement_availability_zone' key/value
|
||||
pair to add 'ansible_ec2_placement_region' key/value pair with
|
||||
the EC2 region name.
|
||||
"""
|
||||
|
||||
# Only add a 'ansible_ec2_placement_region' key if the
|
||||
# 'ansible_ec2_placement_availability_zone' exists.
|
||||
zone = data.get('ansible_ec2_placement_availability_zone')
|
||||
if zone is not None:
|
||||
# Use the zone name as the region name unless the zone
|
||||
# name starts with a known AWS region name.
|
||||
region = zone
|
||||
for r in self.AWS_REGIONS:
|
||||
if zone.startswith(r):
|
||||
region = r
|
||||
break
|
||||
data['ansible_ec2_placement_region'] = region
|
||||
|
||||
def run(self):
|
||||
self.fetch(self.uri_meta) # populate _data
|
||||
data = self._mangle_fields(self._data, self.uri_meta)
|
||||
data[self._prefix % 'user-data'] = self._fetch(self.uri_user)
|
||||
data[self._prefix % 'public-key'] = self._fetch(self.uri_ssh)
|
||||
self.fix_invalid_varnames(data)
|
||||
self.add_ec2_region(data)
|
||||
return data
|
||||
|
||||
def main():
|
||||
argument_spec = url_argument_spec()
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = argument_spec,
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
ec2_facts = Ec2Metadata(module).run()
|
||||
ec2_facts_result = dict(changed=False, ansible_facts=ec2_facts)
|
||||
|
||||
module.exit_json(**ec2_facts_result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.urls import *
|
||||
|
||||
main()
|
@ -1,386 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ec2_group
|
||||
version_added: "1.3"
|
||||
short_description: maintain an ec2 VPC security group.
|
||||
description:
|
||||
- maintains ec2 security groups. This module has a dependency on python-boto >= 2.5
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the security group.
|
||||
required: true
|
||||
description:
|
||||
description:
|
||||
- Description of the security group.
|
||||
required: true
|
||||
vpc_id:
|
||||
description:
|
||||
- ID of the VPC to create the group in.
|
||||
required: false
|
||||
rules:
|
||||
description:
|
||||
- List of firewall inbound rules to enforce in this group (see example).
|
||||
required: false
|
||||
rules_egress:
|
||||
description:
|
||||
- List of firewall outbound rules to enforce in this group (see example).
|
||||
required: false
|
||||
version_added: "1.6"
|
||||
region:
|
||||
description:
|
||||
- the EC2 region to use
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
version_added: "1.4"
|
||||
description:
|
||||
- Create or delete a security group
|
||||
required: false
|
||||
default: 'present'
|
||||
choices: [ "present", "absent" ]
|
||||
aliases: []
|
||||
purge_rules:
|
||||
version_added: "1.8"
|
||||
description:
|
||||
- Purge existing rules on security group that are not found in rules
|
||||
required: false
|
||||
default: 'true'
|
||||
aliases: []
|
||||
purge_rules_egress:
|
||||
version_added: "1.8"
|
||||
description:
|
||||
- Purge existing rules_egree on security group that are not found in rules_egress
|
||||
required: false
|
||||
default: 'true'
|
||||
aliases: []
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
|
||||
notes:
|
||||
- If a rule declares a group_name and that group doesn't exist, it will be
|
||||
automatically created. In that case, group_desc should be provided as well.
|
||||
The module will refuse to create a depended-on group without a description.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: example ec2 group
|
||||
local_action:
|
||||
module: ec2_group
|
||||
name: example
|
||||
description: an example EC2 group
|
||||
vpc_id: 12345
|
||||
region: eu-west-1a
|
||||
aws_secret_key: SECRET
|
||||
aws_access_key: ACCESS
|
||||
rules:
|
||||
- proto: tcp
|
||||
from_port: 80
|
||||
to_port: 80
|
||||
cidr_ip: 0.0.0.0/0
|
||||
- proto: tcp
|
||||
from_port: 22
|
||||
to_port: 22
|
||||
cidr_ip: 10.0.0.0/8
|
||||
- proto: udp
|
||||
from_port: 10050
|
||||
to_port: 10050
|
||||
cidr_ip: 10.0.0.0/8
|
||||
- proto: udp
|
||||
from_port: 10051
|
||||
to_port: 10051
|
||||
group_id: sg-12345678
|
||||
- proto: all
|
||||
# the containing group name may be specified here
|
||||
group_name: example
|
||||
rules_egress:
|
||||
- proto: tcp
|
||||
from_port: 80
|
||||
to_port: 80
|
||||
group_name: example-other
|
||||
# description to use if example-other needs to be created
|
||||
group_desc: other example EC2 group
|
||||
'''
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def addRulesToLookup(rules, prefix, dict):
|
||||
for rule in rules:
|
||||
for grant in rule.grants:
|
||||
dict["%s-%s-%s-%s-%s-%s" % (prefix, rule.ip_protocol, rule.from_port, rule.to_port,
|
||||
grant.group_id, grant.cidr_ip)] = rule
|
||||
|
||||
|
||||
def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id):
|
||||
"""
|
||||
Returns tuple of (group_id, ip) after validating rule params.
|
||||
|
||||
rule: Dict describing a rule.
|
||||
name: Name of the security group being managed.
|
||||
groups: Dict of all available security groups.
|
||||
|
||||
AWS accepts an ip range or a security group as target of a rule. This
|
||||
function validate the rule specification and return either a non-None
|
||||
group_id or a non-None ip range.
|
||||
"""
|
||||
|
||||
group_id = None
|
||||
group_name = None
|
||||
ip = None
|
||||
target_group_created = False
|
||||
if 'group_id' in rule and 'cidr_ip' in rule:
|
||||
module.fail_json(msg="Specify group_id OR cidr_ip, not both")
|
||||
elif 'group_name' in rule and 'cidr_ip' in rule:
|
||||
module.fail_json(msg="Specify group_name OR cidr_ip, not both")
|
||||
elif 'group_id' in rule and 'group_name' in rule:
|
||||
module.fail_json(msg="Specify group_id OR group_name, not both")
|
||||
elif 'group_id' in rule:
|
||||
group_id = rule['group_id']
|
||||
elif 'group_name' in rule:
|
||||
group_name = rule['group_name']
|
||||
if group_name in groups:
|
||||
group_id = groups[group_name].id
|
||||
elif group_name == name:
|
||||
group_id = group.id
|
||||
groups[group_id] = group
|
||||
groups[group_name] = group
|
||||
else:
|
||||
if not rule.get('group_desc', '').strip():
|
||||
module.fail_json(msg="group %s will be automatically created by rule %s and no description was provided" % (group_name, rule))
|
||||
if not module.check_mode:
|
||||
auto_group = ec2.create_security_group(group_name, rule['group_desc'], vpc_id=vpc_id)
|
||||
group_id = auto_group.id
|
||||
groups[group_id] = auto_group
|
||||
groups[group_name] = auto_group
|
||||
target_group_created = True
|
||||
elif 'cidr_ip' in rule:
|
||||
ip = rule['cidr_ip']
|
||||
|
||||
return group_id, ip, target_group_created
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name=dict(required=True),
|
||||
description=dict(required=True),
|
||||
vpc_id=dict(),
|
||||
rules=dict(),
|
||||
rules_egress=dict(),
|
||||
state = dict(default='present', choices=['present', 'absent']),
|
||||
purge_rules=dict(default=True, required=False, type='bool'),
|
||||
purge_rules_egress=dict(default=True, required=False, type='bool'),
|
||||
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
name = module.params['name']
|
||||
description = module.params['description']
|
||||
vpc_id = module.params['vpc_id']
|
||||
rules = module.params['rules']
|
||||
rules_egress = module.params['rules_egress']
|
||||
state = module.params.get('state')
|
||||
purge_rules = module.params['purge_rules']
|
||||
purge_rules_egress = module.params['purge_rules_egress']
|
||||
|
||||
changed = False
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
# find the group if present
|
||||
group = None
|
||||
groups = {}
|
||||
for curGroup in ec2.get_all_security_groups():
|
||||
groups[curGroup.id] = curGroup
|
||||
groups[curGroup.name] = curGroup
|
||||
|
||||
if curGroup.name == name and (vpc_id is None or curGroup.vpc_id == vpc_id):
|
||||
group = curGroup
|
||||
|
||||
# Ensure requested group is absent
|
||||
if state == 'absent':
|
||||
if group:
|
||||
'''found a match, delete it'''
|
||||
try:
|
||||
group.delete()
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e))
|
||||
else:
|
||||
group = None
|
||||
changed = True
|
||||
else:
|
||||
'''no match found, no changes required'''
|
||||
|
||||
# Ensure requested group is present
|
||||
elif state == 'present':
|
||||
if group:
|
||||
'''existing group found'''
|
||||
# check the group parameters are correct
|
||||
group_in_use = False
|
||||
rs = ec2.get_all_instances()
|
||||
for r in rs:
|
||||
for i in r.instances:
|
||||
group_in_use |= reduce(lambda x, y: x | (y.name == 'public-ssh'), i.groups, False)
|
||||
|
||||
if group.description != description:
|
||||
if group_in_use:
|
||||
module.fail_json(msg="Group description does not match, but it is in use so cannot be changed.")
|
||||
|
||||
# if the group doesn't exist, create it now
|
||||
else:
|
||||
'''no match found, create it'''
|
||||
if not module.check_mode:
|
||||
group = ec2.create_security_group(name, description, vpc_id=vpc_id)
|
||||
|
||||
# When a group is created, an egress_rule ALLOW ALL
|
||||
# to 0.0.0.0/0 is added automatically but it's not
|
||||
# reflected in the object returned by the AWS API
|
||||
# call. We re-read the group for getting an updated object
|
||||
# amazon sometimes takes a couple seconds to update the security group so wait till it exists
|
||||
while len(ec2.get_all_security_groups(filters={ 'group_id': group.id, })) == 0:
|
||||
time.sleep(0.1)
|
||||
|
||||
group = ec2.get_all_security_groups(group_ids=(group.id,))[0]
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Unsupported state requested: %s" % state)
|
||||
|
||||
# create a lookup for all existing rules on the group
|
||||
if group:
|
||||
|
||||
# Manage ingress rules
|
||||
groupRules = {}
|
||||
addRulesToLookup(group.rules, 'in', groupRules)
|
||||
|
||||
# Now, go through all provided rules and ensure they are there.
|
||||
if rules:
|
||||
for rule in rules:
|
||||
group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id)
|
||||
if target_group_created:
|
||||
changed = True
|
||||
|
||||
if rule['proto'] in ('all', '-1', -1):
|
||||
rule['proto'] = -1
|
||||
rule['from_port'] = None
|
||||
rule['to_port'] = None
|
||||
|
||||
# If rule already exists, don't later delete it
|
||||
ruleId = "%s-%s-%s-%s-%s-%s" % ('in', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip)
|
||||
if ruleId in groupRules:
|
||||
del groupRules[ruleId]
|
||||
# Otherwise, add new rule
|
||||
else:
|
||||
grantGroup = None
|
||||
if group_id:
|
||||
grantGroup = groups[group_id]
|
||||
|
||||
if not module.check_mode:
|
||||
group.authorize(rule['proto'], rule['from_port'], rule['to_port'], ip, grantGroup)
|
||||
changed = True
|
||||
|
||||
# Finally, remove anything left in the groupRules -- these will be defunct rules
|
||||
if purge_rules:
|
||||
for rule in groupRules.itervalues() :
|
||||
for grant in rule.grants:
|
||||
grantGroup = None
|
||||
if grant.group_id:
|
||||
grantGroup = groups[grant.group_id]
|
||||
if not module.check_mode:
|
||||
group.revoke(rule.ip_protocol, rule.from_port, rule.to_port, grant.cidr_ip, grantGroup)
|
||||
changed = True
|
||||
|
||||
# Manage egress rules
|
||||
groupRules = {}
|
||||
addRulesToLookup(group.rules_egress, 'out', groupRules)
|
||||
|
||||
# Now, go through all provided rules and ensure they are there.
|
||||
if rules_egress:
|
||||
for rule in rules_egress:
|
||||
group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id)
|
||||
if target_group_created:
|
||||
changed = True
|
||||
|
||||
if rule['proto'] in ('all', '-1', -1):
|
||||
rule['proto'] = -1
|
||||
rule['from_port'] = None
|
||||
rule['to_port'] = None
|
||||
|
||||
# If rule already exists, don't later delete it
|
||||
ruleId = "%s-%s-%s-%s-%s-%s" % ('out', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip)
|
||||
if ruleId in groupRules:
|
||||
del groupRules[ruleId]
|
||||
# Otherwise, add new rule
|
||||
else:
|
||||
grantGroup = None
|
||||
if group_id:
|
||||
grantGroup = groups[group_id].id
|
||||
|
||||
if not module.check_mode:
|
||||
ec2.authorize_security_group_egress(
|
||||
group_id=group.id,
|
||||
ip_protocol=rule['proto'],
|
||||
from_port=rule['from_port'],
|
||||
to_port=rule['to_port'],
|
||||
src_group_id=grantGroup,
|
||||
cidr_ip=ip)
|
||||
changed = True
|
||||
elif vpc_id and not module.check_mode:
|
||||
# when using a vpc, but no egress rules are specified,
|
||||
# we add in a default allow all out rule, which was the
|
||||
# default behavior before egress rules were added
|
||||
default_egress_rule = 'out--1-None-None-None-0.0.0.0/0'
|
||||
if default_egress_rule not in groupRules:
|
||||
ec2.authorize_security_group_egress(
|
||||
group_id=group.id,
|
||||
ip_protocol=-1,
|
||||
from_port=None,
|
||||
to_port=None,
|
||||
src_group_id=None,
|
||||
cidr_ip='0.0.0.0/0'
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
# make sure the default egress rule is not removed
|
||||
del groupRules[default_egress_rule]
|
||||
|
||||
# Finally, remove anything left in the groupRules -- these will be defunct rules
|
||||
if purge_rules_egress:
|
||||
for rule in groupRules.itervalues():
|
||||
for grant in rule.grants:
|
||||
grantGroup = None
|
||||
if grant.group_id:
|
||||
grantGroup = groups[grant.group_id].id
|
||||
if not module.check_mode:
|
||||
ec2.revoke_security_group_egress(
|
||||
group_id=group.id,
|
||||
ip_protocol=rule.ip_protocol,
|
||||
from_port=rule.from_port,
|
||||
to_port=rule.to_port,
|
||||
src_group_id=grantGroup,
|
||||
cidr_ip=grant.cidr_ip)
|
||||
changed = True
|
||||
|
||||
if group:
|
||||
module.exit_json(changed=changed, group_id=group.id)
|
||||
else:
|
||||
module.exit_json(changed=changed, group_id=None)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,238 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ec2_key
|
||||
version_added: "1.5"
|
||||
short_description: maintain an ec2 key pair.
|
||||
description:
|
||||
- maintains ec2 key pairs. This module has a dependency on python-boto >= 2.5
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the key pair.
|
||||
required: true
|
||||
key_material:
|
||||
description:
|
||||
- Public key material.
|
||||
required: false
|
||||
region:
|
||||
description:
|
||||
- the EC2 region to use
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- create or delete keypair
|
||||
required: false
|
||||
default: 'present'
|
||||
aliases: []
|
||||
wait:
|
||||
description:
|
||||
- Wait for the specified action to complete before returning.
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
wait_timeout:
|
||||
description:
|
||||
- How long before wait gives up, in seconds
|
||||
required: false
|
||||
default: 300
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
author: Vincent Viallet
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Note: None of these examples set aws_access_key, aws_secret_key, or region.
|
||||
# It is assumed that their matching environment variables are set.
|
||||
|
||||
# Creates a new ec2 key pair named `example` if not present, returns generated
|
||||
# private key
|
||||
- name: example ec2 key
|
||||
local_action:
|
||||
module: ec2_key
|
||||
name: example
|
||||
|
||||
# Creates a new ec2 key pair named `example` if not present using provided key
|
||||
# material
|
||||
- name: example2 ec2 key
|
||||
local_action:
|
||||
module: ec2_key
|
||||
name: example2
|
||||
key_material: 'ssh-rsa AAAAxyz...== me@example.com'
|
||||
state: present
|
||||
|
||||
# Creates a new ec2 key pair named `example` if not present using provided key
|
||||
# material
|
||||
- name: example3 ec2 key
|
||||
local_action:
|
||||
module: ec2_key
|
||||
name: example3
|
||||
key_material: "{{ item }}"
|
||||
with_file: /path/to/public_key.id_rsa.pub
|
||||
|
||||
# Removes ec2 key pair by name
|
||||
- name: remove example key
|
||||
local_action:
|
||||
module: ec2_key
|
||||
name: example
|
||||
state: absent
|
||||
'''
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name=dict(required=True),
|
||||
key_material=dict(required=False),
|
||||
state = dict(default='present', choices=['present', 'absent']),
|
||||
wait = dict(type='bool', default=False),
|
||||
wait_timeout = dict(default=300),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
name = module.params['name']
|
||||
state = module.params.get('state')
|
||||
key_material = module.params.get('key_material')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
changed = False
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
# find the key if present
|
||||
key = ec2.get_key_pair(name)
|
||||
|
||||
# Ensure requested key is absent
|
||||
if state == 'absent':
|
||||
if key:
|
||||
'''found a match, delete it'''
|
||||
try:
|
||||
key.delete()
|
||||
if wait:
|
||||
start = time.time()
|
||||
action_complete = False
|
||||
while (time.time() - start) < wait_timeout:
|
||||
if not ec2.get_key_pair(name):
|
||||
action_complete = True
|
||||
break
|
||||
time.sleep(1)
|
||||
if not action_complete:
|
||||
module.fail_json(msg="timed out while waiting for the key to be removed")
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Unable to delete key pair '%s' - %s" % (key, e))
|
||||
else:
|
||||
key = None
|
||||
changed = True
|
||||
else:
|
||||
'''no match found, no changes required'''
|
||||
|
||||
# Ensure requested key is present
|
||||
elif state == 'present':
|
||||
if key:
|
||||
# existing key found
|
||||
if key_material:
|
||||
# EC2's fingerprints are non-trivial to generate, so push this key
|
||||
# to a temporary name and make ec2 calculate the fingerprint for us.
|
||||
#
|
||||
# http://blog.jbrowne.com/?p=23
|
||||
# https://forums.aws.amazon.com/thread.jspa?messageID=352828
|
||||
|
||||
# find an unused name
|
||||
test = 'empty'
|
||||
while test:
|
||||
randomchars = [random.choice(string.ascii_letters + string.digits) for x in range(0,10)]
|
||||
tmpkeyname = "ansible-" + ''.join(randomchars)
|
||||
test = ec2.get_key_pair(tmpkeyname)
|
||||
|
||||
# create tmp key
|
||||
tmpkey = ec2.import_key_pair(tmpkeyname, key_material)
|
||||
# get tmp key fingerprint
|
||||
tmpfingerprint = tmpkey.fingerprint
|
||||
# delete tmp key
|
||||
tmpkey.delete()
|
||||
|
||||
if key.fingerprint != tmpfingerprint:
|
||||
if not module.check_mode:
|
||||
key.delete()
|
||||
key = ec2.import_key_pair(name, key_material)
|
||||
|
||||
if wait:
|
||||
start = time.time()
|
||||
action_complete = False
|
||||
while (time.time() - start) < wait_timeout:
|
||||
if ec2.get_key_pair(name):
|
||||
action_complete = True
|
||||
break
|
||||
time.sleep(1)
|
||||
if not action_complete:
|
||||
module.fail_json(msg="timed out while waiting for the key to be re-created")
|
||||
|
||||
changed = True
|
||||
pass
|
||||
|
||||
# if the key doesn't exist, create it now
|
||||
else:
|
||||
'''no match found, create it'''
|
||||
if not module.check_mode:
|
||||
if key_material:
|
||||
'''We are providing the key, need to import'''
|
||||
key = ec2.import_key_pair(name, key_material)
|
||||
else:
|
||||
'''
|
||||
No material provided, let AWS handle the key creation and
|
||||
retrieve the private key
|
||||
'''
|
||||
key = ec2.create_key_pair(name)
|
||||
|
||||
if wait:
|
||||
start = time.time()
|
||||
action_complete = False
|
||||
while (time.time() - start) < wait_timeout:
|
||||
if ec2.get_key_pair(name):
|
||||
action_complete = True
|
||||
break
|
||||
time.sleep(1)
|
||||
if not action_complete:
|
||||
module.fail_json(msg="timed out while waiting for the key to be created")
|
||||
|
||||
changed = True
|
||||
|
||||
if key:
|
||||
data = {
|
||||
'name': key.name,
|
||||
'fingerprint': key.fingerprint
|
||||
}
|
||||
if key.material:
|
||||
data.update({'private_key': key.material})
|
||||
|
||||
module.exit_json(changed=changed, key=data)
|
||||
else:
|
||||
module.exit_json(changed=changed, key=None)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,278 +0,0 @@
|
||||
#!/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_lc
|
||||
short_description: Create or delete AWS Autoscaling Launch Configurations
|
||||
description:
|
||||
- Can create or delete AwS Autoscaling Configurations
|
||||
- Works with the ec2_asg module to manage Autoscaling Groups
|
||||
notes:
|
||||
- "Amazon ASG Autoscaling Launch Configurations are immutable once created, so modifying the configuration
|
||||
after it is changed will not modify the launch configuration on AWS. You must create a new config and assign
|
||||
it to the ASG instead."
|
||||
version_added: "1.6"
|
||||
author: Gareth Rushgrove
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the instance
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
name:
|
||||
description:
|
||||
- Unique name for configuration
|
||||
required: true
|
||||
instance_type:
|
||||
description:
|
||||
- instance type to use for the instance
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
image_id:
|
||||
description:
|
||||
- The AMI unique identifier to be used for the group
|
||||
required: false
|
||||
key_name:
|
||||
description:
|
||||
- The SSH key name to be used for access to managed instances
|
||||
required: false
|
||||
security_groups:
|
||||
description:
|
||||
- A list of security groups into which instances should be found
|
||||
required: false
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
volumes:
|
||||
description:
|
||||
- a list of volume dicts, each containing device name and optionally ephemeral id or snapshot id. Size and type (and number of iops for io device type) must be specified for a new volume or a root volume, and may be passed for a snapshot volume. For any volume, a volume size less than 1 will be interpreted as a request not to create the volume.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
user_data:
|
||||
description:
|
||||
- opaque blob of data which is made available to the ec2 instance
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
kernel_id:
|
||||
description:
|
||||
- Kernel id for the EC2 instance
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
spot_price:
|
||||
description:
|
||||
- The spot price you are bidding. Only applies for an autoscaling group with spot instances.
|
||||
required: false
|
||||
default: null
|
||||
instance_monitoring:
|
||||
description:
|
||||
- whether instances in group are launched with detailed monitoring.
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
assign_public_ip:
|
||||
description:
|
||||
- Used for Auto Scaling groups that launch instances into an Amazon Virtual Private Cloud. Specifies whether to assign a public IP address to each instance launched in a Amazon VPC.
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
ramdisk_id:
|
||||
description:
|
||||
- A RAM disk id for the instances.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
instance_profile_name:
|
||||
description:
|
||||
- The name or the Amazon Resource Name (ARN) of the instance profile associated with the IAM role for the instances.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
ebs_optimized:
|
||||
description:
|
||||
- Specifies whether the instance is optimized for EBS I/O (true) or not (false).
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.8"
|
||||
extends_documentation_fragment: aws
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- ec2_lc:
|
||||
name: special
|
||||
image_id: ami-XXX
|
||||
key_name: default
|
||||
security_groups: 'group,group2'
|
||||
instance_type: t1.micro
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
try:
|
||||
from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
|
||||
import boto.ec2.autoscale
|
||||
from boto.ec2.autoscale import LaunchConfiguration
|
||||
from boto.exception import BotoServerError
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_block_device(module, volume):
|
||||
# Not aware of a way to determine this programatically
|
||||
# http://aws.amazon.com/about-aws/whats-new/2013/10/09/ebs-provisioned-iops-maximum-iops-gb-ratio-increased-to-30-1/
|
||||
MAX_IOPS_TO_SIZE_RATIO = 30
|
||||
if 'snapshot' not in volume and 'ephemeral' not in volume:
|
||||
if 'volume_size' not in volume:
|
||||
module.fail_json(msg='Size must be specified when creating a new volume or modifying the root volume')
|
||||
if 'snapshot' in volume:
|
||||
if 'device_type' in volume and volume.get('device_type') == 'io1' and 'iops' not in volume:
|
||||
module.fail_json(msg='io1 volumes must have an iops value set')
|
||||
if 'ephemeral' in volume:
|
||||
if 'snapshot' in volume:
|
||||
module.fail_json(msg='Cannot set both ephemeral and snapshot')
|
||||
return BlockDeviceType(snapshot_id=volume.get('snapshot'),
|
||||
ephemeral_name=volume.get('ephemeral'),
|
||||
size=volume.get('volume_size'),
|
||||
volume_type=volume.get('device_type'),
|
||||
delete_on_termination=volume.get('delete_on_termination', False),
|
||||
iops=volume.get('iops'))
|
||||
|
||||
|
||||
def create_launch_config(connection, module):
|
||||
name = module.params.get('name')
|
||||
image_id = module.params.get('image_id')
|
||||
key_name = module.params.get('key_name')
|
||||
security_groups = module.params['security_groups']
|
||||
user_data = module.params.get('user_data')
|
||||
volumes = module.params['volumes']
|
||||
instance_type = module.params.get('instance_type')
|
||||
spot_price = module.params.get('spot_price')
|
||||
instance_monitoring = module.params.get('instance_monitoring')
|
||||
assign_public_ip = module.params.get('assign_public_ip')
|
||||
kernel_id = module.params.get('kernel_id')
|
||||
ramdisk_id = module.params.get('ramdisk_id')
|
||||
instance_profile_name = module.params.get('instance_profile_name')
|
||||
ebs_optimized = module.params.get('ebs_optimized')
|
||||
bdm = BlockDeviceMapping()
|
||||
|
||||
if volumes:
|
||||
for volume in volumes:
|
||||
if 'device_name' not in volume:
|
||||
module.fail_json(msg='Device name must be set for volume')
|
||||
# Minimum volume size is 1GB. We'll use volume size explicitly set to 0
|
||||
# to be a signal not to create this volume
|
||||
if 'volume_size' not in volume or int(volume['volume_size']) > 0:
|
||||
bdm[volume['device_name']] = create_block_device(module, volume)
|
||||
|
||||
lc = LaunchConfiguration(
|
||||
name=name,
|
||||
image_id=image_id,
|
||||
key_name=key_name,
|
||||
security_groups=security_groups,
|
||||
user_data=user_data,
|
||||
block_device_mappings=[bdm],
|
||||
instance_type=instance_type,
|
||||
kernel_id=kernel_id,
|
||||
spot_price=spot_price,
|
||||
instance_monitoring=instance_monitoring,
|
||||
associate_public_ip_address = assign_public_ip,
|
||||
ramdisk_id=ramdisk_id,
|
||||
instance_profile_name=instance_profile_name,
|
||||
ebs_optimized=ebs_optimized,
|
||||
)
|
||||
|
||||
launch_configs = connection.get_all_launch_configurations(names=[name])
|
||||
changed = False
|
||||
if not launch_configs:
|
||||
try:
|
||||
connection.create_launch_configuration(lc)
|
||||
launch_configs = connection.get_all_launch_configurations(names=[name])
|
||||
changed = True
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
result = launch_configs[0]
|
||||
|
||||
module.exit_json(changed=changed, name=result.name, created_time=str(result.created_time),
|
||||
image_id=result.image_id, arn=result.launch_configuration_arn,
|
||||
security_groups=result.security_groups, instance_type=instance_type)
|
||||
|
||||
|
||||
def delete_launch_config(connection, module):
|
||||
name = module.params.get('name')
|
||||
launch_configs = connection.get_all_launch_configurations(names=[name])
|
||||
if launch_configs:
|
||||
launch_configs[0].delete()
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(required=True, type='str'),
|
||||
image_id=dict(type='str'),
|
||||
key_name=dict(type='str'),
|
||||
security_groups=dict(type='list'),
|
||||
user_data=dict(type='str'),
|
||||
kernel_id=dict(type='str'),
|
||||
volumes=dict(type='list'),
|
||||
instance_type=dict(type='str'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
spot_price=dict(type='float'),
|
||||
ramdisk_id=dict(type='str'),
|
||||
instance_profile_name=dict(type='str'),
|
||||
ebs_optimized=dict(default=False, type='bool'),
|
||||
associate_public_ip_address=dict(type='bool'),
|
||||
instance_monitoring=dict(default=False, type='bool'),
|
||||
assign_public_ip=dict(default=False, type='bool')
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
|
||||
try:
|
||||
connection = connect_to_aws(boto.ec2.autoscale, region, **aws_connect_params)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
if state == 'present':
|
||||
create_launch_config(connection, module)
|
||||
elif state == 'absent':
|
||||
delete_launch_config(connection, module)
|
||||
|
||||
main()
|
@ -1,282 +0,0 @@
|
||||
#!/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_metric_alarm
|
||||
short_description: "Create/update or delete AWS Cloudwatch 'metric alarms'"
|
||||
description:
|
||||
- Can create or delete AWS metric alarms
|
||||
- Metrics you wish to alarm on must already exist
|
||||
version_added: "1.6"
|
||||
author: Zacharie Eakin
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the alarm
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
name:
|
||||
desciption:
|
||||
- Unique name for the alarm
|
||||
required: true
|
||||
metric:
|
||||
description:
|
||||
- Name of the monitored metric (e.g. CPUUtilization)
|
||||
- Metric must already exist
|
||||
required: false
|
||||
namespace:
|
||||
description:
|
||||
- Name of the appropriate namespace ('AWS/EC2', 'System/Linux', etc.), which determines the category it will appear under in cloudwatch
|
||||
required: false
|
||||
statistic:
|
||||
description:
|
||||
- Operation applied to the metric
|
||||
- Works in conjunction with period and evaluation_periods to determine the comparison value
|
||||
required: false
|
||||
options: ['SampleCount','Average','Sum','Minimum','Maximum']
|
||||
comparison:
|
||||
description:
|
||||
- Determines how the threshold value is compared
|
||||
required: false
|
||||
options: ['<=','<','>','>=']
|
||||
threshold:
|
||||
description:
|
||||
- Sets the min/max bound for triggering the alarm
|
||||
required: false
|
||||
period:
|
||||
description:
|
||||
- The time (in seconds) between metric evaluations
|
||||
required: false
|
||||
evaluation_periods:
|
||||
description:
|
||||
- The number of times in which the metric is evaluated before final calculation
|
||||
required: false
|
||||
unit:
|
||||
description:
|
||||
- The threshold's unit of measurement
|
||||
required: false
|
||||
options: ['Seconds','Microseconds','Milliseconds','Bytes','Kilobytes','Megabytes','Gigabytes','Terabytes','Bits','Kilobits','Megabits','Gigabits','Terabits','Percent','Count','Bytes/Second','Kilobytes/Second','Megabytes/Second','Gigabytes/Second','Terabytes/Second','Bits/Second','Kilobits/Second','Megabits/Second','Gigabits/Second','Terabits/Second','Count/Second','None']
|
||||
description:
|
||||
description:
|
||||
- A longer desciption of the alarm
|
||||
required: false
|
||||
dimensions:
|
||||
description:
|
||||
- Describes to what the alarm is applied
|
||||
required: false
|
||||
alarm_actions:
|
||||
description:
|
||||
- A list of the names action(s) taken when the alarm is in the 'alarm' status
|
||||
required: false
|
||||
insufficient_data_actions:
|
||||
description:
|
||||
- A list of the names of action(s) to take when the alarm is in the 'insufficient_data' status
|
||||
required: false
|
||||
ok_actions:
|
||||
description:
|
||||
- A list of the names of action(s) to take when the alarm is in the 'ok' status
|
||||
required: false
|
||||
extends_documentation_fragment: aws
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: create alarm
|
||||
ec2_metric_alarm:
|
||||
state: present
|
||||
region: ap-southeast-2
|
||||
name: "cpu-low"
|
||||
metric: "CPUUtilization"
|
||||
namespace: "AWS/EC2"
|
||||
statistic: Average
|
||||
comparison: "<="
|
||||
threshold: 5.0
|
||||
period: 300
|
||||
evaluation_periods: 3
|
||||
unit: "Percent"
|
||||
description: "This will alarm when a bamboo slave's cpu usage average is lower than 5% for 15 minutes "
|
||||
dimensions: {'InstanceId':'i-XXX'}
|
||||
alarm_actions: ["action1","action2"]
|
||||
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
try:
|
||||
import boto.ec2.cloudwatch
|
||||
from boto.ec2.cloudwatch import CloudWatchConnection, MetricAlarm
|
||||
from boto.exception import BotoServerError
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_metric_alarm(connection, module):
|
||||
|
||||
name = module.params.get('name')
|
||||
metric = module.params.get('metric')
|
||||
namespace = module.params.get('namespace')
|
||||
statistic = module.params.get('statistic')
|
||||
comparison = module.params.get('comparison')
|
||||
threshold = module.params.get('threshold')
|
||||
period = module.params.get('period')
|
||||
evaluation_periods = module.params.get('evaluation_periods')
|
||||
unit = module.params.get('unit')
|
||||
description = module.params.get('description')
|
||||
dimensions = module.params.get('dimensions')
|
||||
alarm_actions = module.params.get('alarm_actions')
|
||||
insufficient_data_actions = module.params.get('insufficient_data_actions')
|
||||
ok_actions = module.params.get('ok_actions')
|
||||
|
||||
alarms = connection.describe_alarms(alarm_names=[name])
|
||||
|
||||
if not alarms:
|
||||
|
||||
alm = MetricAlarm(
|
||||
name=name,
|
||||
metric=metric,
|
||||
namespace=namespace,
|
||||
statistic=statistic,
|
||||
comparison=comparison,
|
||||
threshold=threshold,
|
||||
period=period,
|
||||
evaluation_periods=evaluation_periods,
|
||||
unit=unit,
|
||||
description=description,
|
||||
dimensions=dimensions,
|
||||
alarm_actions=alarm_actions,
|
||||
insufficient_data_actions=insufficient_data_actions,
|
||||
ok_actions=ok_actions
|
||||
)
|
||||
try:
|
||||
connection.create_alarm(alm)
|
||||
changed = True
|
||||
alarms = connection.describe_alarms(alarm_names=[name])
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
else:
|
||||
alarm = alarms[0]
|
||||
changed = False
|
||||
|
||||
for attr in ('comparison','metric','namespace','statistic','threshold','period','evaluation_periods','unit','description'):
|
||||
if getattr(alarm, attr) != module.params.get(attr):
|
||||
changed = True
|
||||
setattr(alarm, attr, module.params.get(attr))
|
||||
#this is to deal with a current bug where you cannot assign '<=>' to the comparator when modifying an existing alarm
|
||||
comparison = alarm.comparison
|
||||
comparisons = {'<=' : 'LessThanOrEqualToThreshold', '<' : 'LessThanThreshold', '>=' : 'GreaterThanOrEqualToThreshold', '>' : 'GreaterThanThreshold'}
|
||||
alarm.comparison = comparisons[comparison]
|
||||
|
||||
dim1 = module.params.get('dimensions')
|
||||
dim2 = alarm.dimensions
|
||||
|
||||
for keys in dim1:
|
||||
if not isinstance(dim1[keys], list):
|
||||
dim1[keys] = [dim1[keys]]
|
||||
if dim1[keys] != dim2[keys]:
|
||||
changed=True
|
||||
setattr(alarm, 'dimensions', dim1)
|
||||
|
||||
for attr in ('alarm_actions','insufficient_data_actions','ok_actions'):
|
||||
action = module.params.get(attr) or []
|
||||
if getattr(alarm, attr) != action:
|
||||
changed = True
|
||||
setattr(alarm, attr, module.params.get(attr))
|
||||
|
||||
try:
|
||||
if changed:
|
||||
connection.create_alarm(alarm)
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
result = alarms[0]
|
||||
module.exit_json(changed=changed, name=result.name,
|
||||
actions_enabled=result.actions_enabled,
|
||||
alarm_actions=result.alarm_actions,
|
||||
alarm_arn=result.alarm_arn,
|
||||
comparison=result.comparison,
|
||||
description=result.description,
|
||||
dimensions=result.dimensions,
|
||||
evaluation_periods=result.evaluation_periods,
|
||||
insufficient_data_actions=result.insufficient_data_actions,
|
||||
last_updated=result.last_updated,
|
||||
metric=result.metric,
|
||||
namespace=result.namespace,
|
||||
ok_actions=result.ok_actions,
|
||||
period=result.period,
|
||||
state_reason=result.state_reason,
|
||||
state_value=result.state_value,
|
||||
statistic=result.statistic,
|
||||
threshold=result.threshold,
|
||||
unit=result.unit)
|
||||
|
||||
def delete_metric_alarm(connection, module):
|
||||
name = module.params.get('name')
|
||||
|
||||
alarms = connection.describe_alarms(alarm_names=[name])
|
||||
|
||||
if alarms:
|
||||
try:
|
||||
connection.delete_alarms([name])
|
||||
module.exit_json(changed=True)
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(required=True, type='str'),
|
||||
metric=dict(type='str'),
|
||||
namespace=dict(type='str'),
|
||||
statistic=dict(type='str', choices=['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum']),
|
||||
comparison=dict(type='str', choices=['<=', '<', '>', '>=']),
|
||||
threshold=dict(type='float'),
|
||||
period=dict(type='int'),
|
||||
unit=dict(type='str', choices=['Seconds', 'Microseconds', 'Milliseconds', 'Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', 'Terabytes', 'Bits', 'Kilobits', 'Megabits', 'Gigabits', 'Terabits', 'Percent', 'Count', 'Bytes/Second', 'Kilobytes/Second', 'Megabytes/Second', 'Gigabytes/Second', 'Terabytes/Second', 'Bits/Second', 'Kilobits/Second', 'Megabits/Second', 'Gigabits/Second', 'Terabits/Second', 'Count/Second', 'None']),
|
||||
evaluation_periods=dict(type='int'),
|
||||
description=dict(type='str'),
|
||||
dimensions=dict(type='dict'),
|
||||
alarm_actions=dict(type='list'),
|
||||
insufficient_data_actions=dict(type='list'),
|
||||
ok_actions=dict(type='list'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
region=dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
try:
|
||||
connection = connect_to_aws(boto.ec2.cloudwatch, region, **aws_connect_params)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if state == 'present':
|
||||
create_metric_alarm(connection, module)
|
||||
elif state == 'absent':
|
||||
delete_metric_alarm(connection, module)
|
||||
|
||||
main()
|
@ -1,177 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: ec2_scaling_policy
|
||||
short_description: Create or delete AWS scaling policies for Autoscaling groups
|
||||
description:
|
||||
- Can create or delete scaling policies for autoscaling groups
|
||||
- Referenced autoscaling groups must already exist
|
||||
version_added: "1.6"
|
||||
author: Zacharie Eakin
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the policy
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
name:
|
||||
description:
|
||||
- Unique name for the scaling policy
|
||||
required: true
|
||||
asg_name:
|
||||
description:
|
||||
- Name of the associated autoscaling group
|
||||
required: true
|
||||
adjustment_type:
|
||||
desciption:
|
||||
- The type of change in capacity of the autoscaling group
|
||||
required: false
|
||||
choices: ['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity']
|
||||
scaling_adjustment:
|
||||
description:
|
||||
- The amount by which the autoscaling group is adjusted by the policy
|
||||
required: false
|
||||
min_adjustment_step:
|
||||
description:
|
||||
- Minimum amount of adjustment when policy is triggered
|
||||
required: false
|
||||
cooldown:
|
||||
description:
|
||||
- The minimum period of time between which autoscaling actions can take place
|
||||
required: false
|
||||
extends_documentation_fragment: aws
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- ec2_scaling_policy:
|
||||
state: present
|
||||
region: US-XXX
|
||||
name: "scaledown-policy"
|
||||
adjustment_type: "ChangeInCapacity"
|
||||
asg_name: "slave-pool"
|
||||
scaling_adjustment: -1
|
||||
min_adjustment_step: 1
|
||||
cooldown: 300
|
||||
'''
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
try:
|
||||
import boto.ec2.autoscale
|
||||
from boto.ec2.autoscale import ScalingPolicy
|
||||
from boto.exception import BotoServerError
|
||||
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_scaling_policy(connection, module):
|
||||
sp_name = module.params.get('name')
|
||||
adjustment_type = module.params.get('adjustment_type')
|
||||
asg_name = module.params.get('asg_name')
|
||||
scaling_adjustment = module.params.get('scaling_adjustment')
|
||||
min_adjustment_step = module.params.get('min_adjustment_step')
|
||||
cooldown = module.params.get('cooldown')
|
||||
|
||||
scalingPolicies = connection.get_all_policies(as_group=asg_name,policy_names=[sp_name])
|
||||
|
||||
if not scalingPolicies:
|
||||
sp = ScalingPolicy(
|
||||
name=sp_name,
|
||||
adjustment_type=adjustment_type,
|
||||
as_name=asg_name,
|
||||
scaling_adjustment=scaling_adjustment,
|
||||
min_adjustment_step=min_adjustment_step,
|
||||
cooldown=cooldown)
|
||||
|
||||
try:
|
||||
connection.create_scaling_policy(sp)
|
||||
policy = connection.get_all_policies(policy_names=[sp_name])[0]
|
||||
module.exit_json(changed=True, name=policy.name, arn=policy.policy_arn, as_name=policy.as_name, scaling_adjustment=policy.scaling_adjustment, cooldown=policy.cooldown, adjustment_type=policy.adjustment_type, min_adjustment_step=policy.min_adjustment_step)
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
else:
|
||||
policy = scalingPolicies[0]
|
||||
changed = False
|
||||
|
||||
# min_adjustment_step attribute is only relevant if the adjustment_type
|
||||
# is set to percentage change in capacity, so it is a special case
|
||||
if getattr(policy, 'adjustment_type') == 'PercentChangeInCapacity':
|
||||
if getattr(policy, 'min_adjustment_step') != module.params.get('min_adjustment_step'):
|
||||
changed = True
|
||||
|
||||
# set the min adjustment step incase the user decided to change their
|
||||
# adjustment type to percentage
|
||||
setattr(policy, 'min_adjustment_step', module.params.get('min_adjustment_step'))
|
||||
|
||||
# check the remaining attributes
|
||||
for attr in ('adjustment_type','scaling_adjustment','cooldown'):
|
||||
if getattr(policy, attr) != module.params.get(attr):
|
||||
changed = True
|
||||
setattr(policy, attr, module.params.get(attr))
|
||||
|
||||
try:
|
||||
if changed:
|
||||
connection.create_scaling_policy(policy)
|
||||
policy = connection.get_all_policies(policy_names=[sp_name])[0]
|
||||
module.exit_json(changed=changed, name=policy.name, arn=policy.policy_arn, as_name=policy.as_name, scaling_adjustment=policy.scaling_adjustment, cooldown=policy.cooldown, adjustment_type=policy.adjustment_type, min_adjustment_step=policy.min_adjustment_step)
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
def delete_scaling_policy(connection, module):
|
||||
sp_name = module.params.get('name')
|
||||
asg_name = module.params.get('asg_name')
|
||||
|
||||
scalingPolicies = connection.get_all_policies(as_group=asg_name,policy_names=[sp_name])
|
||||
|
||||
if scalingPolicies:
|
||||
try:
|
||||
connection.delete_policy(sp_name, asg_name)
|
||||
module.exit_json(changed=True)
|
||||
except BotoServerError, e:
|
||||
module.exit_json(changed=False, msg=str(e))
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name = dict(required=True, type='str'),
|
||||
adjustment_type = dict(type='str', choices=['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity']),
|
||||
asg_name = dict(required=True, type='str'),
|
||||
scaling_adjustment = dict(type='int'),
|
||||
min_adjustment_step = dict(type='int'),
|
||||
cooldown = dict(type='int'),
|
||||
region = dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
try:
|
||||
connection = connect_to_aws(boto.ec2.autoscale, region, **aws_connect_params)
|
||||
if not connection:
|
||||
module.fail_json(msg="failed to connect to AWS for the given region: %s" % str(region))
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg = str(e))
|
||||
|
||||
if state == 'present':
|
||||
create_scaling_policy(connection, module)
|
||||
elif state == 'absent':
|
||||
delete_scaling_policy(connection, module)
|
||||
|
||||
|
||||
main()
|
@ -1,151 +0,0 @@
|
||||
#!/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_snapshot
|
||||
short_description: creates a snapshot from an existing volume
|
||||
description:
|
||||
- creates an EC2 snapshot from an existing EBS volume
|
||||
version_added: "1.5"
|
||||
options:
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
volume_id:
|
||||
description:
|
||||
- volume from which to take the snapshot
|
||||
required: false
|
||||
description:
|
||||
description:
|
||||
- description to be applied to the snapshot
|
||||
required: false
|
||||
instance_id:
|
||||
description:
|
||||
- instance that has the required volume to snapshot mounted
|
||||
required: false
|
||||
device_name:
|
||||
description:
|
||||
- device name of a mounted volume to be snapshotted
|
||||
required: false
|
||||
snapshot_tags:
|
||||
description:
|
||||
- a hash/dictionary of tags to add to the snapshot
|
||||
required: false
|
||||
version_added: "1.6"
|
||||
|
||||
author: Will Thames
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Simple snapshot of volume using volume_id
|
||||
- local_action:
|
||||
module: ec2_snapshot
|
||||
volume_id: vol-abcdef12
|
||||
description: snapshot of /data from DB123 taken 2013/11/28 12:18:32
|
||||
|
||||
# Snapshot of volume mounted on device_name attached to instance_id
|
||||
- local_action:
|
||||
module: ec2_snapshot
|
||||
instance_id: i-12345678
|
||||
device_name: /dev/sdb1
|
||||
description: snapshot of /data from DB123 taken 2013/11/28 12:18:32
|
||||
|
||||
# Snapshot of volume with tagging
|
||||
- local_action:
|
||||
module: ec2_snapshot
|
||||
instance_id: i-12345678
|
||||
device_name: /dev/sdb1
|
||||
snapshot_tags:
|
||||
frequency: hourly
|
||||
source: /data
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
volume_id = dict(),
|
||||
description = dict(),
|
||||
instance_id = dict(),
|
||||
device_name = dict(),
|
||||
wait = dict(type='bool', default='true'),
|
||||
wait_timeout = dict(default=0),
|
||||
snapshot_tags = dict(type='dict', default=dict()),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
volume_id = module.params.get('volume_id')
|
||||
description = module.params.get('description')
|
||||
instance_id = module.params.get('instance_id')
|
||||
device_name = module.params.get('device_name')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
snapshot_tags = module.params.get('snapshot_tags')
|
||||
|
||||
if not volume_id and not instance_id or volume_id and instance_id:
|
||||
module.fail_json('One and only one of volume_id or instance_id must be specified')
|
||||
if instance_id and not device_name or device_name and not instance_id:
|
||||
module.fail_json('Instance ID and device name must both be specified')
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
if instance_id:
|
||||
try:
|
||||
volumes = ec2.get_all_volumes(filters={'attachment.instance-id': instance_id, 'attachment.device': device_name})
|
||||
if not volumes:
|
||||
module.fail_json(msg="Could not find volume with name %s attached to instance %s" % (device_name, instance_id))
|
||||
volume_id = volumes[0].id
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
try:
|
||||
snapshot = ec2.create_snapshot(volume_id, description=description)
|
||||
time_waited = 0
|
||||
if wait:
|
||||
snapshot.update()
|
||||
while snapshot.status != 'completed':
|
||||
time.sleep(3)
|
||||
snapshot.update()
|
||||
time_waited += 3
|
||||
if wait_timeout and time_waited > wait_timeout:
|
||||
module.fail_json('Timed out while creating snapshot.')
|
||||
for k, v in snapshot_tags.items():
|
||||
snapshot.add_tag(k, v)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
module.exit_json(changed=True, snapshot_id=snapshot.id, volume_id=snapshot.volume_id,
|
||||
volume_size=snapshot.volume_size, tags=snapshot.tags.copy())
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,152 +0,0 @@
|
||||
#!/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_tag
|
||||
short_description: create and remove tag(s) to ec2 resources.
|
||||
description:
|
||||
- Creates, removes and lists tags from any EC2 resource. The resource is referenced by its resource id (e.g. an instance being i-XXXXXXX). It is designed to be used with complex args (tags), see the examples. This module has a dependency on python-boto.
|
||||
version_added: "1.3"
|
||||
options:
|
||||
resource:
|
||||
description:
|
||||
- The EC2 resource id.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Whether the tags should be present or absent on the resource. Use list to interrogate the tags of an instance.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present', 'absent', 'list']
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- region in which the resource exists.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
|
||||
author: Lester Wade
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic example of adding tag(s)
|
||||
tasks:
|
||||
- name: tag a resource
|
||||
local_action: ec2_tag resource=vol-XXXXXX region=eu-west-1 state=present
|
||||
args:
|
||||
tags:
|
||||
Name: ubervol
|
||||
env: prod
|
||||
|
||||
# Playbook example of adding tag(s) to spawned instances
|
||||
tasks:
|
||||
- name: launch some instances
|
||||
local_action: ec2 keypair={{ keypair }} group={{ security_group }} instance_type={{ instance_type }} image={{ image_id }} wait=true region=eu-west-1
|
||||
register: ec2
|
||||
|
||||
- name: tag my launched instances
|
||||
local_action: ec2_tag resource={{ item.id }} region=eu-west-1 state=present
|
||||
with_items: ec2.instances
|
||||
args:
|
||||
tags:
|
||||
Name: webserver
|
||||
env: prod
|
||||
'''
|
||||
|
||||
# Note: this module needs to be made idempotent. Possible solution is to use resource tags with the volumes.
|
||||
# if state=present and it doesn't exist, create, tag and attach.
|
||||
# Check for state by looking for volume attachment with tag (and against block device mapping?).
|
||||
# Would personally like to revisit this in May when Eucalyptus also has tagging support (3.3).
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
resource = dict(required=True),
|
||||
tags = dict(),
|
||||
state = dict(default='present', choices=['present', 'absent', 'list']),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
resource = module.params.get('resource')
|
||||
tags = module.params.get('tags')
|
||||
state = module.params.get('state')
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
# We need a comparison here so that we can accurately report back changed status.
|
||||
# Need to expand the gettags return format and compare with "tags" and then tag or detag as appropriate.
|
||||
filters = {'resource-id' : resource}
|
||||
gettags = ec2.get_all_tags(filters=filters)
|
||||
|
||||
dictadd = {}
|
||||
dictremove = {}
|
||||
baddict = {}
|
||||
tagdict = {}
|
||||
for tag in gettags:
|
||||
tagdict[tag.name] = tag.value
|
||||
|
||||
if state == 'present':
|
||||
if not tags:
|
||||
module.fail_json(msg="tags argument is required when state is present")
|
||||
if set(tags.items()).issubset(set(tagdict.items())):
|
||||
module.exit_json(msg="Tags already exists in %s." %resource, changed=False)
|
||||
else:
|
||||
for (key, value) in set(tags.items()):
|
||||
if (key, value) not in set(tagdict.items()):
|
||||
dictadd[key] = value
|
||||
tagger = ec2.create_tags(resource, dictadd)
|
||||
gettags = ec2.get_all_tags(filters=filters)
|
||||
module.exit_json(msg="Tags %s created for resource %s." % (dictadd,resource), changed=True)
|
||||
|
||||
if state == 'absent':
|
||||
if not tags:
|
||||
module.fail_json(msg="tags argument is required when state is absent")
|
||||
for (key, value) in set(tags.items()):
|
||||
if (key, value) not in set(tagdict.items()):
|
||||
baddict[key] = value
|
||||
if set(baddict) == set(tags):
|
||||
module.exit_json(msg="Nothing to remove here. Move along.", changed=False)
|
||||
for (key, value) in set(tags.items()):
|
||||
if (key, value) in set(tagdict.items()):
|
||||
dictremove[key] = value
|
||||
tagger = ec2.delete_tags(resource, dictremove)
|
||||
gettags = ec2.get_all_tags(filters=filters)
|
||||
module.exit_json(msg="Tags %s removed for resource %s." % (dictremove,resource), changed=True)
|
||||
|
||||
if state == 'list':
|
||||
module.exit_json(changed=False, tags=tagdict)
|
||||
sys.exit(0)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,434 +0,0 @@
|
||||
#!/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_vol
|
||||
short_description: create and attach a volume, return volume id and device map
|
||||
description:
|
||||
- creates an EBS volume and optionally attaches it to an instance. If both an instance ID and a device name is given and the instance has a device at the device name, then no volume is created and no attachment is made. This module has a dependency on python-boto.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
instance:
|
||||
description:
|
||||
- instance ID if you wish to attach the volume.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- volume Name tag if you wish to attach an existing volume (requires instance)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
id:
|
||||
description:
|
||||
- volume id if you wish to attach an existing volume (requires instance) or remove an existing volume
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
volume_size:
|
||||
description:
|
||||
- size of volume (in GB) to create.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
iops:
|
||||
description:
|
||||
- the provisioned IOPs you want to associate with this volume (integer).
|
||||
required: false
|
||||
default: 100
|
||||
aliases: []
|
||||
version_added: "1.3"
|
||||
encrypted:
|
||||
description:
|
||||
- Enable encryption at rest for this volume.
|
||||
default: false
|
||||
version_added: "1.8"
|
||||
device_name:
|
||||
description:
|
||||
- device id to override device mapping. Assumes /dev/sdf for Linux/UNIX and /dev/xvdf for Windows.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
zone:
|
||||
description:
|
||||
- zone in which to create the volume, if unset uses the zone the instance is in (if set)
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['aws_zone', 'ec2_zone']
|
||||
snapshot:
|
||||
description:
|
||||
- snapshot ID on which to base the volume
|
||||
required: false
|
||||
default: null
|
||||
version_added: "1.5"
|
||||
validate_certs:
|
||||
description:
|
||||
- When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0.
|
||||
required: false
|
||||
default: "yes"
|
||||
choices: ["yes", "no"]
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
state:
|
||||
description:
|
||||
- whether to ensure the volume is present or absent, or to list existing volumes (The C(list) option was added in version 1.8).
|
||||
required: false
|
||||
default: present
|
||||
choices: ['absent', 'present', 'list']
|
||||
version_added: "1.6"
|
||||
author: Lester Wade
|
||||
extends_documentation_fragment: aws
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Simple attachment action
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
instance: XXXXXX
|
||||
volume_size: 5
|
||||
device_name: sdd
|
||||
|
||||
# Example using custom iops params
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
instance: XXXXXX
|
||||
volume_size: 5
|
||||
iops: 200
|
||||
device_name: sdd
|
||||
|
||||
# Example using snapshot id
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
instance: XXXXXX
|
||||
snapshot: "{{ snapshot }}"
|
||||
|
||||
# Playbook example combined with instance launch
|
||||
- local_action:
|
||||
module: ec2
|
||||
keypair: "{{ keypair }}"
|
||||
image: "{{ image }}"
|
||||
wait: yes
|
||||
count: 3
|
||||
register: ec2
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
instance: "{{ item.id }} "
|
||||
volume_size: 5
|
||||
with_items: ec2.instances
|
||||
register: ec2_vol
|
||||
|
||||
# Example: Launch an instance and then add a volue if not already present
|
||||
# * Nothing will happen if the volume is already attached.
|
||||
# * Volume must exist in the same zone.
|
||||
|
||||
- local_action:
|
||||
module: ec2
|
||||
keypair: "{{ keypair }}"
|
||||
image: "{{ image }}"
|
||||
zone: YYYYYY
|
||||
id: my_instance
|
||||
wait: yes
|
||||
count: 1
|
||||
register: ec2
|
||||
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
instance: "{{ item.id }}"
|
||||
name: my_existing_volume_Name_tag
|
||||
device_name: /dev/xvdf
|
||||
with_items: ec2.instances
|
||||
register: ec2_vol
|
||||
|
||||
# Remove a volume
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
id: vol-XXXXXXXX
|
||||
state: absent
|
||||
|
||||
# List volumes for an instance
|
||||
- local_action:
|
||||
module: ec2_vol
|
||||
instance: i-XXXXXX
|
||||
state: list
|
||||
'''
|
||||
|
||||
# Note: this module needs to be made idempotent. Possible solution is to use resource tags with the volumes.
|
||||
# if state=present and it doesn't exist, create, tag and attach.
|
||||
# Check for state by looking for volume attachment with tag (and against block device mapping?).
|
||||
# Would personally like to revisit this in May when Eucalyptus also has tagging support (3.3).
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def get_volume(module, ec2):
|
||||
name = module.params.get('name')
|
||||
id = module.params.get('id')
|
||||
zone = module.params.get('zone')
|
||||
filters = {}
|
||||
volume_ids = None
|
||||
if zone:
|
||||
filters['availability_zone'] = zone
|
||||
if name:
|
||||
filters = {'tag:Name': name}
|
||||
if id:
|
||||
volume_ids = [id]
|
||||
try:
|
||||
vols = ec2.get_all_volumes(volume_ids=volume_ids, filters=filters)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
if not vols:
|
||||
module.fail_json(msg="Could not find volume in zone (if specified): %s" % name or id)
|
||||
if len(vols) > 1:
|
||||
module.fail_json(msg="Found more than one volume in zone (if specified) with name: %s" % name)
|
||||
return vols[0]
|
||||
|
||||
def get_volumes(module, ec2):
|
||||
instance = module.params.get('instance')
|
||||
|
||||
if not instance:
|
||||
module.fail_json(msg = "Instance must be specified to get volumes")
|
||||
|
||||
try:
|
||||
vols = ec2.get_all_volumes(filters={'attachment.instance-id': instance})
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
return vols
|
||||
|
||||
def delete_volume(module, ec2):
|
||||
vol = get_volume(module, ec2)
|
||||
if not vol:
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
if vol.attachment_state() is not None:
|
||||
adata = vol.attach_data
|
||||
module.fail_json(msg="Volume %s is attached to an instance %s." % (vol.id, adata.instance_id))
|
||||
ec2.delete_volume(vol.id)
|
||||
module.exit_json(changed=True)
|
||||
|
||||
def boto_supports_volume_encryption():
|
||||
"""
|
||||
Check if Boto library supports encryption of EBS volumes (added in 2.29.0)
|
||||
|
||||
Returns:
|
||||
True if boto library has the named param as an argument on the request_spot_instances method, else False
|
||||
"""
|
||||
return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0')
|
||||
|
||||
def create_volume(module, ec2, zone):
|
||||
name = module.params.get('name')
|
||||
id = module.params.get('id')
|
||||
instance = module.params.get('instance')
|
||||
iops = module.params.get('iops')
|
||||
encrypted = module.params.get('encrypted')
|
||||
volume_size = module.params.get('volume_size')
|
||||
snapshot = module.params.get('snapshot')
|
||||
# If custom iops is defined we use volume_type "io1" rather than the default of "standard"
|
||||
if iops:
|
||||
volume_type = 'io1'
|
||||
else:
|
||||
volume_type = 'standard'
|
||||
|
||||
# If no instance supplied, try volume creation based on module parameters.
|
||||
if name or id:
|
||||
if not instance:
|
||||
module.fail_json(msg = "If name or id is specified, instance must also be specified")
|
||||
if iops or volume_size:
|
||||
module.fail_json(msg = "Parameters are not compatible: [id or name] and [iops or volume_size]")
|
||||
|
||||
volume = get_volume(module, ec2)
|
||||
if volume.attachment_state() is not None:
|
||||
adata = volume.attach_data
|
||||
if adata.instance_id != instance:
|
||||
module.fail_json(msg = "Volume %s is already attached to another instance: %s"
|
||||
% (name or id, adata.instance_id))
|
||||
else:
|
||||
module.exit_json(msg="Volume %s is already mapped on instance %s: %s" %
|
||||
(name or id, adata.instance_id, adata.device),
|
||||
volume_id=id,
|
||||
device=adata.device,
|
||||
changed=False)
|
||||
else:
|
||||
try:
|
||||
if boto_supports_volume_encryption():
|
||||
volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops, encrypted)
|
||||
else:
|
||||
volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops)
|
||||
|
||||
while volume.status != 'available':
|
||||
time.sleep(3)
|
||||
volume.update()
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
return volume
|
||||
|
||||
|
||||
def attach_volume(module, ec2, volume, instance):
|
||||
device_name = module.params.get('device_name')
|
||||
|
||||
if device_name and instance:
|
||||
try:
|
||||
attach = volume.attach(instance.id, device_name)
|
||||
while volume.attachment_state() != 'attached':
|
||||
time.sleep(3)
|
||||
volume.update()
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
# If device_name isn't set, make a choice based on best practices here:
|
||||
# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
|
||||
|
||||
# In future this needs to be more dynamic but combining block device mapping best practices
|
||||
# (bounds for devices, as above) with instance.block_device_mapping data would be tricky. For me ;)
|
||||
|
||||
# Use password data attribute to tell whether the instance is Windows or Linux
|
||||
if device_name is None and instance:
|
||||
try:
|
||||
if not ec2.get_password_data(instance.id):
|
||||
device_name = '/dev/sdf'
|
||||
attach = volume.attach(instance.id, device_name)
|
||||
while volume.attachment_state() != 'attached':
|
||||
time.sleep(3)
|
||||
volume.update()
|
||||
else:
|
||||
device_name = '/dev/xvdf'
|
||||
attach = volume.attach(instance.id, device_name)
|
||||
while volume.attachment_state() != 'attached':
|
||||
time.sleep(3)
|
||||
volume.update()
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
instance = dict(),
|
||||
id = dict(),
|
||||
name = dict(),
|
||||
volume_size = dict(),
|
||||
iops = dict(),
|
||||
encrypted = dict(),
|
||||
device_name = dict(),
|
||||
zone = dict(aliases=['availability_zone', 'aws_zone', 'ec2_zone']),
|
||||
snapshot = dict(),
|
||||
state = dict(choices=['absent', 'present', 'list'], default='present')
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
id = module.params.get('id')
|
||||
name = module.params.get('name')
|
||||
instance = module.params.get('instance')
|
||||
volume_size = module.params.get('volume_size')
|
||||
iops = module.params.get('iops')
|
||||
encrypted = module.params.get('encrypted')
|
||||
device_name = module.params.get('device_name')
|
||||
zone = module.params.get('zone')
|
||||
snapshot = module.params.get('snapshot')
|
||||
state = module.params.get('state')
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
if state == 'list':
|
||||
returned_volumes = []
|
||||
vols = get_volumes(module, ec2)
|
||||
|
||||
for v in vols:
|
||||
attachment = v.attach_data
|
||||
|
||||
returned_volumes.append({
|
||||
'create_time': v.create_time,
|
||||
'id': v.id,
|
||||
'iops': v.iops,
|
||||
'size': v.size,
|
||||
'snapshot_id': v.snapshot_id,
|
||||
'status': v.status,
|
||||
'type': v.type,
|
||||
'zone': v.zone,
|
||||
'attachment_set': {
|
||||
'attach_time': attachment.attach_time,
|
||||
'device': attachment.device,
|
||||
'status': attachment.status
|
||||
}
|
||||
})
|
||||
|
||||
module.exit_json(changed=False, volumes=returned_volumes)
|
||||
|
||||
if id and name:
|
||||
module.fail_json(msg="Both id and name cannot be specified")
|
||||
|
||||
if encrypted and not boto_supports_volume_encryption():
|
||||
module.fail_json(msg="You must use boto >= v2.29.0 to use encrypted volumes")
|
||||
|
||||
# Here we need to get the zone info for the instance. This covers situation where
|
||||
# instance is specified but zone isn't.
|
||||
# Useful for playbooks chaining instance launch with volume create + attach and where the
|
||||
# zone doesn't matter to the user.
|
||||
if instance:
|
||||
reservation = ec2.get_all_instances(instance_ids=instance)
|
||||
inst = reservation[0].instances[0]
|
||||
zone = inst.placement
|
||||
|
||||
# Check if there is a volume already mounted there.
|
||||
if device_name:
|
||||
if device_name in inst.block_device_mapping:
|
||||
module.exit_json(msg="Volume mapping for %s already exists on instance %s" % (device_name, instance),
|
||||
volume_id=inst.block_device_mapping[device_name].volume_id,
|
||||
device=device_name,
|
||||
changed=False)
|
||||
|
||||
# Delaying the checks until after the instance check allows us to get volume ids for existing volumes
|
||||
# without needing to pass an unused volume_size
|
||||
if not volume_size and not (id or name):
|
||||
module.fail_json(msg="You must specify an existing volume with id or name or a volume_size")
|
||||
|
||||
if volume_size and (id or name):
|
||||
module.fail_json(msg="Cannot specify volume_size and either one of name or id")
|
||||
|
||||
|
||||
if state == 'absent':
|
||||
delete_volume(module, ec2)
|
||||
|
||||
if state == 'present':
|
||||
volume = create_volume(module, ec2, zone)
|
||||
if instance:
|
||||
attach_volume(module, ec2, volume, inst)
|
||||
module.exit_json(volume_id=volume.id, device=device_name)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,626 +0,0 @@
|
||||
#!/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_vpc
|
||||
short_description: configure AWS virtual private clouds
|
||||
description:
|
||||
- Create or terminates AWS virtual private clouds. This module has a dependency on python-boto.
|
||||
version_added: "1.4"
|
||||
options:
|
||||
cidr_block:
|
||||
description:
|
||||
- "The cidr block representing the VPC, e.g. 10.0.0.0/16"
|
||||
required: false, unless state=present
|
||||
instance_tenancy:
|
||||
description:
|
||||
- "The supported tenancy options for instances launched into the VPC."
|
||||
required: false
|
||||
default: "default"
|
||||
choices: [ "default", "dedicated" ]
|
||||
dns_support:
|
||||
description:
|
||||
- toggles the "Enable DNS resolution" flag
|
||||
required: false
|
||||
default: "yes"
|
||||
choices: [ "yes", "no" ]
|
||||
dns_hostnames:
|
||||
description:
|
||||
- toggles the "Enable DNS hostname support for instances" flag
|
||||
required: false
|
||||
default: "yes"
|
||||
choices: [ "yes", "no" ]
|
||||
subnets:
|
||||
description:
|
||||
- 'A dictionary array of subnets to add of the form: { cidr: ..., az: ... , resource_tags: ... }. Where az is the desired availability zone of the subnet, but it is not required. Tags (i.e.: resource_tags) is also optional and use dictionary form: { "Environment":"Dev", "Tier":"Web", ...}. All VPC subnets not in this list will be removed. As of 1.8, if the subnets parameter is not specified, no existing subnets will be modified.'
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
vpc_id:
|
||||
description:
|
||||
- A VPC id to terminate when state=absent
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
resource_tags:
|
||||
description:
|
||||
- 'A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. Tags in this list are used in conjunction with CIDR block to uniquely identify a VPC in lieu of vpc_id. Therefore, if CIDR/Tag combination does not exits, a new VPC will be created. VPC tags not on this list will be ignored. Prior to 1.7, specifying a resource tag was optional.'
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.6"
|
||||
internet_gateway:
|
||||
description:
|
||||
- Toggle whether there should be an Internet gateway attached to the VPC
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
route_tables:
|
||||
description:
|
||||
- 'A dictionary array of route tables to add of the form: { subnets: [172.22.2.0/24, 172.22.3.0/24,], routes: [{ dest: 0.0.0.0/0, gw: igw},] }. Where the subnets list is those subnets the route table should be associated with, and the routes list is a list of routes to be in the table. The special keyword for the gw of igw specifies that you should the route should go through the internet gateway attached to the VPC. gw also accepts instance-ids in addition igw. This module is currently unable to affect the "main" route table due to some limitations in boto, so you must explicitly define the associated subnets or they will be attached to the main table implicitly. As of 1.8, if the route_tables parameter is not specified, no existing routes will be modified.'
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
wait:
|
||||
description:
|
||||
- wait for the VPC to be in state 'available' before returning
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Create or terminate the VPC
|
||||
required: true
|
||||
default: present
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- region in which the resource exists.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
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: None
|
||||
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: None
|
||||
aliases: ['ec2_access_key', 'access_key' ]
|
||||
validate_certs:
|
||||
description:
|
||||
- When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0.
|
||||
required: false
|
||||
default: "yes"
|
||||
choices: ["yes", "no"]
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
|
||||
requirements: [ "boto" ]
|
||||
author: Carson Gee
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Note: None of these examples set aws_access_key, aws_secret_key, or region.
|
||||
# It is assumed that their matching environment variables are set.
|
||||
|
||||
# Basic creation example:
|
||||
local_action:
|
||||
module: ec2_vpc
|
||||
state: present
|
||||
cidr_block: 172.23.0.0/16
|
||||
resource_tags: { "Environment":"Development" }
|
||||
region: us-west-2
|
||||
# Full creation example with subnets and optional availability zones.
|
||||
# The absence or presence of subnets deletes or creates them respectively.
|
||||
local_action:
|
||||
module: ec2_vpc
|
||||
state: present
|
||||
cidr_block: 172.22.0.0/16
|
||||
resource_tags: { "Environment":"Development" }
|
||||
subnets:
|
||||
- cidr: 172.22.1.0/24
|
||||
az: us-west-2c
|
||||
resource_tags: { "Environment":"Dev", "Tier" : "Web" }
|
||||
- cidr: 172.22.2.0/24
|
||||
az: us-west-2b
|
||||
resource_tags: { "Environment":"Dev", "Tier" : "App" }
|
||||
- cidr: 172.22.3.0/24
|
||||
az: us-west-2a
|
||||
resource_tags: { "Environment":"Dev", "Tier" : "DB" }
|
||||
internet_gateway: True
|
||||
route_tables:
|
||||
- subnets:
|
||||
- 172.22.2.0/24
|
||||
- 172.22.3.0/24
|
||||
routes:
|
||||
- dest: 0.0.0.0/0
|
||||
gw: igw
|
||||
- subnets:
|
||||
- 172.22.1.0/24
|
||||
routes:
|
||||
- dest: 0.0.0.0/0
|
||||
gw: igw
|
||||
region: us-west-2
|
||||
register: vpc
|
||||
|
||||
# Removal of a VPC by id
|
||||
local_action:
|
||||
module: ec2_vpc
|
||||
state: absent
|
||||
vpc_id: vpc-aaaaaaa
|
||||
region: us-west-2
|
||||
If you have added elements not managed by this module, e.g. instances, NATs, etc then
|
||||
the delete will fail until those dependencies are removed.
|
||||
'''
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto.ec2
|
||||
import boto.vpc
|
||||
from boto.exception import EC2ResponseError
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def get_vpc_info(vpc):
|
||||
"""
|
||||
Retrieves vpc information from an instance
|
||||
ID and returns it as a dictionary
|
||||
"""
|
||||
|
||||
return({
|
||||
'id': vpc.id,
|
||||
'cidr_block': vpc.cidr_block,
|
||||
'dhcp_options_id': vpc.dhcp_options_id,
|
||||
'region': vpc.region.name,
|
||||
'state': vpc.state,
|
||||
})
|
||||
|
||||
def find_vpc(module, vpc_conn, vpc_id=None, cidr=None):
|
||||
"""
|
||||
Finds a VPC that matches a specific id or cidr + tags
|
||||
|
||||
module : AnsibleModule object
|
||||
vpc_conn: authenticated VPCConnection connection object
|
||||
|
||||
Returns:
|
||||
A VPC object that matches either an ID or CIDR and one or more tag values
|
||||
"""
|
||||
|
||||
if vpc_id == None and cidr == None:
|
||||
module.fail_json(
|
||||
msg='You must specify either a vpc_id or a cidr block + list of unique tags, aborting'
|
||||
)
|
||||
|
||||
found_vpcs = []
|
||||
|
||||
resource_tags = module.params.get('resource_tags')
|
||||
|
||||
# Check for existing VPC by cidr_block or id
|
||||
if vpc_id is not None:
|
||||
found_vpcs = vpc_conn.get_all_vpcs(None, {'vpc-id': vpc_id, 'state': 'available',})
|
||||
|
||||
else:
|
||||
previous_vpcs = vpc_conn.get_all_vpcs(None, {'cidr': cidr, 'state': 'available'})
|
||||
|
||||
for vpc in previous_vpcs:
|
||||
# Get all tags for each of the found VPCs
|
||||
vpc_tags = dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': vpc.id}))
|
||||
|
||||
# If the supplied list of ID Tags match a subset of the VPC Tags, we found our VPC
|
||||
if resource_tags and set(resource_tags.items()).issubset(set(vpc_tags.items())):
|
||||
found_vpcs.append(vpc)
|
||||
|
||||
found_vpc = None
|
||||
|
||||
if len(found_vpcs) == 1:
|
||||
found_vpc = found_vpcs[0]
|
||||
|
||||
if len(found_vpcs) > 1:
|
||||
module.fail_json(msg='Found more than one vpc based on the supplied criteria, aborting')
|
||||
|
||||
return (found_vpc)
|
||||
|
||||
def create_vpc(module, vpc_conn):
|
||||
"""
|
||||
Creates a new or modifies an existing VPC.
|
||||
|
||||
module : AnsibleModule object
|
||||
vpc_conn: authenticated VPCConnection connection object
|
||||
|
||||
Returns:
|
||||
A dictionary with information
|
||||
about the VPC and subnets that were launched
|
||||
"""
|
||||
|
||||
id = module.params.get('vpc_id')
|
||||
cidr_block = module.params.get('cidr_block')
|
||||
instance_tenancy = module.params.get('instance_tenancy')
|
||||
dns_support = module.params.get('dns_support')
|
||||
dns_hostnames = module.params.get('dns_hostnames')
|
||||
subnets = module.params.get('subnets')
|
||||
internet_gateway = module.params.get('internet_gateway')
|
||||
route_tables = module.params.get('route_tables')
|
||||
vpc_spec_tags = module.params.get('resource_tags')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
changed = False
|
||||
|
||||
# Check for existing VPC by cidr_block + tags or id
|
||||
previous_vpc = find_vpc(module, vpc_conn, id, cidr_block)
|
||||
|
||||
if previous_vpc is not None:
|
||||
changed = False
|
||||
vpc = previous_vpc
|
||||
else:
|
||||
changed = True
|
||||
try:
|
||||
vpc = vpc_conn.create_vpc(cidr_block, instance_tenancy)
|
||||
# wait here until the vpc is available
|
||||
pending = True
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait and wait_timeout > time.time() and pending:
|
||||
try:
|
||||
pvpc = vpc_conn.get_all_vpcs(vpc.id)
|
||||
if hasattr(pvpc, 'state'):
|
||||
if pvpc.state == "available":
|
||||
pending = False
|
||||
elif hasattr(pvpc[0], 'state'):
|
||||
if pvpc[0].state == "available":
|
||||
pending = False
|
||||
# sometimes vpc_conn.create_vpc() will return a vpc that can't be found yet by vpc_conn.get_all_vpcs()
|
||||
# when that happens, just wait a bit longer and try again
|
||||
except boto.exception.BotoServerError, e:
|
||||
if e.error_code != 'InvalidVpcID.NotFound':
|
||||
raise
|
||||
if pending:
|
||||
time.sleep(5)
|
||||
if wait and wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = "wait for vpc availability timeout on %s" % time.asctime())
|
||||
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||
|
||||
# Done with base VPC, now change to attributes and features.
|
||||
|
||||
# Add resource tags
|
||||
vpc_tags = dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': vpc.id}))
|
||||
|
||||
if not set(vpc_spec_tags.items()).issubset(set(vpc_tags.items())):
|
||||
new_tags = {}
|
||||
|
||||
for (key, value) in set(vpc_spec_tags.items()):
|
||||
if (key, value) not in set(vpc_tags.items()):
|
||||
new_tags[key] = value
|
||||
|
||||
if new_tags:
|
||||
vpc_conn.create_tags(vpc.id, new_tags)
|
||||
|
||||
|
||||
# boto doesn't appear to have a way to determine the existing
|
||||
# value of the dns attributes, so we just set them.
|
||||
# It also must be done one at a time.
|
||||
vpc_conn.modify_vpc_attribute(vpc.id, enable_dns_support=dns_support)
|
||||
vpc_conn.modify_vpc_attribute(vpc.id, enable_dns_hostnames=dns_hostnames)
|
||||
|
||||
|
||||
# Process all subnet properties
|
||||
if subnets is not None:
|
||||
if not isinstance(subnets, list):
|
||||
module.fail_json(msg='subnets needs to be a list of cidr blocks')
|
||||
|
||||
current_subnets = vpc_conn.get_all_subnets(filters={ 'vpc_id': vpc.id })
|
||||
|
||||
# First add all new subnets
|
||||
for subnet in subnets:
|
||||
add_subnet = True
|
||||
for csn in current_subnets:
|
||||
if subnet['cidr'] == csn.cidr_block:
|
||||
add_subnet = False
|
||||
if add_subnet:
|
||||
try:
|
||||
new_subnet = vpc_conn.create_subnet(vpc.id, subnet['cidr'], subnet.get('az', None))
|
||||
new_subnet_tags = subnet.get('resource_tags', None)
|
||||
if new_subnet_tags:
|
||||
# Sometimes AWS takes its time to create a subnet and so using new subnets's id
|
||||
# to create tags results in exception.
|
||||
# boto doesn't seem to refresh 'state' of the newly created subnet, i.e.: it's always 'pending'
|
||||
# so i resorted to polling vpc_conn.get_all_subnets with the id of the newly added subnet
|
||||
while len(vpc_conn.get_all_subnets(filters={ 'subnet-id': new_subnet.id })) == 0:
|
||||
time.sleep(0.1)
|
||||
|
||||
vpc_conn.create_tags(new_subnet.id, new_subnet_tags)
|
||||
|
||||
changed = True
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(msg='Unable to create subnet {0}, error: {1}'.format(subnet['cidr'], e))
|
||||
|
||||
# Now delete all absent subnets
|
||||
for csubnet in current_subnets:
|
||||
delete_subnet = True
|
||||
for subnet in subnets:
|
||||
if csubnet.cidr_block == subnet['cidr']:
|
||||
delete_subnet = False
|
||||
if delete_subnet:
|
||||
try:
|
||||
vpc_conn.delete_subnet(csubnet.id)
|
||||
changed = True
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(msg='Unable to delete subnet {0}, error: {1}'.format(csubnet.cidr_block, e))
|
||||
|
||||
# Handle Internet gateway (create/delete igw)
|
||||
igw = None
|
||||
igws = vpc_conn.get_all_internet_gateways(filters={'attachment.vpc-id': vpc.id})
|
||||
if len(igws) > 1:
|
||||
module.fail_json(msg='EC2 returned more than one Internet Gateway for id %s, aborting' % vpc.id)
|
||||
if internet_gateway:
|
||||
if len(igws) != 1:
|
||||
try:
|
||||
igw = vpc_conn.create_internet_gateway()
|
||||
vpc_conn.attach_internet_gateway(igw.id, vpc.id)
|
||||
changed = True
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(msg='Unable to create Internet Gateway, error: {0}'.format(e))
|
||||
else:
|
||||
# Set igw variable to the current igw instance for use in route tables.
|
||||
igw = igws[0]
|
||||
else:
|
||||
if len(igws) > 0:
|
||||
try:
|
||||
vpc_conn.detach_internet_gateway(igws[0].id, vpc.id)
|
||||
vpc_conn.delete_internet_gateway(igws[0].id)
|
||||
changed = True
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(msg='Unable to delete Internet Gateway, error: {0}'.format(e))
|
||||
|
||||
# Handle route tables - this may be worth splitting into a
|
||||
# different module but should work fine here. The strategy to stay
|
||||
# indempotent is to basically build all the route tables as
|
||||
# defined, track the route table ids, and then run through the
|
||||
# remote list of route tables and delete any that we didn't
|
||||
# create. This shouldn't interrupt traffic in theory, but is the
|
||||
# only way to really work with route tables over time that I can
|
||||
# think of without using painful aws ids. Hopefully boto will add
|
||||
# the replace-route-table API to make this smoother and
|
||||
# allow control of the 'main' routing table.
|
||||
if route_tables is not None:
|
||||
if not isinstance(route_tables, list):
|
||||
module.fail_json(msg='route tables need to be a list of dictionaries')
|
||||
|
||||
# Work through each route table and update/create to match dictionary array
|
||||
all_route_tables = []
|
||||
for rt in route_tables:
|
||||
try:
|
||||
new_rt = vpc_conn.create_route_table(vpc.id)
|
||||
for route in rt['routes']:
|
||||
route_kwargs = {}
|
||||
if route['gw'] == 'igw':
|
||||
if not internet_gateway:
|
||||
module.fail_json(
|
||||
msg='You asked for an Internet Gateway ' \
|
||||
'(igw) route, but you have no Internet Gateway'
|
||||
)
|
||||
route_kwargs['gateway_id'] = igw.id
|
||||
elif route['gw'].startswith('i-'):
|
||||
route_kwargs['instance_id'] = route['gw']
|
||||
else:
|
||||
route_kwargs['gateway_id'] = route['gw']
|
||||
vpc_conn.create_route(new_rt.id, route['dest'], **route_kwargs)
|
||||
|
||||
# Associate with subnets
|
||||
for sn in rt['subnets']:
|
||||
rsn = vpc_conn.get_all_subnets(filters={'cidr': sn, 'vpc_id': vpc.id })
|
||||
if len(rsn) != 1:
|
||||
module.fail_json(
|
||||
msg='The subnet {0} to associate with route_table {1} ' \
|
||||
'does not exist, aborting'.format(sn, rt)
|
||||
)
|
||||
rsn = rsn[0]
|
||||
|
||||
# Disassociate then associate since we don't have replace
|
||||
old_rt = vpc_conn.get_all_route_tables(
|
||||
filters={'association.subnet_id': rsn.id, 'vpc_id': vpc.id}
|
||||
)
|
||||
old_rt = [ x for x in old_rt if x.id != None ]
|
||||
if len(old_rt) == 1:
|
||||
old_rt = old_rt[0]
|
||||
association_id = None
|
||||
for a in old_rt.associations:
|
||||
if a.subnet_id == rsn.id:
|
||||
association_id = a.id
|
||||
vpc_conn.disassociate_route_table(association_id)
|
||||
|
||||
vpc_conn.associate_route_table(new_rt.id, rsn.id)
|
||||
|
||||
all_route_tables.append(new_rt)
|
||||
changed = True
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(
|
||||
msg='Unable to create and associate route table {0}, error: ' \
|
||||
'{1}'.format(rt, e)
|
||||
)
|
||||
|
||||
# Now that we are good to go on our new route tables, delete the
|
||||
# old ones except the 'main' route table as boto can't set the main
|
||||
# table yet.
|
||||
all_rts = vpc_conn.get_all_route_tables(filters={'vpc-id': vpc.id})
|
||||
for rt in all_rts:
|
||||
if rt.id is None:
|
||||
continue
|
||||
delete_rt = True
|
||||
for newrt in all_route_tables:
|
||||
if newrt.id == rt.id:
|
||||
delete_rt = False
|
||||
break
|
||||
if delete_rt:
|
||||
rta = rt.associations
|
||||
is_main = False
|
||||
for a in rta:
|
||||
if a.main:
|
||||
is_main = True
|
||||
break
|
||||
try:
|
||||
if not is_main:
|
||||
vpc_conn.delete_route_table(rt.id)
|
||||
changed = True
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(msg='Unable to delete old route table {0}, error: {1}'.format(rt.id, e))
|
||||
|
||||
vpc_dict = get_vpc_info(vpc)
|
||||
created_vpc_id = vpc.id
|
||||
returned_subnets = []
|
||||
current_subnets = vpc_conn.get_all_subnets(filters={ 'vpc_id': vpc.id })
|
||||
|
||||
for sn in current_subnets:
|
||||
returned_subnets.append({
|
||||
'resource_tags': dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': sn.id})),
|
||||
'cidr': sn.cidr_block,
|
||||
'az': sn.availability_zone,
|
||||
'id': sn.id,
|
||||
})
|
||||
|
||||
return (vpc_dict, created_vpc_id, returned_subnets, changed)
|
||||
|
||||
def terminate_vpc(module, vpc_conn, vpc_id=None, cidr=None):
|
||||
"""
|
||||
Terminates a VPC
|
||||
|
||||
module: Ansible module object
|
||||
vpc_conn: authenticated VPCConnection connection object
|
||||
vpc_id: a vpc id to terminate
|
||||
cidr: The cidr block of the VPC - can be used in lieu of an ID
|
||||
|
||||
Returns a dictionary of VPC information
|
||||
about the VPC terminated.
|
||||
|
||||
If the VPC to be terminated is available
|
||||
"changed" will be set to True.
|
||||
|
||||
"""
|
||||
vpc_dict = {}
|
||||
terminated_vpc_id = ''
|
||||
changed = False
|
||||
|
||||
vpc = find_vpc(module, vpc_conn, vpc_id, cidr)
|
||||
|
||||
if vpc is not None:
|
||||
if vpc.state == 'available':
|
||||
terminated_vpc_id=vpc.id
|
||||
vpc_dict=get_vpc_info(vpc)
|
||||
try:
|
||||
subnets = vpc_conn.get_all_subnets(filters={'vpc_id': vpc.id})
|
||||
for sn in subnets:
|
||||
vpc_conn.delete_subnet(sn.id)
|
||||
|
||||
igws = vpc_conn.get_all_internet_gateways(
|
||||
filters={'attachment.vpc-id': vpc.id}
|
||||
)
|
||||
for igw in igws:
|
||||
vpc_conn.detach_internet_gateway(igw.id, vpc.id)
|
||||
vpc_conn.delete_internet_gateway(igw.id)
|
||||
|
||||
rts = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc.id})
|
||||
for rt in rts:
|
||||
rta = rt.associations
|
||||
is_main = False
|
||||
for a in rta:
|
||||
if a.main:
|
||||
is_main = True
|
||||
if not is_main:
|
||||
vpc_conn.delete_route_table(rt.id)
|
||||
|
||||
vpc_conn.delete_vpc(vpc.id)
|
||||
except EC2ResponseError, e:
|
||||
module.fail_json(
|
||||
msg='Unable to delete VPC {0}, error: {1}'.format(vpc.id, e)
|
||||
)
|
||||
changed = True
|
||||
|
||||
return (changed, vpc_dict, terminated_vpc_id)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
cidr_block = dict(),
|
||||
instance_tenancy = dict(choices=['default', 'dedicated'], default='default'),
|
||||
wait = dict(type='bool', default=False),
|
||||
wait_timeout = dict(default=300),
|
||||
dns_support = dict(type='bool', default=True),
|
||||
dns_hostnames = dict(type='bool', default=True),
|
||||
subnets = dict(type='list'),
|
||||
vpc_id = dict(),
|
||||
internet_gateway = dict(type='bool', default=False),
|
||||
resource_tags = dict(type='dict', required=True),
|
||||
route_tables = dict(type='list'),
|
||||
state = dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
# If we have a region specified, connect to its endpoint.
|
||||
if region:
|
||||
try:
|
||||
vpc_conn = boto.vpc.connect_to_region(
|
||||
region,
|
||||
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))
|
||||
else:
|
||||
module.fail_json(msg="region must be specified")
|
||||
|
||||
if module.params.get('state') == 'absent':
|
||||
vpc_id = module.params.get('vpc_id')
|
||||
cidr = module.params.get('cidr_block')
|
||||
(changed, vpc_dict, new_vpc_id) = terminate_vpc(module, vpc_conn, vpc_id, cidr)
|
||||
subnets_changed = None
|
||||
elif module.params.get('state') == 'present':
|
||||
# Changed is always set to true when provisioning a new VPC
|
||||
(vpc_dict, new_vpc_id, subnets_changed, changed) = create_vpc(module, vpc_conn)
|
||||
|
||||
module.exit_json(changed=changed, vpc_id=new_vpc_id, vpc=vpc_dict, subnets=subnets_changed)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,547 +0,0 @@
|
||||
#!/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
|
||||
short_description: Manage cache clusters in Amazon Elasticache.
|
||||
description:
|
||||
- Manage cache clusters in Amazon Elasticache.
|
||||
- Returns information about the specified cache cluster.
|
||||
version_added: "1.4"
|
||||
requirements: [ "boto" ]
|
||||
author: Jim Dalton
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- C(absent) or C(present) are idempotent actions that will create or destroy a cache cluster as needed. C(rebooted) will reboot the cluster, resulting in a momentary outage.
|
||||
choices: ['present', 'absent', 'rebooted']
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- The cache cluster identifier
|
||||
required: true
|
||||
engine:
|
||||
description:
|
||||
- Name of the cache engine to be used (memcached or redis)
|
||||
required: false
|
||||
default: memcached
|
||||
cache_engine_version:
|
||||
description:
|
||||
- The version number of the cache engine
|
||||
required: false
|
||||
default: 1.4.14
|
||||
node_type:
|
||||
description:
|
||||
- The compute and memory capacity of the nodes in the cache cluster
|
||||
required: false
|
||||
default: cache.m1.small
|
||||
num_nodes:
|
||||
description:
|
||||
- The initial number of cache nodes that the cache cluster will have
|
||||
required: false
|
||||
cache_port:
|
||||
description:
|
||||
- The port number on which each of the cache nodes will accept connections
|
||||
required: false
|
||||
default: 11211
|
||||
security_group_ids:
|
||||
description:
|
||||
- A list of vpc security group names to associate with this cache cluster. Only use if inside a vpc
|
||||
required: false
|
||||
default: ['default']
|
||||
version_added: "1.6"
|
||||
cache_security_groups:
|
||||
description:
|
||||
- A list of cache security group names to associate with this cache cluster
|
||||
required: false
|
||||
default: ['default']
|
||||
zone:
|
||||
description:
|
||||
- The EC2 Availability Zone in which the cache cluster will be created
|
||||
required: false
|
||||
default: None
|
||||
wait:
|
||||
description:
|
||||
- Wait for cache cluster result before returning
|
||||
required: false
|
||||
default: yes
|
||||
choices: [ "yes", "no" ]
|
||||
hard_modify:
|
||||
description:
|
||||
- Whether to destroy and recreate an existing cache cluster if necessary in order to modify its state
|
||||
required: false
|
||||
default: no
|
||||
choices: [ "yes", "no" ]
|
||||
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: None
|
||||
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: None
|
||||
aliases: ['ec2_access_key', 'access_key']
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: false
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Note: None of these examples set aws_access_key, aws_secret_key, or region.
|
||||
# It is assumed that their matching environment variables are set.
|
||||
|
||||
# Basic example
|
||||
- local_action:
|
||||
module: elasticache
|
||||
name: "test-please-delete"
|
||||
state: present
|
||||
engine: memcached
|
||||
cache_engine_version: 1.4.14
|
||||
node_type: cache.m1.small
|
||||
num_nodes: 1
|
||||
cache_port: 11211
|
||||
cache_security_groups:
|
||||
- default
|
||||
zone: us-east-1d
|
||||
|
||||
|
||||
# Ensure cache cluster is gone
|
||||
- local_action:
|
||||
module: elasticache
|
||||
name: "test-please-delete"
|
||||
state: absent
|
||||
|
||||
# Reboot cache cluster
|
||||
- local_action:
|
||||
module: elasticache
|
||||
name: "test-please-delete"
|
||||
state: rebooted
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto
|
||||
from boto.elasticache.layer1 import ElastiCacheConnection
|
||||
from boto.regioninfo import RegionInfo
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ElastiCacheManager(object):
|
||||
"""Handles elasticache creation and destruction"""
|
||||
|
||||
EXIST_STATUSES = ['available', 'creating', 'rebooting', 'modifying']
|
||||
|
||||
def __init__(self, module, name, engine, cache_engine_version, node_type,
|
||||
num_nodes, cache_port, cache_security_groups, security_group_ids, zone, wait,
|
||||
hard_modify, aws_access_key, aws_secret_key, region):
|
||||
self.module = module
|
||||
self.name = name
|
||||
self.engine = engine
|
||||
self.cache_engine_version = cache_engine_version
|
||||
self.node_type = node_type
|
||||
self.num_nodes = num_nodes
|
||||
self.cache_port = cache_port
|
||||
self.cache_security_groups = cache_security_groups
|
||||
self.security_group_ids = security_group_ids
|
||||
self.zone = zone
|
||||
self.wait = wait
|
||||
self.hard_modify = hard_modify
|
||||
|
||||
self.aws_access_key = aws_access_key
|
||||
self.aws_secret_key = aws_secret_key
|
||||
self.region = region
|
||||
|
||||
self.changed = False
|
||||
self.data = None
|
||||
self.status = 'gone'
|
||||
self.conn = self._get_elasticache_connection()
|
||||
self._refresh_data()
|
||||
|
||||
def ensure_present(self):
|
||||
"""Ensure cache cluster exists or create it if not"""
|
||||
if self.exists():
|
||||
self.sync()
|
||||
else:
|
||||
self.create()
|
||||
|
||||
def ensure_absent(self):
|
||||
"""Ensure cache cluster is gone or delete it if not"""
|
||||
self.delete()
|
||||
|
||||
def ensure_rebooted(self):
|
||||
"""Ensure cache cluster is gone or delete it if not"""
|
||||
self.reboot()
|
||||
|
||||
def exists(self):
|
||||
"""Check if cache cluster exists"""
|
||||
return self.status in self.EXIST_STATUSES
|
||||
|
||||
def create(self):
|
||||
"""Create an ElastiCache cluster"""
|
||||
if self.status == 'available':
|
||||
return
|
||||
if self.status in ['creating', 'rebooting', 'modifying']:
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
return
|
||||
if self.status == 'deleting':
|
||||
if self.wait:
|
||||
self._wait_for_status('gone')
|
||||
else:
|
||||
msg = "'%s' is currently deleting. Cannot create."
|
||||
self.module.fail_json(msg=msg % self.name)
|
||||
|
||||
try:
|
||||
response = self.conn.create_cache_cluster(cache_cluster_id=self.name,
|
||||
num_cache_nodes=self.num_nodes,
|
||||
cache_node_type=self.node_type,
|
||||
engine=self.engine,
|
||||
engine_version=self.cache_engine_version,
|
||||
cache_security_group_names=self.cache_security_groups,
|
||||
security_group_ids=self.security_group_ids,
|
||||
preferred_availability_zone=self.zone,
|
||||
port=self.cache_port)
|
||||
except boto.exception.BotoServerError, e:
|
||||
self.module.fail_json(msg=e.message)
|
||||
cache_cluster_data = response['CreateCacheClusterResponse']['CreateCacheClusterResult']['CacheCluster']
|
||||
self._refresh_data(cache_cluster_data)
|
||||
|
||||
self.changed = True
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
return True
|
||||
|
||||
def delete(self):
|
||||
"""Destroy an ElastiCache cluster"""
|
||||
if self.status == 'gone':
|
||||
return
|
||||
if self.status == 'deleting':
|
||||
if self.wait:
|
||||
self._wait_for_status('gone')
|
||||
return
|
||||
if self.status in ['creating', 'rebooting', 'modifying']:
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
else:
|
||||
msg = "'%s' is currently %s. Cannot delete."
|
||||
self.module.fail_json(msg=msg % (self.name, self.status))
|
||||
|
||||
try:
|
||||
response = self.conn.delete_cache_cluster(cache_cluster_id=self.name)
|
||||
except boto.exception.BotoServerError, e:
|
||||
self.module.fail_json(msg=e.message)
|
||||
cache_cluster_data = response['DeleteCacheClusterResponse']['DeleteCacheClusterResult']['CacheCluster']
|
||||
self._refresh_data(cache_cluster_data)
|
||||
|
||||
self.changed = True
|
||||
if self.wait:
|
||||
self._wait_for_status('gone')
|
||||
|
||||
def sync(self):
|
||||
"""Sync settings to cluster if required"""
|
||||
if not self.exists():
|
||||
msg = "'%s' is %s. Cannot sync."
|
||||
self.module.fail_json(msg=msg % (self.name, self.status))
|
||||
|
||||
if self.status in ['creating', 'rebooting', 'modifying']:
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
else:
|
||||
# Cluster can only be synced if available. If we can't wait
|
||||
# for this, then just be done.
|
||||
return
|
||||
|
||||
if self._requires_destroy_and_create():
|
||||
if not self.hard_modify:
|
||||
msg = "'%s' requires destructive modification. 'hard_modify' must be set to true to proceed."
|
||||
self.module.fail_json(msg=msg % self.name)
|
||||
if not self.wait:
|
||||
msg = "'%s' requires destructive modification. 'wait' must be set to true."
|
||||
self.module.fail_json(msg=msg % self.name)
|
||||
self.delete()
|
||||
self.create()
|
||||
return
|
||||
|
||||
if self._requires_modification():
|
||||
self.modify()
|
||||
|
||||
def modify(self):
|
||||
"""Modify the cache cluster. Note it's only possible to modify a few select options."""
|
||||
nodes_to_remove = self._get_nodes_to_remove()
|
||||
try:
|
||||
response = self.conn.modify_cache_cluster(cache_cluster_id=self.name,
|
||||
num_cache_nodes=self.num_nodes,
|
||||
cache_node_ids_to_remove=nodes_to_remove,
|
||||
cache_security_group_names=self.cache_security_groups,
|
||||
security_group_ids=self.security_group_ids,
|
||||
apply_immediately=True,
|
||||
engine_version=self.cache_engine_version)
|
||||
except boto.exception.BotoServerError, e:
|
||||
self.module.fail_json(msg=e.message)
|
||||
|
||||
cache_cluster_data = response['ModifyCacheClusterResponse']['ModifyCacheClusterResult']['CacheCluster']
|
||||
self._refresh_data(cache_cluster_data)
|
||||
|
||||
self.changed = True
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
|
||||
def reboot(self):
|
||||
"""Reboot the cache cluster"""
|
||||
if not self.exists():
|
||||
msg = "'%s' is %s. Cannot reboot."
|
||||
self.module.fail_json(msg=msg % (self.name, self.status))
|
||||
if self.status == 'rebooting':
|
||||
return
|
||||
if self.status in ['creating', 'modifying']:
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
else:
|
||||
msg = "'%s' is currently %s. Cannot reboot."
|
||||
self.module.fail_json(msg=msg % (self.name, self.status))
|
||||
|
||||
# Collect ALL nodes for reboot
|
||||
cache_node_ids = [cn['CacheNodeId'] for cn in self.data['CacheNodes']]
|
||||
try:
|
||||
response = self.conn.reboot_cache_cluster(cache_cluster_id=self.name,
|
||||
cache_node_ids_to_reboot=cache_node_ids)
|
||||
except boto.exception.BotoServerError, e:
|
||||
self.module.fail_json(msg=e.message)
|
||||
|
||||
cache_cluster_data = response['RebootCacheClusterResponse']['RebootCacheClusterResult']['CacheCluster']
|
||||
self._refresh_data(cache_cluster_data)
|
||||
|
||||
self.changed = True
|
||||
if self.wait:
|
||||
self._wait_for_status('available')
|
||||
|
||||
def get_info(self):
|
||||
"""Return basic info about the cache cluster"""
|
||||
info = {
|
||||
'name': self.name,
|
||||
'status': self.status
|
||||
}
|
||||
if self.data:
|
||||
info['data'] = self.data
|
||||
return info
|
||||
|
||||
|
||||
def _wait_for_status(self, awaited_status):
|
||||
"""Wait for status to change from present status to awaited_status"""
|
||||
status_map = {
|
||||
'creating': 'available',
|
||||
'rebooting': 'available',
|
||||
'modifying': 'available',
|
||||
'deleting': 'gone'
|
||||
}
|
||||
|
||||
if status_map[self.status] != awaited_status:
|
||||
msg = "Invalid awaited status. '%s' cannot transition to '%s'"
|
||||
self.module.fail_json(msg=msg % (self.status, awaited_status))
|
||||
|
||||
if awaited_status not in set(status_map.values()):
|
||||
msg = "'%s' is not a valid awaited status."
|
||||
self.module.fail_json(msg=msg % awaited_status)
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
self._refresh_data()
|
||||
if self.status == awaited_status:
|
||||
break
|
||||
|
||||
def _requires_modification(self):
|
||||
"""Check if cluster requires (nondestructive) modification"""
|
||||
# Check modifiable data attributes
|
||||
modifiable_data = {
|
||||
'NumCacheNodes': self.num_nodes,
|
||||
'EngineVersion': self.cache_engine_version
|
||||
}
|
||||
for key, value in modifiable_data.iteritems():
|
||||
if self.data[key] != value:
|
||||
return True
|
||||
|
||||
# Check cache security groups
|
||||
cache_security_groups = []
|
||||
for sg in self.data['CacheSecurityGroups']:
|
||||
cache_security_groups.append(sg['CacheSecurityGroupName'])
|
||||
if set(cache_security_groups) - set(self.cache_security_groups):
|
||||
return True
|
||||
|
||||
# check vpc security groups
|
||||
vpc_security_groups = []
|
||||
security_groups = self.data['SecurityGroups'] or []
|
||||
for sg in security_groups:
|
||||
vpc_security_groups.append(sg['SecurityGroupId'])
|
||||
if set(vpc_security_groups) - set(self.security_group_ids):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _requires_destroy_and_create(self):
|
||||
"""
|
||||
Check whether a destroy and create is required to synchronize cluster.
|
||||
"""
|
||||
unmodifiable_data = {
|
||||
'node_type': self.data['CacheNodeType'],
|
||||
'engine': self.data['Engine'],
|
||||
'cache_port': self._get_port()
|
||||
}
|
||||
# Only check for modifications if zone is specified
|
||||
if self.zone is not None:
|
||||
unmodifiable_data['zone'] = self.data['PreferredAvailabilityZone']
|
||||
for key, value in unmodifiable_data.iteritems():
|
||||
if getattr(self, key) != value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_elasticache_connection(self):
|
||||
"""Get an elasticache connection"""
|
||||
try:
|
||||
endpoint = "elasticache.%s.amazonaws.com" % self.region
|
||||
connect_region = RegionInfo(name=self.region, endpoint=endpoint)
|
||||
return ElastiCacheConnection(aws_access_key_id=self.aws_access_key,
|
||||
aws_secret_access_key=self.aws_secret_key,
|
||||
region=connect_region)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
self.module.fail_json(msg=e.message)
|
||||
|
||||
def _get_port(self):
|
||||
"""Get the port. Where this information is retrieved from is engine dependent."""
|
||||
if self.data['Engine'] == 'memcached':
|
||||
return self.data['ConfigurationEndpoint']['Port']
|
||||
elif self.data['Engine'] == 'redis':
|
||||
# Redis only supports a single node (presently) so just use
|
||||
# the first and only
|
||||
return self.data['CacheNodes'][0]['Endpoint']['Port']
|
||||
|
||||
def _refresh_data(self, cache_cluster_data=None):
|
||||
"""Refresh data about this cache cluster"""
|
||||
if cache_cluster_data is None:
|
||||
try:
|
||||
response = self.conn.describe_cache_clusters(cache_cluster_id=self.name,
|
||||
show_cache_node_info=True)
|
||||
except boto.exception.BotoServerError:
|
||||
self.data = None
|
||||
self.status = 'gone'
|
||||
return
|
||||
cache_cluster_data = response['DescribeCacheClustersResponse']['DescribeCacheClustersResult']['CacheClusters'][0]
|
||||
self.data = cache_cluster_data
|
||||
self.status = self.data['CacheClusterStatus']
|
||||
|
||||
# The documentation for elasticache lies -- status on rebooting is set
|
||||
# to 'rebooting cache cluster nodes' instead of 'rebooting'. Fix it
|
||||
# here to make status checks etc. more sane.
|
||||
if self.status == 'rebooting cache cluster nodes':
|
||||
self.status = 'rebooting'
|
||||
|
||||
def _get_nodes_to_remove(self):
|
||||
"""If there are nodes to remove, it figures out which need to be removed"""
|
||||
num_nodes_to_remove = self.data['NumCacheNodes'] - self.num_nodes
|
||||
if num_nodes_to_remove <= 0:
|
||||
return None
|
||||
|
||||
if not self.hard_modify:
|
||||
msg = "'%s' requires removal of cache nodes. 'hard_modify' must be set to true to proceed."
|
||||
self.module.fail_json(msg=msg % self.name)
|
||||
|
||||
cache_node_ids = [cn['CacheNodeId'] for cn in self.data['CacheNodes']]
|
||||
return cache_node_ids[-num_nodes_to_remove:]
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state={'required': True, 'choices': ['present', 'absent', 'rebooted']},
|
||||
name={'required': True},
|
||||
engine={'required': False, 'default': 'memcached'},
|
||||
cache_engine_version={'required': False, 'default': '1.4.14'},
|
||||
node_type={'required': False, 'default': 'cache.m1.small'},
|
||||
num_nodes={'required': False, 'default': None, 'type': 'int'},
|
||||
cache_port={'required': False, 'default': 11211, 'type': 'int'},
|
||||
cache_security_groups={'required': False, 'default': ['default'],
|
||||
'type': 'list'},
|
||||
security_group_ids={'required': False, 'default': [],
|
||||
'type': 'list'},
|
||||
zone={'required': False, 'default': None},
|
||||
wait={'required': False, 'type' : 'bool', 'default': True},
|
||||
hard_modify={'required': False, 'type': 'bool', 'default': False}
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
engine = module.params['engine']
|
||||
cache_engine_version = module.params['cache_engine_version']
|
||||
node_type = module.params['node_type']
|
||||
num_nodes = module.params['num_nodes']
|
||||
cache_port = module.params['cache_port']
|
||||
cache_security_groups = module.params['cache_security_groups']
|
||||
security_group_ids = module.params['security_group_ids']
|
||||
zone = module.params['zone']
|
||||
wait = module.params['wait']
|
||||
hard_modify = module.params['hard_modify']
|
||||
|
||||
if state == 'present' and not num_nodes:
|
||||
module.fail_json(msg="'num_nodes' is a required parameter. Please specify num_nodes > 0")
|
||||
|
||||
if not region:
|
||||
module.fail_json(msg=str("Either region or EC2_REGION environment variable must be set."))
|
||||
|
||||
elasticache_manager = ElastiCacheManager(module, name, engine,
|
||||
cache_engine_version, node_type,
|
||||
num_nodes, cache_port,
|
||||
cache_security_groups,
|
||||
security_group_ids, zone, wait,
|
||||
hard_modify, aws_access_key,
|
||||
aws_secret_key, region)
|
||||
|
||||
if state == 'present':
|
||||
elasticache_manager.ensure_present()
|
||||
elif state == 'absent':
|
||||
elasticache_manager.ensure_absent()
|
||||
elif state == 'rebooted':
|
||||
elasticache_manager.ensure_rebooted()
|
||||
|
||||
facts_result = dict(changed=elasticache_manager.changed,
|
||||
elasticache=elasticache_manager.get_info())
|
||||
|
||||
module.exit_json(**facts_result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,420 +0,0 @@
|
||||
#!/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: gc_storage
|
||||
version_added: "1.4"
|
||||
short_description: This module manages objects/buckets in Google Cloud Storage.
|
||||
description:
|
||||
- This module allows users to manage their objects/buckets in Google Cloud Storage. It allows upload and download operations and can set some canned permissions. It also allows retrieval of URLs for objects for use in playbooks, and retrieval of string contents of objects. This module requires setting the default project in GCS prior to playbook usage. See U(https://developers.google.com/storage/docs/reference/v1/apiversion1) for information about setting the default project.
|
||||
|
||||
options:
|
||||
bucket:
|
||||
description:
|
||||
- Bucket name.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
object:
|
||||
description:
|
||||
- Keyname of the object inside the bucket. Can be also be used to create "virtual directories" (see examples).
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
src:
|
||||
description:
|
||||
- The source file path when performing a PUT operation.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
dest:
|
||||
description:
|
||||
- The destination file path when downloading an object/key with a GET operation.
|
||||
required: false
|
||||
aliases: []
|
||||
force:
|
||||
description:
|
||||
- Forces an overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations.
|
||||
required: false
|
||||
default: true
|
||||
aliases: [ 'overwrite' ]
|
||||
permission:
|
||||
description:
|
||||
- This option let's the user set the canned permissions on the object/bucket that are created. The permissions that can be set are 'private', 'public-read', 'authenticated-read'.
|
||||
required: false
|
||||
default: private
|
||||
expiration:
|
||||
description:
|
||||
- Time limit (in seconds) for the URL generated and returned by GCA when performing a mode=put or mode=get_url operation. This url is only avaialbe when public-read is the acl for the object.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
mode:
|
||||
description:
|
||||
- Switches the module behaviour between upload, download, get_url (return download url) , get_str (download object as string), create (bucket) and delete (bucket).
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'get', 'put', 'get_url', 'get_str', 'delete', 'create' ]
|
||||
gcs_secret_key:
|
||||
description:
|
||||
- GCS secret key. If not set then the value of the GCS_SECRET_KEY environment variable is used.
|
||||
required: true
|
||||
default: null
|
||||
gcs_access_key:
|
||||
description:
|
||||
- GCS access key. If not set then the value of the GCS_ACCESS_KEY environment variable is used.
|
||||
required: true
|
||||
default: null
|
||||
|
||||
requirements: [ "boto 2.9+" ]
|
||||
|
||||
author: benno@ansible.com Note. Most of the code has been taken from the S3 module.
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# upload some content
|
||||
- gc_storage: bucket=mybucket object=key.txt src=/usr/local/myfile.txt mode=put permission=public-read
|
||||
|
||||
# download some content
|
||||
- gc_storage: bucket=mybucket object=key.txt dest=/usr/local/myfile.txt mode=get
|
||||
|
||||
# Download an object as a string to use else where in your playbook
|
||||
- gc_storage: bucket=mybucket object=key.txt mode=get_str
|
||||
|
||||
# Create an empty bucket
|
||||
- gc_storage: bucket=mybucket mode=create
|
||||
|
||||
# Create a bucket with key as directory
|
||||
- gc_storage: bucket=mybucket object=/my/directory/path mode=create
|
||||
|
||||
# Delete a bucket and all contents
|
||||
- gc_storage: bucket=mybucket mode=delete
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import urlparse
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
import boto
|
||||
except ImportError:
|
||||
print "failed=True msg='boto 2.9+ required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def grant_check(module, gs, obj):
|
||||
try:
|
||||
acp = obj.get_acl()
|
||||
if module.params.get('permission') == 'public-read':
|
||||
grant = [ x for x in acp.entries.entry_list if x.scope.type == 'AllUsers']
|
||||
if not grant:
|
||||
obj.set_acl('public-read')
|
||||
module.exit_json(changed=True, result="The objects permission as been set to public-read")
|
||||
if module.params.get('permission') == 'authenticated-read':
|
||||
grant = [ x for x in acp.entries.entry_list if x.scope.type == 'AllAuthenticatedUsers']
|
||||
if not grant:
|
||||
obj.set_acl('authenticated-read')
|
||||
module.exit_json(changed=True, result="The objects permission as been set to authenticated-read")
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def key_check(module, gs, bucket, obj):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
key_check = bucket.get_key(obj)
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
if key_check:
|
||||
grant_check(module, gs, key_check)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def keysum(module, gs, bucket, obj):
|
||||
bucket = gs.lookup(bucket)
|
||||
key_check = bucket.get_key(obj)
|
||||
if not key_check:
|
||||
return None
|
||||
md5_remote = key_check.etag[1:-1]
|
||||
etag_multipart = '-' in md5_remote # Check for multipart, etag is not md5
|
||||
if etag_multipart is True:
|
||||
module.fail_json(msg="Files uploaded with multipart of gs are not supported with checksum, unable to compute checksum.")
|
||||
return md5_remote
|
||||
|
||||
def bucket_check(module, gs, bucket):
|
||||
try:
|
||||
result = gs.lookup(bucket)
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
if result:
|
||||
grant_check(module, gs, result)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create_bucket(module, gs, bucket):
|
||||
try:
|
||||
bucket = gs.create_bucket(bucket)
|
||||
bucket.set_acl(module.params.get('permission'))
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
if bucket:
|
||||
return True
|
||||
|
||||
def delete_bucket(module, gs, bucket):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
bucket_contents = bucket.list()
|
||||
for key in bucket_contents:
|
||||
bucket.delete_key(key.name)
|
||||
bucket.delete()
|
||||
return True
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def delete_key(module, gs, bucket, obj):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
bucket.delete_key(obj)
|
||||
module.exit_json(msg="Object deleted from bucket ", changed=True)
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def create_dirkey(module, gs, bucket, obj):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
key = bucket.new_key(obj)
|
||||
key.set_contents_from_string('')
|
||||
module.exit_json(msg="Virtual directory %s created in bucket %s" % (obj, bucket.name), changed=True)
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def upload_file_check(src):
|
||||
if os.path.exists(src):
|
||||
file_exists is True
|
||||
else:
|
||||
file_exists is False
|
||||
if os.path.isdir(src):
|
||||
module.fail_json(msg="Specifying a directory is not a valid source for upload.", failed=True)
|
||||
return file_exists
|
||||
|
||||
def path_check(path):
|
||||
if os.path.exists(path):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def upload_gsfile(module, gs, bucket, obj, src, expiry):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
key = bucket.new_key(obj)
|
||||
key.set_contents_from_filename(src)
|
||||
key.set_acl(module.params.get('permission'))
|
||||
url = key.generate_url(expiry)
|
||||
module.exit_json(msg="PUT operation complete", url=url, changed=True)
|
||||
except gs.provider.storage_copy_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def download_gsfile(module, gs, bucket, obj, dest):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
key = bucket.lookup(obj)
|
||||
key.get_contents_to_filename(dest)
|
||||
module.exit_json(msg="GET operation complete", changed=True)
|
||||
except gs.provider.storage_copy_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def download_gsstr(module, gs, bucket, obj):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
key = bucket.lookup(obj)
|
||||
contents = key.get_contents_as_string()
|
||||
module.exit_json(msg="GET operation complete", contents=contents, changed=True)
|
||||
except gs.provider.storage_copy_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def get_download_url(module, gs, bucket, obj, expiry):
|
||||
try:
|
||||
bucket = gs.lookup(bucket)
|
||||
key = bucket.lookup(obj)
|
||||
url = key.generate_url(expiry)
|
||||
module.exit_json(msg="Download url:", url=url, expiration=expiry, changed=True)
|
||||
except gs.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def handle_get(module, gs, bucket, obj, overwrite, dest):
|
||||
md5_remote = keysum(module, gs, bucket, obj)
|
||||
md5_local = hashlib.md5(open(dest, 'rb').read()).hexdigest()
|
||||
if md5_local == md5_remote:
|
||||
module.exit_json(changed=False)
|
||||
if md5_local != md5_remote and not overwrite:
|
||||
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.", failed=True)
|
||||
else:
|
||||
download_gsfile(module, gs, bucket, obj, dest)
|
||||
|
||||
def handle_put(module, gs, bucket, obj, overwrite, src, expiration):
|
||||
# Lets check to see if bucket exists to get ground truth.
|
||||
bucket_rc = bucket_check(module, gs, bucket)
|
||||
key_rc = key_check(module, gs, bucket, obj)
|
||||
|
||||
# Lets check key state. Does it exist and if it does, compute the etag md5sum.
|
||||
if bucket_rc and key_rc:
|
||||
md5_remote = keysum(module, gs, bucket, obj)
|
||||
md5_local = hashlib.md5(open(src, 'rb').read()).hexdigest()
|
||||
if md5_local == md5_remote:
|
||||
module.exit_json(msg="Local and remote object are identical", changed=False)
|
||||
if md5_local != md5_remote and not overwrite:
|
||||
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force upload.", failed=True)
|
||||
else:
|
||||
upload_gsfile(module, gs, bucket, obj, src, expiration)
|
||||
|
||||
if not bucket_rc:
|
||||
create_bucket(module, gs, bucket)
|
||||
upload_gsfile(module, gs, bucket, obj, src, expiration)
|
||||
|
||||
# If bucket exists but key doesn't, just upload.
|
||||
if bucket_rc and not key_rc:
|
||||
upload_gsfile(module, gs, bucket, obj, src, expiration)
|
||||
|
||||
def handle_delete(module, gs, bucket, obj):
|
||||
if bucket and not obj:
|
||||
if bucket_check(module, gs, bucket):
|
||||
module.exit_json(msg="Bucket %s and all keys have been deleted."%bucket, changed=delete_bucket(module, gs, bucket))
|
||||
else:
|
||||
module.exit_json(msg="Bucket does not exist.", changed=False)
|
||||
if bucket and obj:
|
||||
if bucket_check(module, gs, bucket):
|
||||
if key_check(module, gs, bucket, obj):
|
||||
module.exit_json(msg="Object has been deleted.", changed=delete_key(module, gs, bucket, obj))
|
||||
else:
|
||||
module.exit_json(msg="Object does not exists.", changed=False)
|
||||
else:
|
||||
module.exit_json(msg="Bucket does not exist.", changed=False)
|
||||
else:
|
||||
module.fail_json(msg="Bucket or Bucket & object parameter is required.", failed=True)
|
||||
|
||||
def handle_create(module, gs, bucket, obj):
|
||||
if bucket and not obj:
|
||||
if bucket_check(module, gs, bucket):
|
||||
module.exit_json(msg="Bucket already exists.", changed=False)
|
||||
else:
|
||||
module.exit_json(msg="Bucket created successfully", changed=create_bucket(module, gs, bucket))
|
||||
if bucket and obj:
|
||||
if bucket_check(module, gs, bucket):
|
||||
if obj.endswith('/'):
|
||||
dirobj = obj
|
||||
else:
|
||||
dirobj = obj + "/"
|
||||
if key_check(module, gs, bucket, dirobj):
|
||||
module.exit_json(msg="Bucket %s and key %s already exists."% (bucket, obj), changed=False)
|
||||
else:
|
||||
create_dirkey(module, gs, bucket, dirobj)
|
||||
else:
|
||||
create_bucket(module, gs, bucket)
|
||||
create_dirkey(module, gs, bucket, dirobj)
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
bucket = dict(required=True),
|
||||
object = dict(default=None),
|
||||
src = dict(default=None),
|
||||
dest = dict(default=None),
|
||||
expiration = dict(default=600, aliases=['expiry']),
|
||||
mode = dict(choices=['get', 'put', 'delete', 'create', 'get_url', 'get_str'], required=True),
|
||||
permission = dict(choices=['private', 'public-read', 'authenticated-read'], default='private'),
|
||||
gs_secret_key = dict(no_log=True, required=True),
|
||||
gs_access_key = dict(required=True),
|
||||
overwrite = dict(default=True, type='bool', aliases=['force']),
|
||||
),
|
||||
)
|
||||
|
||||
bucket = module.params.get('bucket')
|
||||
obj = module.params.get('object')
|
||||
src = module.params.get('src')
|
||||
dest = module.params.get('dest')
|
||||
if dest:
|
||||
dest = os.path.expanduser(dest)
|
||||
mode = module.params.get('mode')
|
||||
expiry = module.params.get('expiration')
|
||||
gs_secret_key = module.params.get('gs_secret_key')
|
||||
gs_access_key = module.params.get('gs_access_key')
|
||||
overwrite = module.params.get('overwrite')
|
||||
|
||||
if mode == 'put':
|
||||
if not src or not object:
|
||||
module.fail_json(msg="When using PUT, src, bucket, object are mandatory parameters")
|
||||
if mode == 'get':
|
||||
if not dest or not object:
|
||||
module.fail_json(msg="When using GET, dest, bucket, object are mandatory parameters")
|
||||
if obj:
|
||||
obj = os.path.expanduser(module.params['object'])
|
||||
|
||||
try:
|
||||
gs = boto.connect_gs(gs_access_key, gs_secret_key)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg = str(e))
|
||||
|
||||
if mode == 'get':
|
||||
if not bucket_check(module, gs, bucket) or not key_check(module, gs, bucket, obj):
|
||||
module.fail_json(msg="Target bucket/key cannot be found", failed=True)
|
||||
if not path_check(dest):
|
||||
download_gsfile(module, gs, bucket, obj, dest)
|
||||
else:
|
||||
handle_get(module, gs, bucket, obj, overwrite, dest)
|
||||
|
||||
if mode == 'put':
|
||||
if not path_check(src):
|
||||
module.fail_json(msg="Local object for PUT does not exist", failed=True)
|
||||
handle_put(module, gs, bucket, obj, overwrite, src, expiry)
|
||||
|
||||
# Support for deleting an object if we have both params.
|
||||
if mode == 'delete':
|
||||
handle_delete(module, gs, bucket, obj)
|
||||
|
||||
if mode == 'create':
|
||||
handle_create(module, gs, bucket, obj)
|
||||
|
||||
if mode == 'get_url':
|
||||
if bucket and obj:
|
||||
if bucket_check(module, gs, bucket) and key_check(module, gs, bucket, obj):
|
||||
get_download_url(module, gs, bucket, obj, expiry)
|
||||
else:
|
||||
module.fail_json(msg="Key/Bucket does not exist", failed=True)
|
||||
else:
|
||||
module.fail_json(msg="Bucket and Object parameters must be set", failed=True)
|
||||
|
||||
# --------------------------- Get the String contents of an Object -------------------------
|
||||
if mode == 'get_str':
|
||||
if bucket and obj:
|
||||
if bucket_check(module, gs, bucket) and key_check(module, gs, bucket, obj):
|
||||
download_gsstr(module, gs, bucket, obj)
|
||||
else:
|
||||
module.fail_json(msg="Key/Bucket does not exist", failed=True)
|
||||
else:
|
||||
module.fail_json(msg="Bucket and Object parameters must be set", failed=True)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,474 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# 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: gce
|
||||
version_added: "1.4"
|
||||
short_description: create or terminate GCE instances
|
||||
description:
|
||||
- Creates or terminates Google Compute Engine (GCE) instances. See
|
||||
U(https://cloud.google.com/products/compute-engine) for an overview.
|
||||
Full install/configuration instructions for the gce* modules can
|
||||
be found in the comments of ansible/test/gce_tests.py.
|
||||
options:
|
||||
image:
|
||||
description:
|
||||
- image string to use for the instance
|
||||
required: false
|
||||
default: "debian-7"
|
||||
aliases: []
|
||||
instance_names:
|
||||
description:
|
||||
- a comma-separated list of instance names to create or destroy
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
machine_type:
|
||||
description:
|
||||
- machine type to use for the instance, use 'n1-standard-1' by default
|
||||
required: false
|
||||
default: "n1-standard-1"
|
||||
aliases: []
|
||||
metadata:
|
||||
description:
|
||||
- a hash/dictionary of custom data for the instance; '{"key":"value",...}'
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
service_account_email:
|
||||
version_added: 1.5.1
|
||||
description:
|
||||
- service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
pem_file:
|
||||
version_added: 1.5.1
|
||||
description:
|
||||
- path to the pem file associated with the service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
project_id:
|
||||
version_added: 1.5.1
|
||||
description:
|
||||
- your GCE project ID
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- identifier when working with a single instance
|
||||
required: false
|
||||
aliases: []
|
||||
network:
|
||||
description:
|
||||
- name of the network, 'default' will be used if not specified
|
||||
required: false
|
||||
default: "default"
|
||||
aliases: []
|
||||
persistent_boot_disk:
|
||||
description:
|
||||
- if set, create the instance with a persistent boot disk
|
||||
required: false
|
||||
default: "false"
|
||||
aliases: []
|
||||
disks:
|
||||
description:
|
||||
- a list of persistent disks to attach to the instance; a string value gives the name of the disk; alternatively, a dictionary value can define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry will be the boot disk (which must be READ_WRITE).
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.7"
|
||||
state:
|
||||
description:
|
||||
- desired state of the resource
|
||||
required: false
|
||||
default: "present"
|
||||
choices: ["active", "present", "absent", "deleted"]
|
||||
aliases: []
|
||||
tags:
|
||||
description:
|
||||
- a comma-separated list of tags to associate with the instance
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
zone:
|
||||
description:
|
||||
- the GCE zone to use
|
||||
required: true
|
||||
default: "us-central1-a"
|
||||
aliases: []
|
||||
|
||||
requirements: [ "libcloud" ]
|
||||
notes:
|
||||
- Either I(name) or I(instance_names) is required.
|
||||
author: Eric Johnson <erjohnso@google.com>
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic provisioning example. Create a single Debian 7 instance in the
|
||||
# us-central1-a Zone of n1-standard-1 machine type.
|
||||
- local_action:
|
||||
module: gce
|
||||
name: test-instance
|
||||
zone: us-central1-a
|
||||
machine_type: n1-standard-1
|
||||
image: debian-7
|
||||
|
||||
# Example using defaults and with metadata to create a single 'foo' instance
|
||||
- local_action:
|
||||
module: gce
|
||||
name: foo
|
||||
metadata: '{"db":"postgres", "group":"qa", "id":500}'
|
||||
|
||||
|
||||
# Launch instances from a control node, runs some tasks on the new instances,
|
||||
# and then terminate them
|
||||
- name: Create a sandbox instance
|
||||
hosts: localhost
|
||||
vars:
|
||||
names: foo,bar
|
||||
machine_type: n1-standard-1
|
||||
image: debian-6
|
||||
zone: us-central1-a
|
||||
service_account_email: unique-email@developer.gserviceaccount.com
|
||||
pem_file: /path/to/pem_file
|
||||
project_id: project-id
|
||||
tasks:
|
||||
- name: Launch instances
|
||||
local_action: gce instance_names={{names}} machine_type={{machine_type}}
|
||||
image={{image}} zone={{zone}} service_account_email={{ service_account_email }}
|
||||
pem_file={{ pem_file }} project_id={{ project_id }}
|
||||
register: gce
|
||||
- name: Wait for SSH to come up
|
||||
local_action: wait_for host={{item.public_ip}} port=22 delay=10
|
||||
timeout=60 state=started
|
||||
with_items: {{gce.instance_data}}
|
||||
|
||||
- name: Configure instance(s)
|
||||
hosts: launched
|
||||
sudo: True
|
||||
roles:
|
||||
- my_awesome_role
|
||||
- my_awesome_tasks
|
||||
|
||||
- name: Terminate instances
|
||||
hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Terminate instances that were previously launched
|
||||
local_action:
|
||||
module: gce
|
||||
state: 'absent'
|
||||
instance_names: {{gce.instance_names}}
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
|
||||
ResourceExistsError, ResourceInUseError, ResourceNotFoundError
|
||||
_ = Provider.GCE
|
||||
except ImportError:
|
||||
print("failed=True " + \
|
||||
"msg='libcloud with GCE support (0.13.3+) required for this module'")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from ast import literal_eval
|
||||
except ImportError:
|
||||
print("failed=True " + \
|
||||
"msg='GCE module requires python's 'ast' module, python v2.6+'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_instance_info(inst):
|
||||
"""Retrieves instance information from an instance object and returns it
|
||||
as a dictionary.
|
||||
|
||||
"""
|
||||
metadata = {}
|
||||
if 'metadata' in inst.extra and 'items' in inst.extra['metadata']:
|
||||
for md in inst.extra['metadata']['items']:
|
||||
metadata[md['key']] = md['value']
|
||||
|
||||
try:
|
||||
netname = inst.extra['networkInterfaces'][0]['network'].split('/')[-1]
|
||||
except:
|
||||
netname = None
|
||||
if 'disks' in inst.extra:
|
||||
disk_names = [disk_info['source'].split('/')[-1]
|
||||
for disk_info
|
||||
in sorted(inst.extra['disks'],
|
||||
key=lambda disk_info: disk_info['index'])]
|
||||
else:
|
||||
disk_names = []
|
||||
return({
|
||||
'image': not inst.image is None and inst.image.split('/')[-1] or None,
|
||||
'disks': disk_names,
|
||||
'machine_type': inst.size,
|
||||
'metadata': metadata,
|
||||
'name': inst.name,
|
||||
'network': netname,
|
||||
'private_ip': inst.private_ips[0],
|
||||
'public_ip': inst.public_ips[0],
|
||||
'status': ('status' in inst.extra) and inst.extra['status'] or None,
|
||||
'tags': ('tags' in inst.extra) and inst.extra['tags'] or [],
|
||||
'zone': ('zone' in inst.extra) and inst.extra['zone'].name or None,
|
||||
})
|
||||
|
||||
def create_instances(module, gce, instance_names):
|
||||
"""Creates new instances. Attributes other than instance_names are picked
|
||||
up from 'module'
|
||||
|
||||
module : AnsibleModule object
|
||||
gce: authenticated GCE libcloud driver
|
||||
instance_names: python list of instance names to create
|
||||
|
||||
Returns:
|
||||
A list of dictionaries with instance information
|
||||
about the instances that were launched.
|
||||
|
||||
"""
|
||||
image = module.params.get('image')
|
||||
machine_type = module.params.get('machine_type')
|
||||
metadata = module.params.get('metadata')
|
||||
network = module.params.get('network')
|
||||
persistent_boot_disk = module.params.get('persistent_boot_disk')
|
||||
disks = module.params.get('disks')
|
||||
state = module.params.get('state')
|
||||
tags = module.params.get('tags')
|
||||
zone = module.params.get('zone')
|
||||
|
||||
new_instances = []
|
||||
changed = False
|
||||
|
||||
lc_image = gce.ex_get_image(image)
|
||||
lc_disks = []
|
||||
disk_modes = []
|
||||
for i, disk in enumerate(disks or []):
|
||||
if isinstance(disk, dict):
|
||||
lc_disks.append(gce.ex_get_volume(disk['name']))
|
||||
disk_modes.append(disk['mode'])
|
||||
else:
|
||||
lc_disks.append(gce.ex_get_volume(disk))
|
||||
# boot disk is implicitly READ_WRITE
|
||||
disk_modes.append('READ_ONLY' if i > 0 else 'READ_WRITE')
|
||||
lc_network = gce.ex_get_network(network)
|
||||
lc_machine_type = gce.ex_get_size(machine_type)
|
||||
lc_zone = gce.ex_get_zone(zone)
|
||||
|
||||
# Try to convert the user's metadata value into the format expected
|
||||
# by GCE. First try to ensure user has proper quoting of a
|
||||
# dictionary-like syntax using 'literal_eval', then convert the python
|
||||
# dict into a python list of 'key' / 'value' dicts. Should end up
|
||||
# with:
|
||||
# [ {'key': key1, 'value': value1}, {'key': key2, 'value': value2}, ...]
|
||||
if metadata:
|
||||
try:
|
||||
md = literal_eval(metadata)
|
||||
if not isinstance(md, dict):
|
||||
raise ValueError('metadata must be a dict')
|
||||
except ValueError, e:
|
||||
print("failed=True msg='bad metadata: %s'" % str(e))
|
||||
sys.exit(1)
|
||||
except SyntaxError, e:
|
||||
print("failed=True msg='bad metadata syntax'")
|
||||
sys.exit(1)
|
||||
|
||||
items = []
|
||||
for k,v in md.items():
|
||||
items.append({"key": k,"value": v})
|
||||
metadata = {'items': items}
|
||||
|
||||
# These variables all have default values but check just in case
|
||||
if not lc_image or not lc_network or not lc_machine_type or not lc_zone:
|
||||
module.fail_json(msg='Missing required create instance variable',
|
||||
changed=False)
|
||||
|
||||
for name in instance_names:
|
||||
pd = None
|
||||
if lc_disks:
|
||||
pd = lc_disks[0]
|
||||
elif persistent_boot_disk:
|
||||
try:
|
||||
pd = gce.create_volume(None, "%s" % name, image=lc_image)
|
||||
except ResourceExistsError:
|
||||
pd = gce.ex_get_volume("%s" % name, lc_zone)
|
||||
inst = None
|
||||
try:
|
||||
inst = gce.create_node(name, lc_machine_type, lc_image,
|
||||
location=lc_zone, ex_network=network, ex_tags=tags,
|
||||
ex_metadata=metadata, ex_boot_disk=pd)
|
||||
changed = True
|
||||
except ResourceExistsError:
|
||||
inst = gce.ex_get_node(name, lc_zone)
|
||||
except GoogleBaseError, e:
|
||||
module.fail_json(msg='Unexpected error attempting to create ' + \
|
||||
'instance %s, error: %s' % (name, e.value))
|
||||
|
||||
for i, lc_disk in enumerate(lc_disks):
|
||||
# Check whether the disk is already attached
|
||||
if (len(inst.extra['disks']) > i):
|
||||
attached_disk = inst.extra['disks'][i]
|
||||
if attached_disk['source'] != lc_disk.extra['selfLink']:
|
||||
module.fail_json(
|
||||
msg=("Disk at index %d does not match: requested=%s found=%s" % (
|
||||
i, lc_disk.extra['selfLink'], attached_disk['source'])))
|
||||
elif attached_disk['mode'] != disk_modes[i]:
|
||||
module.fail_json(
|
||||
msg=("Disk at index %d is in the wrong mode: requested=%s found=%s" % (
|
||||
i, disk_modes[i], attached_disk['mode'])))
|
||||
else:
|
||||
continue
|
||||
gce.attach_volume(inst, lc_disk, ex_mode=disk_modes[i])
|
||||
# Work around libcloud bug: attached volumes don't get added
|
||||
# to the instance metadata. get_instance_info() only cares about
|
||||
# source and index.
|
||||
if len(inst.extra['disks']) != i+1:
|
||||
inst.extra['disks'].append(
|
||||
{'source': lc_disk.extra['selfLink'], 'index': i})
|
||||
|
||||
if inst:
|
||||
new_instances.append(inst)
|
||||
|
||||
instance_names = []
|
||||
instance_json_data = []
|
||||
for inst in new_instances:
|
||||
d = get_instance_info(inst)
|
||||
instance_names.append(d['name'])
|
||||
instance_json_data.append(d)
|
||||
|
||||
return (changed, instance_json_data, instance_names)
|
||||
|
||||
|
||||
def terminate_instances(module, gce, instance_names, zone_name):
|
||||
"""Terminates a list of instances.
|
||||
|
||||
module: Ansible module object
|
||||
gce: authenticated GCE connection object
|
||||
instance_names: a list of instance names to terminate
|
||||
zone_name: the zone where the instances reside prior to termination
|
||||
|
||||
Returns a dictionary of instance names that were terminated.
|
||||
|
||||
"""
|
||||
changed = False
|
||||
terminated_instance_names = []
|
||||
for name in instance_names:
|
||||
inst = None
|
||||
try:
|
||||
inst = gce.ex_get_node(name, zone_name)
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
if inst:
|
||||
gce.destroy_node(inst)
|
||||
terminated_instance_names.append(inst.name)
|
||||
changed = True
|
||||
|
||||
return (changed, terminated_instance_names)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
image = dict(default='debian-7'),
|
||||
instance_names = dict(),
|
||||
machine_type = dict(default='n1-standard-1'),
|
||||
metadata = dict(),
|
||||
name = dict(),
|
||||
network = dict(default='default'),
|
||||
persistent_boot_disk = dict(type='bool', default=False),
|
||||
disks = dict(type='list'),
|
||||
state = dict(choices=['active', 'present', 'absent', 'deleted'],
|
||||
default='present'),
|
||||
tags = dict(type='list'),
|
||||
zone = dict(default='us-central1-a'),
|
||||
service_account_email = dict(),
|
||||
pem_file = dict(),
|
||||
project_id = dict(),
|
||||
)
|
||||
)
|
||||
|
||||
gce = gce_connect(module)
|
||||
|
||||
image = module.params.get('image')
|
||||
instance_names = module.params.get('instance_names')
|
||||
machine_type = module.params.get('machine_type')
|
||||
metadata = module.params.get('metadata')
|
||||
name = module.params.get('name')
|
||||
network = module.params.get('network')
|
||||
persistent_boot_disk = module.params.get('persistent_boot_disk')
|
||||
state = module.params.get('state')
|
||||
tags = module.params.get('tags')
|
||||
zone = module.params.get('zone')
|
||||
changed = False
|
||||
|
||||
inames = []
|
||||
if isinstance(instance_names, list):
|
||||
inames = instance_names
|
||||
elif isinstance(instance_names, str):
|
||||
inames = instance_names.split(',')
|
||||
if name:
|
||||
inames.append(name)
|
||||
if not inames:
|
||||
module.fail_json(msg='Must specify a "name" or "instance_names"',
|
||||
changed=False)
|
||||
if not zone:
|
||||
module.fail_json(msg='Must specify a "zone"', changed=False)
|
||||
|
||||
json_output = {'zone': zone}
|
||||
if state in ['absent', 'deleted']:
|
||||
json_output['state'] = 'absent'
|
||||
(changed, terminated_instance_names) = terminate_instances(module,
|
||||
gce, inames, zone)
|
||||
|
||||
# based on what user specified, return the same variable, although
|
||||
# value could be different if an instance could not be destroyed
|
||||
if instance_names:
|
||||
json_output['instance_names'] = terminated_instance_names
|
||||
elif name:
|
||||
json_output['name'] = name
|
||||
|
||||
elif state in ['active', 'present']:
|
||||
json_output['state'] = 'present'
|
||||
(changed, instance_data,instance_name_list) = create_instances(
|
||||
module, gce, inames)
|
||||
json_output['instance_data'] = instance_data
|
||||
if instance_names:
|
||||
json_output['instance_names'] = instance_name_list
|
||||
elif name:
|
||||
json_output['name'] = name
|
||||
|
||||
|
||||
json_output['changed'] = changed
|
||||
print json.dumps(json_output)
|
||||
sys.exit(0)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.gce import *
|
||||
|
||||
main()
|
@ -1,335 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# 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: gce_lb
|
||||
version_added: "1.5"
|
||||
short_description: create/destroy GCE load-balancer resources
|
||||
description:
|
||||
- This module can create and destroy Google Compute Engine C(loadbalancer)
|
||||
and C(httphealthcheck) resources. The primary LB resource is the
|
||||
C(load_balancer) resource and the health check parameters are all
|
||||
prefixed with I(httphealthcheck).
|
||||
The full documentation for Google Compute Engine load balancing is at
|
||||
U(https://developers.google.com/compute/docs/load-balancing/). However,
|
||||
the ansible module simplifies the configuration by following the
|
||||
libcloud model.
|
||||
Full install/configuration instructions for the gce* modules can
|
||||
be found in the comments of ansible/test/gce_tests.py.
|
||||
options:
|
||||
httphealthcheck_name:
|
||||
description:
|
||||
- the name identifier for the HTTP health check
|
||||
required: false
|
||||
default: null
|
||||
httphealthcheck_port:
|
||||
description:
|
||||
- the TCP port to use for HTTP health checking
|
||||
required: false
|
||||
default: 80
|
||||
httphealthcheck_path:
|
||||
description:
|
||||
- the url path to use for HTTP health checking
|
||||
required: false
|
||||
default: "/"
|
||||
httphealthcheck_interval:
|
||||
description:
|
||||
- the duration in seconds between each health check request
|
||||
required: false
|
||||
default: 5
|
||||
httphealthcheck_timeout:
|
||||
description:
|
||||
- the timeout in seconds before a request is considered a failed check
|
||||
required: false
|
||||
default: 5
|
||||
httphealthcheck_unhealthy_count:
|
||||
description:
|
||||
- number of consecutive failed checks before marking a node unhealthy
|
||||
required: false
|
||||
default: 2
|
||||
httphealthcheck_healthy_count:
|
||||
description:
|
||||
- number of consecutive successful checks before marking a node healthy
|
||||
required: false
|
||||
default: 2
|
||||
httphealthcheck_host:
|
||||
description:
|
||||
- host header to pass through on HTTP check requests
|
||||
required: false
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- name of the load-balancer resource
|
||||
required: false
|
||||
default: null
|
||||
protocol:
|
||||
description:
|
||||
- the protocol used for the load-balancer packet forwarding, tcp or udp
|
||||
required: false
|
||||
default: "tcp"
|
||||
choices: ['tcp', 'udp']
|
||||
region:
|
||||
description:
|
||||
- the GCE region where the load-balancer is defined
|
||||
required: false
|
||||
external_ip:
|
||||
description:
|
||||
- the external static IPv4 (or auto-assigned) address for the LB
|
||||
required: false
|
||||
default: null
|
||||
port_range:
|
||||
description:
|
||||
- the port (range) to forward, e.g. 80 or 8000-8888 defaults to all ports
|
||||
required: false
|
||||
default: null
|
||||
members:
|
||||
description:
|
||||
- a list of zone/nodename pairs, e.g ['us-central1-a/www-a', ...]
|
||||
required: false
|
||||
aliases: ['nodes']
|
||||
state:
|
||||
description:
|
||||
- desired state of the LB
|
||||
default: "present"
|
||||
choices: ["active", "present", "absent", "deleted"]
|
||||
aliases: []
|
||||
service_account_email:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
pem_file:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- path to the pem file associated with the service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
project_id:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- your GCE project ID
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
|
||||
requirements: [ "libcloud" ]
|
||||
author: Eric Johnson <erjohnso@google.com>
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Simple example of creating a new LB, adding members, and a health check
|
||||
- local_action:
|
||||
module: gce_lb
|
||||
name: testlb
|
||||
region: us-central1
|
||||
members: ["us-central1-a/www-a", "us-central1-b/www-b"]
|
||||
httphealthcheck_name: hc
|
||||
httphealthcheck_port: 80
|
||||
httphealthcheck_path: "/up"
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
try:
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.loadbalancer.types import Provider as Provider_lb
|
||||
from libcloud.loadbalancer.providers import get_driver as get_driver_lb
|
||||
from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
|
||||
ResourceExistsError, ResourceNotFoundError
|
||||
_ = Provider.GCE
|
||||
except ImportError:
|
||||
print("failed=True " + \
|
||||
"msg='libcloud with GCE support required for this module.'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
httphealthcheck_name = dict(),
|
||||
httphealthcheck_port = dict(default=80),
|
||||
httphealthcheck_path = dict(default='/'),
|
||||
httphealthcheck_interval = dict(default=5),
|
||||
httphealthcheck_timeout = dict(default=5),
|
||||
httphealthcheck_unhealthy_count = dict(default=2),
|
||||
httphealthcheck_healthy_count = dict(default=2),
|
||||
httphealthcheck_host = dict(),
|
||||
name = dict(),
|
||||
protocol = dict(default='tcp'),
|
||||
region = dict(),
|
||||
external_ip = dict(),
|
||||
port_range = dict(),
|
||||
members = dict(type='list'),
|
||||
state = dict(default='present'),
|
||||
service_account_email = dict(),
|
||||
pem_file = dict(),
|
||||
project_id = dict(),
|
||||
)
|
||||
)
|
||||
|
||||
gce = gce_connect(module)
|
||||
|
||||
httphealthcheck_name = module.params.get('httphealthcheck_name')
|
||||
httphealthcheck_port = module.params.get('httphealthcheck_port')
|
||||
httphealthcheck_path = module.params.get('httphealthcheck_path')
|
||||
httphealthcheck_interval = module.params.get('httphealthcheck_interval')
|
||||
httphealthcheck_timeout = module.params.get('httphealthcheck_timeout')
|
||||
httphealthcheck_unhealthy_count = \
|
||||
module.params.get('httphealthcheck_unhealthy_count')
|
||||
httphealthcheck_healthy_count = \
|
||||
module.params.get('httphealthcheck_healthy_count')
|
||||
httphealthcheck_host = module.params.get('httphealthcheck_host')
|
||||
name = module.params.get('name')
|
||||
protocol = module.params.get('protocol')
|
||||
region = module.params.get('region')
|
||||
external_ip = module.params.get('external_ip')
|
||||
port_range = module.params.get('port_range')
|
||||
members = module.params.get('members')
|
||||
state = module.params.get('state')
|
||||
|
||||
try:
|
||||
gcelb = get_driver_lb(Provider_lb.GCE)(gce_driver=gce)
|
||||
gcelb.connection.user_agent_append("%s/%s" % (
|
||||
USER_AGENT_PRODUCT, USER_AGENT_VERSION))
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
changed = False
|
||||
json_output = {'name': name, 'state': state}
|
||||
|
||||
if not name and not httphealthcheck_name:
|
||||
module.fail_json(msg='Nothing to do, please specify a "name" ' + \
|
||||
'or "httphealthcheck_name" parameter', changed=False)
|
||||
|
||||
if state in ['active', 'present']:
|
||||
# first, create the httphealthcheck if requested
|
||||
hc = None
|
||||
if httphealthcheck_name:
|
||||
json_output['httphealthcheck_name'] = httphealthcheck_name
|
||||
try:
|
||||
hc = gcelb.ex_create_healthcheck(httphealthcheck_name,
|
||||
host=httphealthcheck_host, path=httphealthcheck_path,
|
||||
port=httphealthcheck_port,
|
||||
interval=httphealthcheck_interval,
|
||||
timeout=httphealthcheck_timeout,
|
||||
unhealthy_threshold=httphealthcheck_unhealthy_count,
|
||||
healthy_threshold=httphealthcheck_healthy_count)
|
||||
changed = True
|
||||
except ResourceExistsError:
|
||||
hc = gce.ex_get_healthcheck(httphealthcheck_name)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
if hc is not None:
|
||||
json_output['httphealthcheck_host'] = hc.extra['host']
|
||||
json_output['httphealthcheck_path'] = hc.path
|
||||
json_output['httphealthcheck_port'] = hc.port
|
||||
json_output['httphealthcheck_interval'] = hc.interval
|
||||
json_output['httphealthcheck_timeout'] = hc.timeout
|
||||
json_output['httphealthcheck_unhealthy_count'] = \
|
||||
hc.unhealthy_threshold
|
||||
json_output['httphealthcheck_healthy_count'] = \
|
||||
hc.healthy_threshold
|
||||
|
||||
# create the forwarding rule (and target pool under the hood)
|
||||
lb = None
|
||||
if name:
|
||||
if not region:
|
||||
module.fail_json(msg='Missing required region name',
|
||||
changed=False)
|
||||
nodes = []
|
||||
output_nodes = []
|
||||
json_output['name'] = name
|
||||
# members is a python list of 'zone/inst' strings
|
||||
if members:
|
||||
for node in members:
|
||||
try:
|
||||
zone, node_name = node.split('/')
|
||||
nodes.append(gce.ex_get_node(node_name, zone))
|
||||
output_nodes.append(node)
|
||||
except:
|
||||
# skip nodes that are badly formatted or don't exist
|
||||
pass
|
||||
try:
|
||||
if hc is not None:
|
||||
lb = gcelb.create_balancer(name, port_range, protocol,
|
||||
None, nodes, ex_region=region, ex_healthchecks=[hc],
|
||||
ex_address=external_ip)
|
||||
else:
|
||||
lb = gcelb.create_balancer(name, port_range, protocol,
|
||||
None, nodes, ex_region=region, ex_address=external_ip)
|
||||
changed = True
|
||||
except ResourceExistsError:
|
||||
lb = gcelb.get_balancer(name)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
if lb is not None:
|
||||
json_output['members'] = output_nodes
|
||||
json_output['protocol'] = protocol
|
||||
json_output['region'] = region
|
||||
json_output['external_ip'] = lb.ip
|
||||
json_output['port_range'] = lb.port
|
||||
hc_names = []
|
||||
if 'healthchecks' in lb.extra:
|
||||
for hc in lb.extra['healthchecks']:
|
||||
hc_names.append(hc.name)
|
||||
json_output['httphealthchecks'] = hc_names
|
||||
|
||||
if state in ['absent', 'deleted']:
|
||||
# first, delete the load balancer (forwarding rule and target pool)
|
||||
# if specified.
|
||||
if name:
|
||||
json_output['name'] = name
|
||||
try:
|
||||
lb = gcelb.get_balancer(name)
|
||||
gcelb.destroy_balancer(lb)
|
||||
changed = True
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
# destroy the health check if specified
|
||||
if httphealthcheck_name:
|
||||
json_output['httphealthcheck_name'] = httphealthcheck_name
|
||||
try:
|
||||
hc = gce.ex_get_healthcheck(httphealthcheck_name)
|
||||
gce.ex_destroy_healthcheck(hc)
|
||||
changed = True
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
|
||||
json_output['changed'] = changed
|
||||
print json.dumps(json_output)
|
||||
sys.exit(0)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.gce import *
|
||||
|
||||
main()
|
@ -1,271 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# 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: gce_net
|
||||
version_added: "1.5"
|
||||
short_description: create/destroy GCE networks and firewall rules
|
||||
description:
|
||||
- This module can create and destroy Google Compue Engine networks and
|
||||
firewall rules U(https://developers.google.com/compute/docs/networking).
|
||||
The I(name) parameter is reserved for referencing a network while the
|
||||
I(fwname) parameter is used to reference firewall rules.
|
||||
IPv4 Address ranges must be specified using the CIDR
|
||||
U(http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) format.
|
||||
Full install/configuration instructions for the gce* modules can
|
||||
be found in the comments of ansible/test/gce_tests.py.
|
||||
options:
|
||||
allowed:
|
||||
description:
|
||||
- the protocol:ports to allow ('tcp:80' or 'tcp:80,443' or 'tcp:80-800')
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
ipv4_range:
|
||||
description:
|
||||
- the IPv4 address range in CIDR notation for the network
|
||||
required: false
|
||||
aliases: ['cidr']
|
||||
fwname:
|
||||
description:
|
||||
- name of the firewall rule
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['fwrule']
|
||||
name:
|
||||
description:
|
||||
- name of the network
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
src_range:
|
||||
description:
|
||||
- the source IPv4 address range in CIDR notation
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['src_cidr']
|
||||
src_tags:
|
||||
description:
|
||||
- the source instance tags for creating a firewall rule
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- desired state of the persistent disk
|
||||
required: false
|
||||
default: "present"
|
||||
choices: ["active", "present", "absent", "deleted"]
|
||||
aliases: []
|
||||
service_account_email:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
pem_file:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- path to the pem file associated with the service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
project_id:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- your GCE project ID
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
|
||||
requirements: [ "libcloud" ]
|
||||
author: Eric Johnson <erjohnso@google.com>
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Simple example of creating a new network
|
||||
- local_action:
|
||||
module: gce_net
|
||||
name: privatenet
|
||||
ipv4_range: '10.240.16.0/24'
|
||||
|
||||
# Simple example of creating a new firewall rule
|
||||
- local_action:
|
||||
module: gce_net
|
||||
name: privatenet
|
||||
allowed: tcp:80,8080
|
||||
src_tags: ["web", "proxy"]
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
|
||||
ResourceExistsError, ResourceNotFoundError
|
||||
_ = Provider.GCE
|
||||
except ImportError:
|
||||
print("failed=True " + \
|
||||
"msg='libcloud with GCE support required for this module.'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def format_allowed(allowed):
|
||||
"""Format the 'allowed' value so that it is GCE compatible."""
|
||||
if allowed.count(":") == 0:
|
||||
protocol = allowed
|
||||
ports = []
|
||||
elif allowed.count(":") == 1:
|
||||
protocol, ports = allowed.split(":")
|
||||
else:
|
||||
return []
|
||||
if ports.count(","):
|
||||
ports = ports.split(",")
|
||||
else:
|
||||
ports = [ports]
|
||||
return_val = {"IPProtocol": protocol}
|
||||
if ports:
|
||||
return_val["ports"] = ports
|
||||
return [return_val]
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
allowed = dict(),
|
||||
ipv4_range = dict(),
|
||||
fwname = dict(),
|
||||
name = dict(),
|
||||
src_range = dict(),
|
||||
src_tags = dict(type='list'),
|
||||
state = dict(default='present'),
|
||||
service_account_email = dict(),
|
||||
pem_file = dict(),
|
||||
project_id = dict(),
|
||||
)
|
||||
)
|
||||
|
||||
gce = gce_connect(module)
|
||||
|
||||
allowed = module.params.get('allowed')
|
||||
ipv4_range = module.params.get('ipv4_range')
|
||||
fwname = module.params.get('fwname')
|
||||
name = module.params.get('name')
|
||||
src_range = module.params.get('src_range')
|
||||
src_tags = module.params.get('src_tags')
|
||||
state = module.params.get('state')
|
||||
|
||||
changed = False
|
||||
json_output = {'state': state}
|
||||
|
||||
if state in ['active', 'present']:
|
||||
network = None
|
||||
try:
|
||||
network = gce.ex_get_network(name)
|
||||
json_output['name'] = name
|
||||
json_output['ipv4_range'] = network.cidr
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
# user wants to create a new network that doesn't yet exist
|
||||
if name and not network:
|
||||
if not ipv4_range:
|
||||
module.fail_json(msg="Missing required 'ipv4_range' parameter",
|
||||
changed=False)
|
||||
|
||||
try:
|
||||
network = gce.ex_create_network(name, ipv4_range)
|
||||
json_output['name'] = name
|
||||
json_output['ipv4_range'] = ipv4_range
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
if fwname:
|
||||
# user creating a firewall rule
|
||||
if not allowed and not src_range and not src_tags:
|
||||
if changed and network:
|
||||
module.fail_json(
|
||||
msg="Network created, but missing required " + \
|
||||
"firewall rule parameter(s)", changed=True)
|
||||
module.fail_json(
|
||||
msg="Missing required firewall rule parameter(s)",
|
||||
changed=False)
|
||||
|
||||
allowed_list = format_allowed(allowed)
|
||||
|
||||
try:
|
||||
gce.ex_create_firewall(fwname, allowed_list, network=name,
|
||||
source_ranges=src_range, source_tags=src_tags)
|
||||
changed = True
|
||||
except ResourceExistsError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
json_output['fwname'] = fwname
|
||||
json_output['allowed'] = allowed
|
||||
json_output['src_range'] = src_range
|
||||
json_output['src_tags'] = src_tags
|
||||
|
||||
if state in ['absent', 'deleted']:
|
||||
if fwname:
|
||||
json_output['fwname'] = fwname
|
||||
fw = None
|
||||
try:
|
||||
fw = gce.ex_get_firewall(fwname)
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
if fw:
|
||||
gce.ex_destroy_firewall(fw)
|
||||
changed = True
|
||||
if name:
|
||||
json_output['name'] = name
|
||||
network = None
|
||||
try:
|
||||
network = gce.ex_get_network(name)
|
||||
# json_output['d1'] = 'found network name %s' % name
|
||||
except ResourceNotFoundError:
|
||||
# json_output['d2'] = 'not found network name %s' % name
|
||||
pass
|
||||
except Exception, e:
|
||||
# json_output['d3'] = 'error with %s' % name
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
if network:
|
||||
# json_output['d4'] = 'deleting %s' % name
|
||||
gce.ex_destroy_network(network)
|
||||
# json_output['d5'] = 'deleted %s' % name
|
||||
changed = True
|
||||
|
||||
json_output['changed'] = changed
|
||||
print json.dumps(json_output)
|
||||
sys.exit(0)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.gce import *
|
||||
|
||||
main()
|
@ -1,285 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# 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: gce_pd
|
||||
version_added: "1.4"
|
||||
short_description: utilize GCE persistent disk resources
|
||||
description:
|
||||
- This module can create and destroy unformatted GCE persistent disks
|
||||
U(https://developers.google.com/compute/docs/disks#persistentdisks).
|
||||
It also supports attaching and detaching disks from running instances.
|
||||
Full install/configuration instructions for the gce* modules can
|
||||
be found in the comments of ansible/test/gce_tests.py.
|
||||
options:
|
||||
detach_only:
|
||||
description:
|
||||
- do not destroy the disk, merely detach it from an instance
|
||||
required: false
|
||||
default: "no"
|
||||
choices: ["yes", "no"]
|
||||
aliases: []
|
||||
instance_name:
|
||||
description:
|
||||
- instance name if you wish to attach or detach the disk
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
mode:
|
||||
description:
|
||||
- GCE mount mode of disk, READ_ONLY (default) or READ_WRITE
|
||||
required: false
|
||||
default: "READ_ONLY"
|
||||
choices: ["READ_WRITE", "READ_ONLY"]
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- name of the disk
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
size_gb:
|
||||
description:
|
||||
- whole integer size of disk (in GB) to create, default is 10 GB
|
||||
required: false
|
||||
default: 10
|
||||
aliases: []
|
||||
image:
|
||||
description:
|
||||
- the source image to use for the disk
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.7"
|
||||
snapshot:
|
||||
description:
|
||||
- the source snapshot to use for the disk
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.7"
|
||||
state:
|
||||
description:
|
||||
- desired state of the persistent disk
|
||||
required: false
|
||||
default: "present"
|
||||
choices: ["active", "present", "absent", "deleted"]
|
||||
aliases: []
|
||||
zone:
|
||||
description:
|
||||
- zone in which to create the disk
|
||||
required: false
|
||||
default: "us-central1-b"
|
||||
aliases: []
|
||||
service_account_email:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
pem_file:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- path to the pem file associated with the service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
project_id:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- your GCE project ID
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
|
||||
requirements: [ "libcloud" ]
|
||||
author: Eric Johnson <erjohnso@google.com>
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Simple attachment action to an existing instance
|
||||
- local_action:
|
||||
module: gce_pd
|
||||
instance_name: notlocalhost
|
||||
size_gb: 5
|
||||
name: pd
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
|
||||
ResourceExistsError, ResourceNotFoundError, ResourceInUseError
|
||||
_ = Provider.GCE
|
||||
except ImportError:
|
||||
print("failed=True " + \
|
||||
"msg='libcloud with GCE support is required for this module.'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
detach_only = dict(type='bool'),
|
||||
instance_name = dict(),
|
||||
mode = dict(default='READ_ONLY', choices=['READ_WRITE', 'READ_ONLY']),
|
||||
name = dict(required=True),
|
||||
size_gb = dict(default=10),
|
||||
image = dict(),
|
||||
snapshot = dict(),
|
||||
state = dict(default='present'),
|
||||
zone = dict(default='us-central1-b'),
|
||||
service_account_email = dict(),
|
||||
pem_file = dict(),
|
||||
project_id = dict(),
|
||||
)
|
||||
)
|
||||
|
||||
gce = gce_connect(module)
|
||||
|
||||
detach_only = module.params.get('detach_only')
|
||||
instance_name = module.params.get('instance_name')
|
||||
mode = module.params.get('mode')
|
||||
name = module.params.get('name')
|
||||
size_gb = module.params.get('size_gb')
|
||||
image = module.params.get('image')
|
||||
snapshot = module.params.get('snapshot')
|
||||
state = module.params.get('state')
|
||||
zone = module.params.get('zone')
|
||||
|
||||
if detach_only and not instance_name:
|
||||
module.fail_json(
|
||||
msg='Must specify an instance name when detaching a disk',
|
||||
changed=False)
|
||||
|
||||
disk = inst = None
|
||||
changed = is_attached = False
|
||||
|
||||
json_output = { 'name': name, 'zone': zone, 'state': state }
|
||||
if detach_only:
|
||||
json_output['detach_only'] = True
|
||||
json_output['detached_from_instance'] = instance_name
|
||||
|
||||
if instance_name:
|
||||
# user wants to attach/detach from an existing instance
|
||||
try:
|
||||
inst = gce.ex_get_node(instance_name, zone)
|
||||
# is the disk attached?
|
||||
for d in inst.extra['disks']:
|
||||
if d['deviceName'] == name:
|
||||
is_attached = True
|
||||
json_output['attached_mode'] = d['mode']
|
||||
json_output['attached_to_instance'] = inst.name
|
||||
except:
|
||||
pass
|
||||
|
||||
# find disk if it already exists
|
||||
try:
|
||||
disk = gce.ex_get_volume(name)
|
||||
json_output['size_gb'] = int(disk.size)
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
# user wants a disk to exist. If "instance_name" is supplied the user
|
||||
# also wants it attached
|
||||
if state in ['active', 'present']:
|
||||
|
||||
if not size_gb:
|
||||
module.fail_json(msg="Must supply a size_gb", changed=False)
|
||||
try:
|
||||
size_gb = int(round(float(size_gb)))
|
||||
if size_gb < 1:
|
||||
raise Exception
|
||||
except:
|
||||
module.fail_json(msg="Must supply a size_gb larger than 1 GB",
|
||||
changed=False)
|
||||
|
||||
if instance_name and inst is None:
|
||||
module.fail_json(msg='Instance %s does not exist in zone %s' % (
|
||||
instance_name, zone), changed=False)
|
||||
|
||||
if not disk:
|
||||
if image is not None and snapshot is not None:
|
||||
module.fail_json(
|
||||
msg='Cannot give both image (%s) and snapshot (%s)' % (
|
||||
image, snapshot), changed=False)
|
||||
lc_image = None
|
||||
lc_snapshot = None
|
||||
if image is not None:
|
||||
lc_image = gce.ex_get_image(image)
|
||||
elif snapshot is not None:
|
||||
lc_snapshot = gce.ex_get_snapshot(snapshot)
|
||||
try:
|
||||
disk = gce.create_volume(
|
||||
size_gb, name, location=zone, image=lc_image,
|
||||
snapshot=lc_snapshot)
|
||||
except ResourceExistsError:
|
||||
pass
|
||||
except QuotaExceededError:
|
||||
module.fail_json(msg='Requested disk size exceeds quota',
|
||||
changed=False)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
json_output['size_gb'] = size_gb
|
||||
if image is not None:
|
||||
json_output['image'] = image
|
||||
if snapshot is not None:
|
||||
json_output['snapshot'] = snapshot
|
||||
changed = True
|
||||
if inst and not is_attached:
|
||||
try:
|
||||
gce.attach_volume(inst, disk, device=name, ex_mode=mode)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
json_output['attached_to_instance'] = inst.name
|
||||
json_output['attached_mode'] = mode
|
||||
changed = True
|
||||
|
||||
# user wants to delete a disk (or perhaps just detach it).
|
||||
if state in ['absent', 'deleted'] and disk:
|
||||
|
||||
if inst and is_attached:
|
||||
try:
|
||||
gce.detach_volume(disk, ex_node=inst)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
changed = True
|
||||
if not detach_only:
|
||||
try:
|
||||
gce.destroy_volume(disk)
|
||||
except ResourceInUseError, e:
|
||||
module.fail_json(msg=str(e.value), changed=False)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
changed = True
|
||||
|
||||
json_output['changed'] = changed
|
||||
print json.dumps(json_output)
|
||||
sys.exit(0)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.gce import *
|
||||
|
||||
main()
|
@ -1,260 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: glance_image
|
||||
version_added: "1.2"
|
||||
short_description: Add/Delete images from glance
|
||||
description:
|
||||
- Add or Remove images from the glance repository.
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
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
|
||||
required: false
|
||||
default: 'yes'
|
||||
copy_from:
|
||||
description:
|
||||
- A url from where the image can be downloaded, mutually exclusive with file parameter
|
||||
required: false
|
||||
default: None
|
||||
timeout:
|
||||
description:
|
||||
- The time to wait for the image process to complete in seconds
|
||||
required: false
|
||||
default: 180
|
||||
file:
|
||||
description:
|
||||
- The path to the file which has to be uploaded, mutually exclusive with copy_from
|
||||
required: false
|
||||
default: None
|
||||
endpoint_type:
|
||||
description:
|
||||
- The name of the glance service's endpoint URL type
|
||||
choices: [publicURL, internalURL]
|
||||
required: false
|
||||
default: publicURL
|
||||
version_added: "1.7"
|
||||
requirements: ["glanceclient", "keystoneclient"]
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Upload an image from an HTTP URL
|
||||
- glance_image: login_username=admin
|
||||
login_password=passme
|
||||
login_tenant_name=admin
|
||||
name=cirros
|
||||
container_format=bare
|
||||
disk_format=qcow2
|
||||
state=present
|
||||
copy_from=http:launchpad.net/cirros/trunk/0.3.0/+download/cirros-0.3.0-x86_64-disk.img
|
||||
'''
|
||||
|
||||
import time
|
||||
try:
|
||||
import glanceclient
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
except ImportError:
|
||||
print("failed=True msg='glanceclient and keystone client are required'")
|
||||
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
client = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error authenticating to the keystone: %s " % e.message)
|
||||
return client
|
||||
|
||||
|
||||
def _get_endpoint(module, client, endpoint_type):
|
||||
try:
|
||||
endpoint = client.service_catalog.url_for(service_type='image', endpoint_type=endpoint_type)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error getting endpoint for glance: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
|
||||
def _get_glance_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint =_get_endpoint(module, _ksclient, kwargs.get('endpoint_type'))
|
||||
kwargs = {
|
||||
'token': token,
|
||||
}
|
||||
try:
|
||||
client = glanceclient.Client('1', endpoint, **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error in connecting to glance: %s" % e.message)
|
||||
return client
|
||||
|
||||
|
||||
def _glance_image_present(module, params, client):
|
||||
try:
|
||||
for image in client.images.list():
|
||||
if image.name == params['name']:
|
||||
return image.id
|
||||
return None
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error in fetching image list: %s" % e.message)
|
||||
|
||||
|
||||
def _glance_image_create(module, params, client):
|
||||
kwargs = {
|
||||
'name': params.get('name'),
|
||||
'disk_format': params.get('disk_format'),
|
||||
'container_format': params.get('container_format'),
|
||||
'owner': params.get('owner'),
|
||||
'is_public': params.get('is_public'),
|
||||
'copy_from': params.get('copy_from'),
|
||||
}
|
||||
try:
|
||||
timeout = float(params.get('timeout'))
|
||||
expire = time.time() + timeout
|
||||
image = client.images.create(**kwargs)
|
||||
if not params['copy_from']:
|
||||
image.update(data=open(params['file'], 'rb'))
|
||||
while time.time() < expire:
|
||||
image = client.images.get(image.id)
|
||||
if image.status == 'active':
|
||||
break
|
||||
time.sleep(5)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error in creating image: %s" % e.message)
|
||||
if image.status == 'active':
|
||||
module.exit_json(changed=True, result=image.status, id=image.id)
|
||||
else:
|
||||
module.fail_json(msg=" The module timed out, please check manually " + image.status)
|
||||
|
||||
|
||||
def _glance_delete_image(module, params, client):
|
||||
try:
|
||||
for image in client.images.list():
|
||||
if image.name == params['name']:
|
||||
client.images.delete(image)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error in deleting image: %s" % e.message)
|
||||
module.exit_json(changed=True, result="Deleted")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name = dict(required=True),
|
||||
disk_format = dict(default='qcow2', choices=['aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso']),
|
||||
container_format = dict(default='bare', choices=['aki', 'ari', 'bare', 'ovf']),
|
||||
owner = dict(default=None),
|
||||
min_disk = dict(default=None),
|
||||
min_ram = dict(default=None),
|
||||
is_public = dict(default=True),
|
||||
copy_from = dict(default= None),
|
||||
timeout = dict(default=180),
|
||||
file = dict(default=None),
|
||||
endpoint_type = dict(default='publicURL', choices=['publicURL', 'internalURL']),
|
||||
state = dict(default='present', choices=['absent', 'present'])
|
||||
))
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive = [['file','copy_from']],
|
||||
)
|
||||
if module.params['state'] == 'present':
|
||||
if not module.params['file'] and not module.params['copy_from']:
|
||||
module.fail_json(msg="Either file or copy_from variable should be set to create the image")
|
||||
client = _get_glance_client(module, module.params)
|
||||
id = _glance_image_present(module, module.params, client)
|
||||
if not id:
|
||||
_glance_image_create(module, module.params, client)
|
||||
module.exit_json(changed=False, id=id, result="success")
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
client = _get_glance_client(module, module.params)
|
||||
id = _glance_image_present(module, module.params, client)
|
||||
if not id:
|
||||
module.exit_json(changed=False, result="Success")
|
||||
else:
|
||||
_glance_delete_image(module, module.params, client)
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
@ -1,394 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Based on Jimmy Tang's implementation
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keystone_user
|
||||
version_added: "1.2"
|
||||
short_description: Manage OpenStack Identity (keystone) users, tenants and roles
|
||||
description:
|
||||
- Manage users,tenants, roles from OpenStack.
|
||||
options:
|
||||
login_user:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: false
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: false
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant login_user belongs to
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.3"
|
||||
token:
|
||||
description:
|
||||
- The token to be uses in case the password is not specified
|
||||
required: false
|
||||
default: None
|
||||
endpoint:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
user:
|
||||
description:
|
||||
- The name of the user that has to added/removed from OpenStack
|
||||
required: false
|
||||
default: None
|
||||
password:
|
||||
description:
|
||||
- The password to be assigned to the user
|
||||
required: false
|
||||
default: None
|
||||
tenant:
|
||||
description:
|
||||
- The tenant name that has be added/removed
|
||||
required: false
|
||||
default: None
|
||||
tenant_description:
|
||||
description:
|
||||
- A description for the tenant
|
||||
required: false
|
||||
default: None
|
||||
email:
|
||||
description:
|
||||
- An email address for the user
|
||||
required: false
|
||||
default: None
|
||||
role:
|
||||
description:
|
||||
- The name of the role to be assigned or created
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
requirements: [ python-keystoneclient ]
|
||||
author: Lorin Hochstein
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a tenant
|
||||
- keystone_user: tenant=demo tenant_description="Default Tenant"
|
||||
|
||||
# Create a user
|
||||
- keystone_user: user=john tenant=demo password=secrete
|
||||
|
||||
# Apply the admin role to the john user in the demo tenant
|
||||
- keystone_user: role=admin user=john tenant=demo
|
||||
'''
|
||||
|
||||
try:
|
||||
from keystoneclient.v2_0 import client
|
||||
except ImportError:
|
||||
keystoneclient_found = False
|
||||
else:
|
||||
keystoneclient_found = True
|
||||
|
||||
|
||||
def authenticate(endpoint, token, login_user, login_password, login_tenant_name):
|
||||
"""Return a keystone client object"""
|
||||
|
||||
if token:
|
||||
return client.Client(endpoint=endpoint, token=token)
|
||||
else:
|
||||
return client.Client(auth_url=endpoint, username=login_user,
|
||||
password=login_password, tenant_name=login_tenant_name)
|
||||
|
||||
|
||||
def tenant_exists(keystone, tenant):
|
||||
""" Return True if tenant already exists"""
|
||||
return tenant in [x.name for x in keystone.tenants.list()]
|
||||
|
||||
|
||||
def user_exists(keystone, user):
|
||||
"""" Return True if user already exists"""
|
||||
return user in [x.name for x in keystone.users.list()]
|
||||
|
||||
|
||||
def get_tenant(keystone, name):
|
||||
""" Retrieve a tenant by name"""
|
||||
tenants = [x for x in keystone.tenants.list() if x.name == name]
|
||||
count = len(tenants)
|
||||
if count == 0:
|
||||
raise KeyError("No keystone tenants with name %s" % name)
|
||||
elif count > 1:
|
||||
raise ValueError("%d tenants with name %s" % (count, name))
|
||||
else:
|
||||
return tenants[0]
|
||||
|
||||
|
||||
def get_user(keystone, name):
|
||||
""" Retrieve a user by name"""
|
||||
users = [x for x in keystone.users.list() if x.name == name]
|
||||
count = len(users)
|
||||
if count == 0:
|
||||
raise KeyError("No keystone users with name %s" % name)
|
||||
elif count > 1:
|
||||
raise ValueError("%d users with name %s" % (count, name))
|
||||
else:
|
||||
return users[0]
|
||||
|
||||
|
||||
def get_role(keystone, name):
|
||||
""" Retrieve a role by name"""
|
||||
roles = [x for x in keystone.roles.list() if x.name == name]
|
||||
count = len(roles)
|
||||
if count == 0:
|
||||
raise KeyError("No keystone roles with name %s" % name)
|
||||
elif count > 1:
|
||||
raise ValueError("%d roles with name %s" % (count, name))
|
||||
else:
|
||||
return roles[0]
|
||||
|
||||
|
||||
def get_tenant_id(keystone, name):
|
||||
return get_tenant(keystone, name).id
|
||||
|
||||
|
||||
def get_user_id(keystone, name):
|
||||
return get_user(keystone, name).id
|
||||
|
||||
|
||||
def ensure_tenant_exists(keystone, tenant_name, tenant_description,
|
||||
check_mode):
|
||||
""" Ensure that a tenant exists.
|
||||
|
||||
Return (True, id) if a new tenant was created, (False, None) if it
|
||||
already existed.
|
||||
"""
|
||||
|
||||
# Check if tenant already exists
|
||||
try:
|
||||
tenant = get_tenant(keystone, tenant_name)
|
||||
except KeyError:
|
||||
# Tenant doesn't exist yet
|
||||
pass
|
||||
else:
|
||||
if tenant.description == tenant_description:
|
||||
return (False, tenant.id)
|
||||
else:
|
||||
# We need to update the tenant description
|
||||
if check_mode:
|
||||
return (True, tenant.id)
|
||||
else:
|
||||
tenant.update(description=tenant_description)
|
||||
return (True, tenant.id)
|
||||
|
||||
# We now know we will have to create a new tenant
|
||||
if check_mode:
|
||||
return (True, None)
|
||||
|
||||
ks_tenant = keystone.tenants.create(tenant_name=tenant_name,
|
||||
description=tenant_description,
|
||||
enabled=True)
|
||||
return (True, ks_tenant.id)
|
||||
|
||||
|
||||
def ensure_tenant_absent(keystone, tenant, check_mode):
|
||||
""" Ensure that a tenant does not exist
|
||||
|
||||
Return True if the tenant was removed, False if it didn't exist
|
||||
in the first place
|
||||
"""
|
||||
if not tenant_exists(keystone, tenant):
|
||||
return False
|
||||
|
||||
# We now know we will have to delete the tenant
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
|
||||
def ensure_user_exists(keystone, user_name, password, email, tenant_name,
|
||||
check_mode):
|
||||
""" Check if user exists
|
||||
|
||||
Return (True, id) if a new user was created, (False, id) user alrady
|
||||
exists
|
||||
"""
|
||||
|
||||
# Check if tenant already exists
|
||||
try:
|
||||
user = get_user(keystone, user_name)
|
||||
except KeyError:
|
||||
# Tenant doesn't exist yet
|
||||
pass
|
||||
else:
|
||||
# User does exist, we're done
|
||||
return (False, user.id)
|
||||
|
||||
# We now know we will have to create a new user
|
||||
if check_mode:
|
||||
return (True, None)
|
||||
|
||||
tenant = get_tenant(keystone, tenant_name)
|
||||
|
||||
user = keystone.users.create(name=user_name, password=password,
|
||||
email=email, tenant_id=tenant.id)
|
||||
return (True, user.id)
|
||||
|
||||
|
||||
def ensure_role_exists(keystone, user_name, tenant_name, role_name,
|
||||
check_mode):
|
||||
""" Check if role exists
|
||||
|
||||
Return (True, id) if a new role was created or if the role was newly
|
||||
assigned to the user for the tenant. (False, id) if the role already
|
||||
exists and was already assigned to the user ofr the tenant.
|
||||
|
||||
"""
|
||||
# Check if the user has the role in the tenant
|
||||
user = get_user(keystone, user_name)
|
||||
tenant = get_tenant(keystone, tenant_name)
|
||||
roles = [x for x in keystone.roles.roles_for_user(user, tenant)
|
||||
if x.name == role_name]
|
||||
count = len(roles)
|
||||
|
||||
if count == 1:
|
||||
# If the role is in there, we are done
|
||||
role = roles[0]
|
||||
return (False, role.id)
|
||||
elif count > 1:
|
||||
# Too many roles with the same name, throw an error
|
||||
raise ValueError("%d roles with name %s" % (count, role_name))
|
||||
|
||||
# At this point, we know we will need to make changes
|
||||
if check_mode:
|
||||
return (True, None)
|
||||
|
||||
# Get the role if it exists
|
||||
try:
|
||||
role = get_role(keystone, role_name)
|
||||
except KeyError:
|
||||
# Role doesn't exist yet
|
||||
role = keystone.roles.create(role_name)
|
||||
|
||||
# Associate the role with the user in the admin
|
||||
keystone.roles.add_user_role(user, role, tenant)
|
||||
return (True, role.id)
|
||||
|
||||
|
||||
def ensure_user_absent(keystone, user, check_mode):
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
|
||||
def ensure_role_absent(keystone, uesr, tenant, role, check_mode):
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
tenant_description=dict(required=False),
|
||||
email=dict(required=False),
|
||||
role=dict(required=False),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
endpoint=dict(required=False,
|
||||
default="http://127.0.0.1:35357/v2.0"),
|
||||
token=dict(required=False),
|
||||
login_user=dict(required=False),
|
||||
login_password=dict(required=False),
|
||||
login_tenant_name=dict(required=False)
|
||||
))
|
||||
# keystone operations themselves take an endpoint, not a keystone auth_url
|
||||
del(argument_spec['auth_url'])
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[['token', 'login_user'],
|
||||
['token', 'login_password'],
|
||||
['token', 'login_tenant_name']]
|
||||
)
|
||||
|
||||
if not keystoneclient_found:
|
||||
module.fail_json(msg="the python-keystoneclient module is required")
|
||||
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
tenant = module.params['tenant']
|
||||
tenant_description = module.params['tenant_description']
|
||||
email = module.params['email']
|
||||
role = module.params['role']
|
||||
state = module.params['state']
|
||||
endpoint = module.params['endpoint']
|
||||
token = module.params['token']
|
||||
login_user = module.params['login_user']
|
||||
login_password = module.params['login_password']
|
||||
login_tenant_name = module.params['login_tenant_name']
|
||||
|
||||
keystone = authenticate(endpoint, token, login_user, login_password, login_tenant_name)
|
||||
|
||||
check_mode = module.check_mode
|
||||
|
||||
try:
|
||||
d = dispatch(keystone, user, password, tenant, tenant_description,
|
||||
email, role, state, endpoint, token, login_user,
|
||||
login_password, check_mode)
|
||||
except Exception, e:
|
||||
if check_mode:
|
||||
# If we have a failure in check mode
|
||||
module.exit_json(changed=True,
|
||||
msg="exception: %s" % e)
|
||||
else:
|
||||
module.fail_json(msg="exception: %s" % e)
|
||||
else:
|
||||
module.exit_json(**d)
|
||||
|
||||
|
||||
def dispatch(keystone, user=None, password=None, tenant=None,
|
||||
tenant_description=None, email=None, role=None,
|
||||
state="present", endpoint=None, token=None, login_user=None,
|
||||
login_password=None, check_mode=False):
|
||||
""" Dispatch to the appropriate method.
|
||||
|
||||
Returns a dict that will be passed to exit_json
|
||||
|
||||
tenant user role state
|
||||
------ ---- ---- --------
|
||||
X present ensure_tenant_exists
|
||||
X absent ensure_tenant_absent
|
||||
X X present ensure_user_exists
|
||||
X X absent ensure_user_absent
|
||||
X X X present ensure_role_exists
|
||||
X X X absent ensure_role_absent
|
||||
|
||||
|
||||
"""
|
||||
changed = False
|
||||
id = None
|
||||
if tenant and not user and not role and state == "present":
|
||||
changed, id = ensure_tenant_exists(keystone, tenant,
|
||||
tenant_description, check_mode)
|
||||
elif tenant and not user and not role and state == "absent":
|
||||
changed = ensure_tenant_absent(keystone, tenant, check_mode)
|
||||
elif tenant and user and not role and state == "present":
|
||||
changed, id = ensure_user_exists(keystone, user, password,
|
||||
email, tenant, check_mode)
|
||||
elif tenant and user and not role and state == "absent":
|
||||
changed = ensure_user_absent(keystone, user, check_mode)
|
||||
elif tenant and user and role and state == "present":
|
||||
changed, id = ensure_role_exists(keystone, user, tenant, role,
|
||||
check_mode)
|
||||
elif tenant and user and role and state == "absent":
|
||||
changed = ensure_role_absent(keystone, user, tenant, role, check_mode)
|
||||
else:
|
||||
# Should never reach here
|
||||
raise ValueError("Code should never reach here")
|
||||
|
||||
return dict(changed=changed, id=id)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,493 +0,0 @@
|
||||
#!/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: linode
|
||||
short_description: create / delete / stop / restart an instance in Linode Public Cloud
|
||||
description:
|
||||
- creates / deletes a Linode Public Cloud instance and optionally waits for it to be 'running'.
|
||||
version_added: "1.3"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'active', 'started', 'absent', 'deleted', 'stopped', 'restarted']
|
||||
default: present
|
||||
api_key:
|
||||
description:
|
||||
- Linode API key
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- Name to give the instance (alphanumeric, dashes, underscore)
|
||||
- To keep sanity on the Linode Web Console, name is prepended with LinodeID_
|
||||
default: null
|
||||
type: string
|
||||
linode_id:
|
||||
description:
|
||||
- Unique ID of a linode server
|
||||
aliases: lid
|
||||
default: null
|
||||
type: integer
|
||||
plan:
|
||||
description:
|
||||
- plan to use for the instance (Linode plan)
|
||||
default: null
|
||||
type: integer
|
||||
payment_term:
|
||||
description:
|
||||
- payment term to use for the instance (payment term in months)
|
||||
default: 1
|
||||
type: integer
|
||||
choices: [1, 12, 24]
|
||||
password:
|
||||
description:
|
||||
- root password to apply to a new server (auto generated if missing)
|
||||
default: null
|
||||
type: string
|
||||
ssh_pub_key:
|
||||
description:
|
||||
- SSH public key applied to root user
|
||||
default: null
|
||||
type: string
|
||||
swap:
|
||||
description:
|
||||
- swap size in MB
|
||||
default: 512
|
||||
type: integer
|
||||
distribution:
|
||||
description:
|
||||
- distribution to use for the instance (Linode Distribution)
|
||||
default: null
|
||||
type: integer
|
||||
datacenter:
|
||||
description:
|
||||
- datacenter to create an instance in (Linode Datacenter)
|
||||
default: null
|
||||
type: integer
|
||||
wait:
|
||||
description:
|
||||
- wait for the instance to be in state 'running' before returning
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
requirements: [ "linode-python", "pycurl" ]
|
||||
author: Vincent Viallet
|
||||
notes:
|
||||
- LINODE_API_KEY env variable can be used instead
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a server
|
||||
- local_action:
|
||||
module: linode
|
||||
api_key: 'longStringFromLinodeApi'
|
||||
name: linode-test1
|
||||
plan: 1
|
||||
datacenter: 2
|
||||
distribution: 99
|
||||
password: 'superSecureRootPassword'
|
||||
ssh_pub_key: 'ssh-rsa qwerty'
|
||||
swap: 768
|
||||
wait: yes
|
||||
wait_timeout: 600
|
||||
state: present
|
||||
|
||||
# Ensure a running server (create if missing)
|
||||
- local_action:
|
||||
module: linode
|
||||
api_key: 'longStringFromLinodeApi'
|
||||
name: linode-test1
|
||||
linode_id: 12345678
|
||||
plan: 1
|
||||
datacenter: 2
|
||||
distribution: 99
|
||||
password: 'superSecureRootPassword'
|
||||
ssh_pub_key: 'ssh-rsa qwerty'
|
||||
swap: 768
|
||||
wait: yes
|
||||
wait_timeout: 600
|
||||
state: present
|
||||
|
||||
# Delete a server
|
||||
- local_action:
|
||||
module: linode
|
||||
api_key: 'longStringFromLinodeApi'
|
||||
name: linode-test1
|
||||
linode_id: 12345678
|
||||
state: absent
|
||||
|
||||
# Stop a server
|
||||
- local_action:
|
||||
module: linode
|
||||
api_key: 'longStringFromLinodeApi'
|
||||
name: linode-test1
|
||||
linode_id: 12345678
|
||||
state: stopped
|
||||
|
||||
# Reboot a server
|
||||
- local_action:
|
||||
module: linode
|
||||
api_key: 'longStringFromLinodeApi'
|
||||
name: linode-test1
|
||||
linode_id: 12345678
|
||||
state: restarted
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
|
||||
try:
|
||||
import pycurl
|
||||
except ImportError:
|
||||
print("failed=True msg='pycurl required for this module'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
from linode import api as linode_api
|
||||
except ImportError:
|
||||
print("failed=True msg='linode-python required for this module'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def randompass():
|
||||
'''
|
||||
Generate a long random password that comply to Linode requirements
|
||||
'''
|
||||
# Linode API currently requires the following:
|
||||
# It must contain at least two of these four character classes:
|
||||
# lower case letters - upper case letters - numbers - punctuation
|
||||
# we play it safe :)
|
||||
import random
|
||||
import string
|
||||
# as of python 2.4, this reseeds the PRNG from urandom
|
||||
random.seed()
|
||||
lower = ''.join(random.choice(string.ascii_lowercase) for x in range(6))
|
||||
upper = ''.join(random.choice(string.ascii_uppercase) for x in range(6))
|
||||
number = ''.join(random.choice(string.digits) for x in range(6))
|
||||
punct = ''.join(random.choice(string.punctuation) for x in range(6))
|
||||
p = lower + upper + number + punct
|
||||
return ''.join(random.sample(p, len(p)))
|
||||
|
||||
def getInstanceDetails(api, server):
|
||||
'''
|
||||
Return the details of an instance, populating IPs, etc.
|
||||
'''
|
||||
instance = {'id': server['LINODEID'],
|
||||
'name': server['LABEL'],
|
||||
'public': [],
|
||||
'private': []}
|
||||
|
||||
# Populate with ips
|
||||
for ip in api.linode_ip_list(LinodeId=server['LINODEID']):
|
||||
if ip['ISPUBLIC'] and 'ipv4' not in instance:
|
||||
instance['ipv4'] = ip['IPADDRESS']
|
||||
instance['fqdn'] = ip['RDNS_NAME']
|
||||
if ip['ISPUBLIC']:
|
||||
instance['public'].append({'ipv4': ip['IPADDRESS'],
|
||||
'fqdn': ip['RDNS_NAME'],
|
||||
'ip_id': ip['IPADDRESSID']})
|
||||
else:
|
||||
instance['private'].append({'ipv4': ip['IPADDRESS'],
|
||||
'fqdn': ip['RDNS_NAME'],
|
||||
'ip_id': ip['IPADDRESSID']})
|
||||
return instance
|
||||
|
||||
def linodeServers(module, api, state, name, plan, distribution, datacenter, linode_id,
|
||||
payment_term, password, ssh_pub_key, swap, wait, wait_timeout):
|
||||
instances = []
|
||||
changed = False
|
||||
new_server = False
|
||||
servers = []
|
||||
disks = []
|
||||
configs = []
|
||||
jobs = []
|
||||
|
||||
# See if we can match an existing server details with the provided linode_id
|
||||
if linode_id:
|
||||
# For the moment we only consider linode_id as criteria for match
|
||||
# Later we can use more (size, name, etc.) and update existing
|
||||
servers = api.linode_list(LinodeId=linode_id)
|
||||
# Attempt to fetch details about disks and configs only if servers are
|
||||
# found with linode_id
|
||||
if servers:
|
||||
disks = api.linode_disk_list(LinodeId=linode_id)
|
||||
configs = api.linode_config_list(LinodeId=linode_id)
|
||||
|
||||
# Act on the state
|
||||
if state in ('active', 'present', 'started'):
|
||||
# TODO: validate all the plan / distribution / datacenter are valid
|
||||
|
||||
# Multi step process/validation:
|
||||
# - need linode_id (entity)
|
||||
# - need disk_id for linode_id - create disk from distrib
|
||||
# - need config_id for linode_id - create config (need kernel)
|
||||
|
||||
# Any create step triggers a job that need to be waited for.
|
||||
if not servers:
|
||||
for arg in ('name', 'plan', 'distribution', 'datacenter'):
|
||||
if not eval(arg):
|
||||
module.fail_json(msg='%s is required for active state' % arg)
|
||||
# Create linode entity
|
||||
new_server = True
|
||||
try:
|
||||
res = api.linode_create(DatacenterID=datacenter, PlanID=plan,
|
||||
PaymentTerm=payment_term)
|
||||
linode_id = res['LinodeID']
|
||||
# Update linode Label to match name
|
||||
api.linode_update(LinodeId=linode_id, Label='%s_%s' % (linode_id, name))
|
||||
# Save server
|
||||
servers = api.linode_list(LinodeId=linode_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
|
||||
if not disks:
|
||||
for arg in ('name', 'linode_id', 'distribution'):
|
||||
if not eval(arg):
|
||||
module.fail_json(msg='%s is required for active state' % arg)
|
||||
# Create disks (1 from distrib, 1 for SWAP)
|
||||
new_server = True
|
||||
try:
|
||||
if not password:
|
||||
# Password is required on creation, if not provided generate one
|
||||
password = randompass()
|
||||
if not swap:
|
||||
swap = 512
|
||||
# Create data disk
|
||||
size = servers[0]['TOTALHD'] - swap
|
||||
if ssh_pub_key:
|
||||
res = api.linode_disk_createfromdistribution(
|
||||
LinodeId=linode_id, DistributionID=distribution,
|
||||
rootPass=password, rootSSHKey=ssh_pub_key,
|
||||
Label='%s data disk (lid: %s)' % (name, linode_id), Size=size)
|
||||
else:
|
||||
res = api.linode_disk_createfromdistribution(
|
||||
LinodeId=linode_id, DistributionID=distribution, rootPass=password,
|
||||
Label='%s data disk (lid: %s)' % (name, linode_id), Size=size)
|
||||
jobs.append(res['JobID'])
|
||||
# Create SWAP disk
|
||||
res = api.linode_disk_create(LinodeId=linode_id, Type='swap',
|
||||
Label='%s swap disk (lid: %s)' % (name, linode_id),
|
||||
Size=swap)
|
||||
jobs.append(res['JobID'])
|
||||
except Exception, e:
|
||||
# TODO: destroy linode ?
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
|
||||
if not configs:
|
||||
for arg in ('name', 'linode_id', 'distribution'):
|
||||
if not eval(arg):
|
||||
module.fail_json(msg='%s is required for active state' % arg)
|
||||
|
||||
# Check architecture
|
||||
for distrib in api.avail_distributions():
|
||||
if distrib['DISTRIBUTIONID'] != distribution:
|
||||
continue
|
||||
arch = '32'
|
||||
if distrib['IS64BIT']:
|
||||
arch = '64'
|
||||
break
|
||||
|
||||
# Get latest kernel matching arch
|
||||
for kernel in api.avail_kernels():
|
||||
if not kernel['LABEL'].startswith('Latest %s' % arch):
|
||||
continue
|
||||
kernel_id = kernel['KERNELID']
|
||||
break
|
||||
|
||||
# Get disk list
|
||||
disks_id = []
|
||||
for disk in api.linode_disk_list(LinodeId=linode_id):
|
||||
if disk['TYPE'] == 'ext3':
|
||||
disks_id.insert(0, str(disk['DISKID']))
|
||||
continue
|
||||
disks_id.append(str(disk['DISKID']))
|
||||
# Trick to get the 9 items in the list
|
||||
while len(disks_id) < 9:
|
||||
disks_id.append('')
|
||||
disks_list = ','.join(disks_id)
|
||||
|
||||
# Create config
|
||||
new_server = True
|
||||
try:
|
||||
api.linode_config_create(LinodeId=linode_id, KernelId=kernel_id,
|
||||
Disklist=disks_list, Label='%s config' % name)
|
||||
configs = api.linode_config_list(LinodeId=linode_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
|
||||
# Start / Ensure servers are running
|
||||
for server in servers:
|
||||
# Refresh server state
|
||||
server = api.linode_list(LinodeId=server['LINODEID'])[0]
|
||||
# Ensure existing servers are up and running, boot if necessary
|
||||
if server['STATUS'] != 1:
|
||||
res = api.linode_boot(LinodeId=linode_id)
|
||||
jobs.append(res['JobID'])
|
||||
changed = True
|
||||
|
||||
# wait here until the instances are up
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
while wait and wait_timeout > time.time():
|
||||
# refresh the server details
|
||||
server = api.linode_list(LinodeId=server['LINODEID'])[0]
|
||||
# status:
|
||||
# -2: Boot failed
|
||||
# 1: Running
|
||||
if server['STATUS'] in (-2, 1):
|
||||
break
|
||||
time.sleep(5)
|
||||
if wait and wait_timeout <= time.time():
|
||||
# waiting took too long
|
||||
module.fail_json(msg = 'Timeout waiting on %s (lid: %s)' %
|
||||
(server['LABEL'], server['LINODEID']))
|
||||
# Get a fresh copy of the server details
|
||||
server = api.linode_list(LinodeId=server['LINODEID'])[0]
|
||||
if server['STATUS'] == -2:
|
||||
module.fail_json(msg = '%s (lid: %s) failed to boot' %
|
||||
(server['LABEL'], server['LINODEID']))
|
||||
# From now on we know the task is a success
|
||||
# Build instance report
|
||||
instance = getInstanceDetails(api, server)
|
||||
# depending on wait flag select the status
|
||||
if wait:
|
||||
instance['status'] = 'Running'
|
||||
else:
|
||||
instance['status'] = 'Starting'
|
||||
|
||||
# Return the root password if this is a new box and no SSH key
|
||||
# has been provided
|
||||
if new_server and not ssh_pub_key:
|
||||
instance['password'] = password
|
||||
instances.append(instance)
|
||||
|
||||
elif state in ('stopped'):
|
||||
for arg in ('name', 'linode_id'):
|
||||
if not eval(arg):
|
||||
module.fail_json(msg='%s is required for active state' % arg)
|
||||
|
||||
if not servers:
|
||||
module.fail_json(msg = 'Server %s (lid: %s) not found' % (name, linode_id))
|
||||
|
||||
for server in servers:
|
||||
instance = getInstanceDetails(api, server)
|
||||
if server['STATUS'] != 2:
|
||||
try:
|
||||
res = api.linode_shutdown(LinodeId=linode_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
instance['status'] = 'Stopping'
|
||||
changed = True
|
||||
else:
|
||||
instance['status'] = 'Stopped'
|
||||
instances.append(instance)
|
||||
|
||||
elif state in ('restarted'):
|
||||
for arg in ('name', 'linode_id'):
|
||||
if not eval(arg):
|
||||
module.fail_json(msg='%s is required for active state' % arg)
|
||||
|
||||
if not servers:
|
||||
module.fail_json(msg = 'Server %s (lid: %s) not found' % (name, linode_id))
|
||||
|
||||
for server in servers:
|
||||
instance = getInstanceDetails(api, server)
|
||||
try:
|
||||
res = api.linode_reboot(LinodeId=server['LINODEID'])
|
||||
except Exception, e:
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
instance['status'] = 'Restarting'
|
||||
changed = True
|
||||
instances.append(instance)
|
||||
|
||||
elif state in ('absent', 'deleted'):
|
||||
for server in servers:
|
||||
instance = getInstanceDetails(api, server)
|
||||
try:
|
||||
api.linode_delete(LinodeId=server['LINODEID'], skipChecks=True)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
instance['status'] = 'Deleting'
|
||||
changed = True
|
||||
instances.append(instance)
|
||||
|
||||
# Ease parsing if only 1 instance
|
||||
if len(instances) == 1:
|
||||
module.exit_json(changed=changed, instance=instances[0])
|
||||
module.exit_json(changed=changed, instances=instances)
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(default='present', choices=['active', 'present', 'started',
|
||||
'deleted', 'absent', 'stopped',
|
||||
'restarted']),
|
||||
api_key = dict(),
|
||||
name = dict(type='str'),
|
||||
plan = dict(type='int'),
|
||||
distribution = dict(type='int'),
|
||||
datacenter = dict(type='int'),
|
||||
linode_id = dict(type='int', aliases=['lid']),
|
||||
payment_term = dict(type='int', default=1, choices=[1, 12, 24]),
|
||||
password = dict(type='str'),
|
||||
ssh_pub_key = dict(type='str'),
|
||||
swap = dict(type='int', default=512),
|
||||
wait = dict(type='bool', default=True),
|
||||
wait_timeout = dict(default=300),
|
||||
)
|
||||
)
|
||||
|
||||
state = module.params.get('state')
|
||||
api_key = module.params.get('api_key')
|
||||
name = module.params.get('name')
|
||||
plan = module.params.get('plan')
|
||||
distribution = module.params.get('distribution')
|
||||
datacenter = module.params.get('datacenter')
|
||||
linode_id = module.params.get('linode_id')
|
||||
payment_term = module.params.get('payment_term')
|
||||
password = module.params.get('password')
|
||||
ssh_pub_key = module.params.get('ssh_pub_key')
|
||||
swap = module.params.get('swap')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
# Setup the api_key
|
||||
if not api_key:
|
||||
try:
|
||||
api_key = os.environ['LINODE_API_KEY']
|
||||
except KeyError, e:
|
||||
module.fail_json(msg = 'Unable to load %s' % e.message)
|
||||
|
||||
# setup the auth
|
||||
try:
|
||||
api = linode_api.Api(api_key)
|
||||
api.test_echo()
|
||||
except Exception, e:
|
||||
module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE'])
|
||||
|
||||
linodeServers(module, api, state, name, plan, distribution, datacenter, linode_id,
|
||||
payment_term, password, ssh_pub_key, swap, wait, wait_timeout)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,585 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Benno Joy <benno@ansible.com>
|
||||
# (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/>.
|
||||
|
||||
import operator
|
||||
import os
|
||||
|
||||
try:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
from novaclient.v1_1 import floating_ips
|
||||
from novaclient import exceptions
|
||||
from novaclient import utils
|
||||
import time
|
||||
except ImportError:
|
||||
print("failed=True msg='novaclient is required for this module'")
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: nova_compute
|
||||
version_added: "1.2"
|
||||
short_description: Create/Delete VMs from OpenStack
|
||||
description:
|
||||
- Create or Remove virtual machines from Openstack.
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- Name that has to be given to the instance
|
||||
required: true
|
||||
default: None
|
||||
image_id:
|
||||
description:
|
||||
- The id of the base image to boot. Mutually exclusive with image_name
|
||||
required: true
|
||||
default: None
|
||||
image_name:
|
||||
description:
|
||||
- The name of the base image to boot. Mutually exclusive with image_id
|
||||
required: true
|
||||
default: None
|
||||
version_added: "1.8"
|
||||
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)"
|
||||
version_added: "1.8"
|
||||
flavor_id:
|
||||
description:
|
||||
- The id of the flavor in which the new VM 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 VM has to be created must have. Mutually exclusive with flavor_id
|
||||
required: false
|
||||
default: 1
|
||||
version_added: "1.8"
|
||||
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.
|
||||
version_added: "1.8"
|
||||
key_name:
|
||||
description:
|
||||
- The key pair name to be used when creating a VM
|
||||
required: false
|
||||
default: None
|
||||
security_groups:
|
||||
description:
|
||||
- The name of the security group to which the VM should be added
|
||||
required: false
|
||||
default: None
|
||||
nics:
|
||||
description:
|
||||
- A list of network id's to which the VM's interface should be attached
|
||||
required: false
|
||||
default: None
|
||||
auto_floating_ip:
|
||||
description:
|
||||
- Should a floating ip be auto created and assigned
|
||||
required: false
|
||||
default: 'yes'
|
||||
version_added: "1.8"
|
||||
floating_ips:
|
||||
decription:
|
||||
- list of valid floating IPs that pre-exist to assign to this node
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.8"
|
||||
floating_ip_pools:
|
||||
description:
|
||||
- list of floating IP pools from which to choose a floating IP
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.8"
|
||||
availability_zone:
|
||||
description:
|
||||
- Name of the availability zone
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.8"
|
||||
meta:
|
||||
description:
|
||||
- A list of key value pairs that should be provided as a metadata to the new VM
|
||||
required: false
|
||||
default: None
|
||||
wait:
|
||||
description:
|
||||
- If the module should wait for the VM to be created.
|
||||
required: false
|
||||
default: 'yes'
|
||||
wait_for:
|
||||
description:
|
||||
- The amount of time the module should wait for the VM 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'
|
||||
version_added: "1.8"
|
||||
user_data:
|
||||
description:
|
||||
- Opaque blob of data which is made available to the instance
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.6"
|
||||
requirements: ["novaclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Creates a new VM and attaches to a network and passes metadata to the instance
|
||||
- nova_compute:
|
||||
state: present
|
||||
login_username: admin
|
||||
login_password: admin
|
||||
login_tenant_name: admin
|
||||
name: vm1
|
||||
image_id: 4f905f38-e52a-43d2-b6ec-754a13ffb529
|
||||
key_name: ansible_key
|
||||
wait_for: 200
|
||||
flavor_id: 4
|
||||
nics:
|
||||
- net-id: 34605f38-e52a-25d2-b6ec-754a13ffb723
|
||||
meta:
|
||||
hostname: test1
|
||||
group: uge_master
|
||||
|
||||
# Creates a new VM in HP Cloud AE1 region availability zone az2 and automatically assigns a floating IP
|
||||
- name: launch a nova instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
nova_compute:
|
||||
state: present
|
||||
login_username: username
|
||||
login_password: Equality7-2521
|
||||
login_tenant_name: username-project1
|
||||
name: vm1
|
||||
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
|
||||
region_name: region-b.geo-1
|
||||
availability_zone: az2
|
||||
image_id: 9302692b-b787-4b52-a3a6-daebb79cb498
|
||||
key_name: test
|
||||
wait_for: 200
|
||||
flavor_id: 101
|
||||
security_groups: default
|
||||
auto_floating_ip: yes
|
||||
|
||||
# Creates a new VM in HP Cloud AE1 region availability zone az2 and assigns a pre-known floating IP
|
||||
- name: launch a nova instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
nova_compute:
|
||||
state: present
|
||||
login_username: username
|
||||
login_password: Equality7-2521
|
||||
login_tenant_name: username-project1
|
||||
name: vm1
|
||||
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
|
||||
region_name: region-b.geo-1
|
||||
availability_zone: az2
|
||||
image_id: 9302692b-b787-4b52-a3a6-daebb79cb498
|
||||
key_name: test
|
||||
wait_for: 200
|
||||
flavor_id: 101
|
||||
floating-ips:
|
||||
- 12.34.56.79
|
||||
|
||||
# Creates a new VM with 4G of RAM on Ubuntu Trusty, ignoring deprecated images
|
||||
- name: launch a nova instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
nova_compute:
|
||||
name: vm1
|
||||
state: present
|
||||
login_username: username
|
||||
login_password: Equality7-2521
|
||||
login_tenant_name: username-project1
|
||||
auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/
|
||||
region_name: region-b.geo-1
|
||||
image_name: Ubuntu Server 14.04
|
||||
image_exclude: deprecated
|
||||
flavor_ram: 4096
|
||||
|
||||
# Creates a new VM with 4G of RAM on Ubuntu Trusty on a Rackspace Performance node in DFW
|
||||
- name: launch a nova instance
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- name: launch an instance
|
||||
nova_compute:
|
||||
name: vm1
|
||||
state: present
|
||||
login_username: username
|
||||
login_password: Equality7-2521
|
||||
login_tenant_name: username-project1
|
||||
auth_url: https://identity.api.rackspacecloud.com/v2.0/
|
||||
region_name: DFW
|
||||
image_name: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)
|
||||
flavor_ram: 4096
|
||||
flavor_include: Performance
|
||||
'''
|
||||
|
||||
|
||||
|
||||
def _delete_server(module, nova):
|
||||
name = None
|
||||
server_list = None
|
||||
try:
|
||||
server_list = nova.servers.list(True, {'name': module.params['name']})
|
||||
if server_list:
|
||||
server = [x for x in server_list if x.name == module.params['name']]
|
||||
nova.servers.delete(server.pop())
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in deleting vm: %s" % e.message)
|
||||
if module.params['wait'] == 'no':
|
||||
module.exit_json(changed = True, result = "deleted")
|
||||
expire = time.time() + int(module.params['wait_for'])
|
||||
while time.time() < expire:
|
||||
name = nova.servers.list(True, {'name': module.params['name']})
|
||||
if not name:
|
||||
module.exit_json(changed = True, result = "deleted")
|
||||
time.sleep(5)
|
||||
module.fail_json(msg = "Timed out waiting for server to get deleted, please check manually")
|
||||
|
||||
|
||||
def _add_floating_ip_from_pool(module, nova, server):
|
||||
|
||||
# instantiate FloatingIPManager object
|
||||
floating_ip_obj = floating_ips.FloatingIPManager(nova)
|
||||
|
||||
# empty dict and list
|
||||
usable_floating_ips = {}
|
||||
pools = []
|
||||
|
||||
# user specified
|
||||
pools = module.params['floating_ip_pools']
|
||||
|
||||
# get the list of all floating IPs. Mileage may
|
||||
# vary according to Nova Compute configuration
|
||||
# per cloud provider
|
||||
all_floating_ips = floating_ip_obj.list()
|
||||
|
||||
# iterate through all pools of IP address. Empty
|
||||
# string means all and is the default value
|
||||
for pool in pools:
|
||||
# temporary list per pool
|
||||
pool_ips = []
|
||||
# loop through all floating IPs
|
||||
for f_ip in all_floating_ips:
|
||||
# if not reserved and the correct pool, add
|
||||
if f_ip.instance_id is None and (f_ip.pool == pool):
|
||||
pool_ips.append(f_ip.ip)
|
||||
# only need one
|
||||
break
|
||||
|
||||
# if the list is empty, add for this pool
|
||||
if not pool_ips:
|
||||
try:
|
||||
new_ip = nova.floating_ips.create(pool)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Unable to create floating ip")
|
||||
pool_ips.append(new_ip.ip)
|
||||
# Add to the main list
|
||||
usable_floating_ips[pool] = pool_ips
|
||||
|
||||
# finally, add ip(s) to instance for each pool
|
||||
for pool in usable_floating_ips:
|
||||
for ip in usable_floating_ips[pool]:
|
||||
try:
|
||||
server.add_floating_ip(ip)
|
||||
# We only need to assign one ip - but there is an inherent
|
||||
# race condition and some other cloud operation may have
|
||||
# stolen an available floating ip
|
||||
break
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
|
||||
|
||||
|
||||
def _add_floating_ip_list(module, server, ips):
|
||||
# add ip(s) to instance
|
||||
for ip in ips:
|
||||
try:
|
||||
server.add_floating_ip(ip)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
|
||||
|
||||
|
||||
def _add_auto_floating_ip(module, nova, server):
|
||||
|
||||
try:
|
||||
new_ip = nova.floating_ips.create()
|
||||
except Exception as e:
|
||||
module.fail_json(msg = "Unable to create floating ip: %s" % (e.message))
|
||||
|
||||
try:
|
||||
server.add_floating_ip(new_ip)
|
||||
except Exception as e:
|
||||
# Clean up - we auto-created this ip, and it's not attached
|
||||
# to the server, so the cloud will not know what to do with it
|
||||
server.floating_ips.delete(new_ip)
|
||||
module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message))
|
||||
|
||||
|
||||
def _add_floating_ip(module, nova, server):
|
||||
|
||||
if module.params['floating_ip_pools']:
|
||||
_add_floating_ip_from_pool(module, nova, server)
|
||||
elif module.params['floating_ips']:
|
||||
_add_floating_ip_list(module, server, module.params['floating_ips'])
|
||||
elif module.params['auto_floating_ip']:
|
||||
_add_auto_floating_ip(module, nova, server)
|
||||
else:
|
||||
return server
|
||||
|
||||
# this may look redundant, but if there is now a
|
||||
# floating IP, then it needs to be obtained from
|
||||
# a recent server object if the above code path exec'd
|
||||
try:
|
||||
server = nova.servers.get(server.id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting info from instance: %s " % e.message)
|
||||
return server
|
||||
|
||||
|
||||
def _get_image_id(module, nova):
|
||||
if module.params['image_name']:
|
||||
for image in nova.images.list():
|
||||
if (module.params['image_name'] in image.name and (
|
||||
not module.params['image_exclude']
|
||||
or module.params['image_exclude'] not in image.name)):
|
||||
return image.id
|
||||
module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name'])
|
||||
return module.params['image_id']
|
||||
|
||||
|
||||
def _get_flavor_id(module, nova):
|
||||
if module.params['flavor_ram']:
|
||||
for flavor in sorted(nova.flavors.list(), key=operator.attrgetter('ram')):
|
||||
if (flavor.ram >= module.params['flavor_ram'] and
|
||||
(not module.params['flavor_include'] or module.params['flavor_include'] in flavor.name)):
|
||||
return flavor.id
|
||||
module.fail_json(msg = "Error finding flavor with %sMB of RAM" % module.params['flavor_ram'])
|
||||
return module.params['flavor_id']
|
||||
|
||||
|
||||
def _create_server(module, nova):
|
||||
image_id = _get_image_id(module, nova)
|
||||
flavor_id = _get_flavor_id(module, nova)
|
||||
bootargs = [module.params['name'], image_id, flavor_id]
|
||||
bootkwargs = {
|
||||
'nics' : module.params['nics'],
|
||||
'meta' : module.params['meta'],
|
||||
'security_groups': module.params['security_groups'].split(','),
|
||||
#userdata is unhyphenated in novaclient, but hyphenated here for consistency with the ec2 module:
|
||||
'userdata': module.params['user_data'],
|
||||
'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]
|
||||
try:
|
||||
server = nova.servers.create(*bootargs, **bootkwargs)
|
||||
server = nova.servers.get(server.id)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in creating instance: %s " % e.message)
|
||||
if module.params['wait'] == 'yes':
|
||||
expire = time.time() + int(module.params['wait_for'])
|
||||
while time.time() < expire:
|
||||
try:
|
||||
server = nova.servers.get(server.id)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in getting info from instance: %s" % e.message)
|
||||
if server.status == 'ACTIVE':
|
||||
server = _add_floating_ip(module, nova, server)
|
||||
|
||||
private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private')
|
||||
public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public')
|
||||
|
||||
# now exit with info
|
||||
module.exit_json(changed = True, id = server.id, private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info)
|
||||
|
||||
if server.status == 'ERROR':
|
||||
module.fail_json(msg = "Error in creating the server, please check logs")
|
||||
time.sleep(2)
|
||||
|
||||
module.fail_json(msg = "Timeout waiting for the server to come up.. Please check manually")
|
||||
if server.status == 'ERROR':
|
||||
module.fail_json(msg = "Error in creating the server.. Please check manually")
|
||||
private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private')
|
||||
public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public')
|
||||
|
||||
module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info)
|
||||
|
||||
|
||||
def _delete_floating_ip_list(module, nova, server, extra_ips):
|
||||
for ip in extra_ips:
|
||||
nova.servers.remove_floating_ip(server=server.id, address=ip)
|
||||
|
||||
|
||||
def _check_floating_ips(module, nova, server):
|
||||
changed = False
|
||||
if module.params['floating_ip_pools'] or module.params['floating_ips'] or module.params['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 = _add_floating_ip(module, nova, server)
|
||||
changed = True
|
||||
elif module.params['floating_ips']:
|
||||
# we were configured to have specific ips, let's make sure we have
|
||||
# those
|
||||
missing_ips = []
|
||||
for ip in module.params['floating_ips']:
|
||||
if ip not in ips:
|
||||
missing_ips.append(ip)
|
||||
if missing_ips:
|
||||
server = _add_floating_ip_list(module, server, missing_ips)
|
||||
changed = True
|
||||
extra_ips = []
|
||||
for ip in ips:
|
||||
if ip not in module.params['floating_ips']:
|
||||
extra_ips.append(ip)
|
||||
if extra_ips:
|
||||
_delete_floating_ip_list(module, server, extra_ips)
|
||||
changed = True
|
||||
return (changed, server)
|
||||
|
||||
|
||||
def _get_server_state(module, nova):
|
||||
server = None
|
||||
try:
|
||||
servers = nova.servers.list(True, {'name': module.params['name']})
|
||||
if servers:
|
||||
# the {'name': module.params['name']} will also return servers
|
||||
# with names that partially match the server name, so we have to
|
||||
# strictly filter here
|
||||
servers = [x for x in servers if x.name == module.params['name']]
|
||||
if servers:
|
||||
server = servers[0]
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting the server list: %s" % e.message)
|
||||
if server and module.params['state'] == 'present':
|
||||
if server.status != 'ACTIVE':
|
||||
module.fail_json( msg="The VM is available but not Active. state:" + server.status)
|
||||
(ip_changed, server) = _check_floating_ips(module, nova, server)
|
||||
private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private')
|
||||
public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public')
|
||||
module.exit_json(changed = ip_changed, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info)
|
||||
if server and module.params['state'] == 'absent':
|
||||
return True
|
||||
if module.params['state'] == 'absent':
|
||||
module.exit_json(changed = False, result = "not present")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name = dict(required=True),
|
||||
image_id = dict(default=None),
|
||||
image_name = dict(default=None),
|
||||
image_exclude = dict(default='(deprecated)'),
|
||||
flavor_id = dict(default=1),
|
||||
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=None),
|
||||
meta = dict(default=None),
|
||||
wait = dict(default='yes', choices=['yes', 'no']),
|
||||
wait_for = dict(default=180),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
user_data = dict(default=None),
|
||||
config_drive = dict(default=False, type='bool'),
|
||||
auto_floating_ip = dict(default=False, type='bool'),
|
||||
floating_ips = dict(default=None),
|
||||
floating_ip_pools = dict(default=None),
|
||||
))
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
['auto_floating_ip','floating_ips'],
|
||||
['auto_floating_ip','floating_ip_pools'],
|
||||
['floating_ips','floating_ip_pools'],
|
||||
['image_id','image_name'],
|
||||
['flavor_id','flavor_ram'],
|
||||
],
|
||||
)
|
||||
|
||||
nova = nova_client.Client(module.params['login_username'],
|
||||
module.params['login_password'],
|
||||
module.params['login_tenant_name'],
|
||||
module.params['auth_url'],
|
||||
region_name=module.params['region_name'],
|
||||
service_type='compute')
|
||||
try:
|
||||
nova.authenticate()
|
||||
except exceptions.Unauthorized, e:
|
||||
module.fail_json(msg = "Invalid OpenStack Nova credentials.: %s" % e.message)
|
||||
except exceptions.AuthorizationFailure, e:
|
||||
module.fail_json(msg = "Unable to authorize user: %s" % e.message)
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
if not module.params['image_id'] and not module.params['image_name']:
|
||||
module.fail_json( msg = "Parameter 'image_id' or `image_name` is required if state == 'present'")
|
||||
else:
|
||||
_get_server_state(module, nova)
|
||||
_create_server(module, nova)
|
||||
if module.params['state'] == 'absent':
|
||||
_get_server_state(module, nova)
|
||||
_delete_server(module, nova)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,139 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Benno Joy <benno@ansible.com>
|
||||
# (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:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
from novaclient import exceptions as exc
|
||||
import time
|
||||
except ImportError:
|
||||
print("failed=True msg='novaclient is required for this module to work'")
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: nova_keypair
|
||||
version_added: "1.2"
|
||||
short_description: Add/Delete key pair from nova
|
||||
description:
|
||||
- Add or Remove key pair from nova .
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- Name that has to be given to the key pair
|
||||
required: true
|
||||
default: None
|
||||
public_key:
|
||||
description:
|
||||
- The public key that would be uploaded to nova and injected to vm's upon creation
|
||||
required: false
|
||||
default: None
|
||||
|
||||
requirements: ["novaclient"]
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
# Creates a key pair with the running users public key
|
||||
- nova_keypair: state=present login_username=admin
|
||||
login_password=admin login_tenant_name=admin name=ansible_key
|
||||
public_key={{ lookup('file','~/.ssh/id_rsa.pub') }}
|
||||
|
||||
# Creates a new key pair and the private key returned after the run.
|
||||
- nova_keypair: state=present login_username=admin login_password=admin
|
||||
login_tenant_name=admin name=ansible_key
|
||||
'''
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name = dict(required=True),
|
||||
public_key = dict(default=None),
|
||||
state = dict(default='present', choices=['absent', 'present'])
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
nova = nova_client.Client(module.params['login_username'],
|
||||
module.params['login_password'],
|
||||
module.params['login_tenant_name'],
|
||||
module.params['auth_url'],
|
||||
region_name=module.params['region_name'],
|
||||
service_type='compute')
|
||||
try:
|
||||
nova.authenticate()
|
||||
except exc.Unauthorized, e:
|
||||
module.fail_json(msg = "Invalid OpenStack Nova credentials.: %s" % e.message)
|
||||
except exc.AuthorizationFailure, e:
|
||||
module.fail_json(msg = "Unable to authorize user: %s" % e.message)
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
for key in nova.keypairs.list():
|
||||
if key.name == module.params['name']:
|
||||
if module.params['public_key'] and (module.params['public_key'] != key.public_key ):
|
||||
module.fail_json(msg = "name {} present but key hash not the same as offered. Delete key first.".format(key['name']))
|
||||
else:
|
||||
module.exit_json(changed = False, result = "Key present")
|
||||
try:
|
||||
key = nova.keypairs.create(module.params['name'], module.params['public_key'])
|
||||
except Exception, e:
|
||||
module.exit_json(msg = "Error in creating the keypair: %s" % e.message)
|
||||
if not module.params['public_key']:
|
||||
module.exit_json(changed = True, key = key.private_key)
|
||||
module.exit_json(changed = True, key = None)
|
||||
if module.params['state'] == 'absent':
|
||||
for key in nova.keypairs.list():
|
||||
if key.name == module.params['name']:
|
||||
try:
|
||||
nova.keypairs.delete(module.params['name'])
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "The keypair deletion has failed: %s" % e.message)
|
||||
module.exit_json( changed = True, result = "deleted")
|
||||
module.exit_json(changed = False, result = "not present")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,425 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Vincent Van der Kussen <vincent at vanderkussen.org>
|
||||
#
|
||||
# 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: ovirt
|
||||
author: Vincent Van der Kussen
|
||||
short_description: oVirt/RHEV platform management
|
||||
description:
|
||||
- allows you to create new instances, either from scratch or an image, in addition to deleting or stopping instances on the oVirt/RHEV platform
|
||||
version_added: "1.4"
|
||||
options:
|
||||
user:
|
||||
description:
|
||||
- the user to authenticate with
|
||||
default: null
|
||||
required: true
|
||||
aliases: []
|
||||
url:
|
||||
description:
|
||||
- the url of the oVirt instance
|
||||
default: null
|
||||
required: true
|
||||
aliases: []
|
||||
instance_name:
|
||||
description:
|
||||
- the name of the instance to use
|
||||
default: null
|
||||
required: true
|
||||
aliases: [ vmname ]
|
||||
password:
|
||||
description:
|
||||
- password of the user to authenticate with
|
||||
default: null
|
||||
required: true
|
||||
aliases: []
|
||||
image:
|
||||
description:
|
||||
- template to use for the instance
|
||||
default: null
|
||||
required: false
|
||||
aliases: []
|
||||
resource_type:
|
||||
description:
|
||||
- whether you want to deploy an image or create an instance from scratch.
|
||||
default: null
|
||||
required: false
|
||||
aliases: []
|
||||
choices: [ 'new', 'template' ]
|
||||
zone:
|
||||
description:
|
||||
- deploy the image to this oVirt cluster
|
||||
default: null
|
||||
required: false
|
||||
aliases: []
|
||||
instance_disksize:
|
||||
description:
|
||||
- size of the instance's disk in GB
|
||||
default: null
|
||||
required: false
|
||||
aliases: [ vm_disksize]
|
||||
instance_cpus:
|
||||
description:
|
||||
- the instance's number of cpu's
|
||||
default: 1
|
||||
required: false
|
||||
aliases: [ vmcpus ]
|
||||
instance_nic:
|
||||
description:
|
||||
- name of the network interface in oVirt/RHEV
|
||||
default: null
|
||||
required: false
|
||||
aliases: [ vmnic ]
|
||||
instance_network:
|
||||
description:
|
||||
- the logical network the machine should belong to
|
||||
default: rhevm
|
||||
required: false
|
||||
aliases: [ vmnetwork ]
|
||||
instance_mem:
|
||||
description:
|
||||
- the instance's amount of memory in MB
|
||||
default: null
|
||||
required: false
|
||||
aliases: [ vmmem ]
|
||||
instance_type:
|
||||
description:
|
||||
- define if the instance is a server or desktop
|
||||
default: server
|
||||
required: false
|
||||
aliases: [ vmtype ]
|
||||
choices: [ 'server', 'desktop' ]
|
||||
disk_alloc:
|
||||
description:
|
||||
- define if disk is thin or preallocated
|
||||
default: thin
|
||||
required: false
|
||||
aliases: []
|
||||
choices: [ 'thin', 'preallocated' ]
|
||||
disk_int:
|
||||
description:
|
||||
- interface type of the disk
|
||||
default: virtio
|
||||
required: false
|
||||
aliases: []
|
||||
choices: [ 'virtio', 'ide' ]
|
||||
instance_os:
|
||||
description:
|
||||
- type of Operating System
|
||||
default: null
|
||||
required: false
|
||||
aliases: [ vmos ]
|
||||
instance_cores:
|
||||
description:
|
||||
- define the instance's number of cores
|
||||
default: 1
|
||||
required: false
|
||||
aliases: [ vmcores ]
|
||||
sdomain:
|
||||
description:
|
||||
- the Storage Domain where you want to create the instance's disk on.
|
||||
default: null
|
||||
required: false
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- the oVirt/RHEV datacenter where you want to deploy to
|
||||
default: null
|
||||
required: false
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- create, terminate or remove instances
|
||||
default: 'present'
|
||||
required: false
|
||||
aliases: []
|
||||
choices: ['present', 'absent', 'shutdown', 'started', 'restarted']
|
||||
|
||||
requirements: [ "ovirt-engine-sdk" ]
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
# Basic example provisioning from image.
|
||||
|
||||
action: ovirt >
|
||||
user=admin@internal
|
||||
url=https://ovirt.example.com
|
||||
instance_name=ansiblevm04
|
||||
password=secret
|
||||
image=centos_64
|
||||
zone=cluster01
|
||||
resource_type=template"
|
||||
|
||||
# Full example to create new instance from scratch
|
||||
action: ovirt >
|
||||
instance_name=testansible
|
||||
resource_type=new
|
||||
instance_type=server
|
||||
user=admin@internal
|
||||
password=secret
|
||||
url=https://ovirt.example.com
|
||||
instance_disksize=10
|
||||
zone=cluster01
|
||||
region=datacenter1
|
||||
instance_cpus=1
|
||||
instance_nic=nic1
|
||||
instance_network=rhevm
|
||||
instance_mem=1000
|
||||
disk_alloc=thin
|
||||
sdomain=FIBER01
|
||||
instance_cores=1
|
||||
instance_os=rhel_6x64
|
||||
disk_int=virtio"
|
||||
|
||||
# stopping an instance
|
||||
action: ovirt >
|
||||
instance_name=testansible
|
||||
state=stopped
|
||||
user=admin@internal
|
||||
password=secret
|
||||
url=https://ovirt.example.com
|
||||
|
||||
# starting an instance
|
||||
action: ovirt >
|
||||
instance_name=testansible
|
||||
state=started
|
||||
user=admin@internal
|
||||
password=secret
|
||||
url=https://ovirt.example.com
|
||||
|
||||
|
||||
'''
|
||||
try:
|
||||
from ovirtsdk.api import API
|
||||
from ovirtsdk.xml import params
|
||||
except ImportError:
|
||||
print "failed=True msg='ovirtsdk required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
# ------------------------------------------------------------------- #
|
||||
# create connection with API
|
||||
#
|
||||
def conn(url, user, password):
|
||||
api = API(url=url, username=user, password=password, insecure=True)
|
||||
try:
|
||||
value = api.test()
|
||||
except:
|
||||
print "error connecting to the oVirt API"
|
||||
sys.exit(1)
|
||||
return api
|
||||
|
||||
# ------------------------------------------------------------------- #
|
||||
# Create VM from scratch
|
||||
def create_vm(conn, vmtype, vmname, zone, vmdisk_size, vmcpus, vmnic, vmnetwork, vmmem, vmdisk_alloc, sdomain, vmcores, vmos, vmdisk_int):
|
||||
if vmdisk_alloc == 'thin':
|
||||
# define VM params
|
||||
vmparams = params.VM(name=vmname,cluster=conn.clusters.get(name=zone),os=params.OperatingSystem(type_=vmos),template=conn.templates.get(name="Blank"),memory=1024 * 1024 * int(vmmem),cpu=params.CPU(topology=params.CpuTopology(cores=int(vmcores))), type_=vmtype)
|
||||
# define disk params
|
||||
vmdisk= params.Disk(size=1024 * 1024 * 1024 * int(vmdisk_size), wipe_after_delete=True, sparse=True, interface=vmdisk_int, type_="System", format='cow',
|
||||
storage_domains=params.StorageDomains(storage_domain=[conn.storagedomains.get(name=sdomain)]))
|
||||
# define network parameters
|
||||
network_net = params.Network(name=vmnetwork)
|
||||
nic_net1 = params.NIC(name='nic1', network=network_net, interface='virtio')
|
||||
elif vmdisk_alloc == 'preallocated':
|
||||
# define VM params
|
||||
vmparams = params.VM(name=vmname,cluster=conn.clusters.get(name=zone),os=params.OperatingSystem(type_=vmos),template=conn.templates.get(name="Blank"),memory=1024 * 1024 * int(vmmem),cpu=params.CPU(topology=params.CpuTopology(cores=int(vmcores))) ,type_=vmtype)
|
||||
# define disk params
|
||||
vmdisk= params.Disk(size=1024 * 1024 * 1024 * int(vmdisk_size), wipe_after_delete=True, sparse=False, interface=vmdisk_int, type_="System", format='raw',
|
||||
storage_domains=params.StorageDomains(storage_domain=[conn.storagedomains.get(name=sdomain)]))
|
||||
# define network parameters
|
||||
network_net = params.Network(name=vmnetwork)
|
||||
nic_net1 = params.NIC(name=vmnic, network=network_net, interface='virtio')
|
||||
|
||||
try:
|
||||
conn.vms.add(vmparams)
|
||||
except:
|
||||
print "Error creating VM with specified parameters"
|
||||
sys.exit(1)
|
||||
vm = conn.vms.get(name=vmname)
|
||||
try:
|
||||
vm.disks.add(vmdisk)
|
||||
except:
|
||||
print "Error attaching disk"
|
||||
try:
|
||||
vm.nics.add(nic_net1)
|
||||
except:
|
||||
print "Error adding nic"
|
||||
|
||||
|
||||
# create an instance from a template
|
||||
def create_vm_template(conn, vmname, image, zone):
|
||||
vmparams = params.VM(name=vmname, cluster=conn.clusters.get(name=zone), template=conn.templates.get(name=image),disks=params.Disks(clone=True))
|
||||
try:
|
||||
conn.vms.add(vmparams)
|
||||
except:
|
||||
print 'error adding template %s' % image
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# start instance
|
||||
def vm_start(conn, vmname):
|
||||
vm = conn.vms.get(name=vmname)
|
||||
vm.start()
|
||||
|
||||
# Stop instance
|
||||
def vm_stop(conn, vmname):
|
||||
vm = conn.vms.get(name=vmname)
|
||||
vm.stop()
|
||||
|
||||
# restart instance
|
||||
def vm_restart(conn, vmname):
|
||||
state = vm_status(conn, vmname)
|
||||
vm = conn.vms.get(name=vmname)
|
||||
vm.stop()
|
||||
while conn.vms.get(vmname).get_status().get_state() != 'down':
|
||||
time.sleep(5)
|
||||
vm.start()
|
||||
|
||||
# remove an instance
|
||||
def vm_remove(conn, vmname):
|
||||
vm = conn.vms.get(name=vmname)
|
||||
vm.delete()
|
||||
|
||||
# ------------------------------------------------------------------- #
|
||||
# VM statuses
|
||||
#
|
||||
# Get the VMs status
|
||||
def vm_status(conn, vmname):
|
||||
status = conn.vms.get(name=vmname).status.state
|
||||
print "vm status is : %s" % status
|
||||
return status
|
||||
|
||||
|
||||
# Get VM object and return it's name if object exists
|
||||
def get_vm(conn, vmname):
|
||||
vm = conn.vms.get(name=vmname)
|
||||
if vm == None:
|
||||
name = "empty"
|
||||
print "vmname: %s" % name
|
||||
else:
|
||||
name = vm.get_name()
|
||||
print "vmname: %s" % name
|
||||
return name
|
||||
|
||||
# ------------------------------------------------------------------- #
|
||||
# Hypervisor operations
|
||||
#
|
||||
# not available yet
|
||||
# ------------------------------------------------------------------- #
|
||||
# Main
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(default='present', choices=['present', 'absent', 'shutdown', 'started', 'restart']),
|
||||
#name = dict(required=True),
|
||||
user = dict(required=True),
|
||||
url = dict(required=True),
|
||||
instance_name = dict(required=True, aliases=['vmname']),
|
||||
password = dict(required=True),
|
||||
image = dict(),
|
||||
resource_type = dict(choices=['new', 'template']),
|
||||
zone = dict(),
|
||||
instance_disksize = dict(aliases=['vm_disksize']),
|
||||
instance_cpus = dict(default=1, aliases=['vmcpus']),
|
||||
instance_nic = dict(aliases=['vmnic']),
|
||||
instance_network = dict(default='rhevm', aliases=['vmnetwork']),
|
||||
instance_mem = dict(aliases=['vmmem']),
|
||||
instance_type = dict(default='server', aliases=['vmtype'], choices=['server', 'desktop']),
|
||||
disk_alloc = dict(default='thin', choices=['thin', 'preallocated']),
|
||||
disk_int = dict(default='virtio', choices=['virtio', 'ide']),
|
||||
instance_os = dict(aliases=['vmos']),
|
||||
instance_cores = dict(default=1, aliases=['vmcores']),
|
||||
sdomain = dict(),
|
||||
region = dict(),
|
||||
)
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
user = module.params['user']
|
||||
url = module.params['url']
|
||||
vmname = module.params['instance_name']
|
||||
password = module.params['password']
|
||||
image = module.params['image'] # name of the image to deploy
|
||||
resource_type = module.params['resource_type'] # template or from scratch
|
||||
zone = module.params['zone'] # oVirt cluster
|
||||
vmdisk_size = module.params['instance_disksize'] # disksize
|
||||
vmcpus = module.params['instance_cpus'] # number of cpu
|
||||
vmnic = module.params['instance_nic'] # network interface
|
||||
vmnetwork = module.params['instance_network'] # logical network
|
||||
vmmem = module.params['instance_mem'] # mem size
|
||||
vmdisk_alloc = module.params['disk_alloc'] # thin, preallocated
|
||||
vmdisk_int = module.params['disk_int'] # disk interface virtio or ide
|
||||
vmos = module.params['instance_os'] # Operating System
|
||||
vmtype = module.params['instance_type'] # server or desktop
|
||||
vmcores = module.params['instance_cores'] # number of cores
|
||||
sdomain = module.params['sdomain'] # storage domain to store disk on
|
||||
region = module.params['region'] # oVirt Datacenter
|
||||
#initialize connection
|
||||
c = conn(url+"/api", user, password)
|
||||
|
||||
if state == 'present':
|
||||
if get_vm(c, vmname) == "empty":
|
||||
if resource_type == 'template':
|
||||
create_vm_template(c, vmname, image, zone)
|
||||
module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmname,image))
|
||||
elif resource_type == 'new':
|
||||
# FIXME: refactor, use keyword args.
|
||||
create_vm(c, vmtype, vmname, zone, vmdisk_size, vmcpus, vmnic, vmnetwork, vmmem, vmdisk_alloc, sdomain, vmcores, vmos, vmdisk_int)
|
||||
module.exit_json(changed=True, msg="deployed VM %s from scratch" % vmname)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="You did not specify a resource type")
|
||||
else:
|
||||
module.exit_json(changed=False, msg="VM %s already exists" % vmname)
|
||||
|
||||
if state == 'started':
|
||||
if vm_status(c, vmname) == 'up':
|
||||
module.exit_json(changed=False, msg="VM %s is already running" % vmname)
|
||||
else:
|
||||
vm_start(c, vmname)
|
||||
module.exit_json(changed=True, msg="VM %s started" % vmname)
|
||||
|
||||
if state == 'shutdown':
|
||||
if vm_status(c, vmname) == 'down':
|
||||
module.exit_json(changed=False, msg="VM %s is already shutdown" % vmname)
|
||||
else:
|
||||
vm_stop(c, vmname)
|
||||
module.exit_json(changed=True, msg="VM %s is shutting down" % vmname)
|
||||
|
||||
if state == 'restart':
|
||||
if vm_status(c, vmname) == 'up':
|
||||
vm_restart(c, vmname)
|
||||
module.exit_json(changed=True, msg="VM %s is restarted" % vmname)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="VM %s is not running" % vmname)
|
||||
|
||||
if state == 'absent':
|
||||
if get_vm(c, vmname) == "empty":
|
||||
module.exit_json(changed=False, msg="VM %s does not exist" % vmname)
|
||||
else:
|
||||
vm_remove(c, vmname)
|
||||
module.exit_json(changed=True, msg="VM %s removed" % vmname)
|
||||
|
||||
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,266 +0,0 @@
|
||||
#!/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:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
import time
|
||||
except ImportError:
|
||||
print("failed=True msg='novaclient,keystoneclient and quantumclient (or neutronclient) are required'")
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_floating_ip
|
||||
version_added: "1.2"
|
||||
short_description: Add/Remove floating IP from an instance
|
||||
description:
|
||||
- Add or Remove a floating IP to an instance
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
network_name:
|
||||
description:
|
||||
- Name of the network from which IP has to be assigned to VM. Please make sure the network is an external network
|
||||
required: true
|
||||
default: None
|
||||
instance_name:
|
||||
description:
|
||||
- The name of the instance to which the IP address should be assigned
|
||||
required: true
|
||||
default: None
|
||||
internal_network_name:
|
||||
description:
|
||||
- The name of the network of the port to associate with the floating ip. Necessary when VM multiple networks.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.5"
|
||||
requirements: ["novaclient", "quantumclient", "neutronclient", "keystoneclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Assign a floating ip to the instance from an external network
|
||||
- quantum_floating_ip: state=present login_username=admin login_password=admin
|
||||
login_tenant_name=admin network_name=external_network
|
||||
instance_name=vm1 internal_network_name=internal_network
|
||||
'''
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s " % e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in connecting to neutron: %s " % e.message)
|
||||
return neutron
|
||||
|
||||
def _get_server_state(module, nova):
|
||||
server_info = None
|
||||
server = None
|
||||
try:
|
||||
for server in nova.servers.list():
|
||||
if server:
|
||||
info = server._info
|
||||
if info['name'] == module.params['instance_name']:
|
||||
if info['status'] != 'ACTIVE' and module.params['state'] == 'present':
|
||||
module.fail_json( msg="The VM is available but not Active. state:" + info['status'])
|
||||
server_info = info
|
||||
break
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting the server list: %s" % e.message)
|
||||
return server_info, server
|
||||
|
||||
def _get_port_info(neutron, module, instance_id, internal_network_name=None):
|
||||
subnet_id = None
|
||||
if internal_network_name:
|
||||
kwargs = {'name': internal_network_name}
|
||||
networks = neutron.list_networks(**kwargs)
|
||||
network_id = networks['networks'][0]['id']
|
||||
kwargs = {
|
||||
'network_id': network_id,
|
||||
'ip_version': 4
|
||||
}
|
||||
subnets = neutron.list_subnets(**kwargs)
|
||||
subnet_id = subnets['subnets'][0]['id']
|
||||
kwargs = {
|
||||
'device_id': instance_id,
|
||||
}
|
||||
try:
|
||||
ports = neutron.list_ports(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in listing ports: %s" % e.message)
|
||||
if subnet_id:
|
||||
port = next(port for port in ports['ports'] if port['fixed_ips'][0]['subnet_id'] == subnet_id)
|
||||
port_id = port['id']
|
||||
fixed_ip_address = port['fixed_ips'][0]['ip_address']
|
||||
else:
|
||||
port_id = ports['ports'][0]['id']
|
||||
fixed_ip_address = ports['ports'][0]['fixed_ips'][0]['ip_address']
|
||||
if not ports['ports']:
|
||||
return None, None
|
||||
return fixed_ip_address, port_id
|
||||
|
||||
def _get_floating_ip(module, neutron, fixed_ip_address):
|
||||
kwargs = {
|
||||
'fixed_ip_address': fixed_ip_address
|
||||
}
|
||||
try:
|
||||
ips = neutron.list_floatingips(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "error in fetching the floatingips's %s" % e.message)
|
||||
if not ips['floatingips']:
|
||||
return None, None
|
||||
return ips['floatingips'][0]['id'], ips['floatingips'][0]['floating_ip_address']
|
||||
|
||||
def _create_floating_ip(neutron, module, port_id, net_id, fixed_ip):
|
||||
kwargs = {
|
||||
'port_id': port_id,
|
||||
'floating_network_id': net_id,
|
||||
'fixed_ip_address': fixed_ip
|
||||
}
|
||||
try:
|
||||
result = neutron.create_floatingip({'floatingip': kwargs})
|
||||
except Exception, e:
|
||||
module.fail_json(msg="There was an error in updating the floating ip address: %s" % e.message)
|
||||
module.exit_json(changed=True, result=result, public_ip=result['floatingip']['floating_ip_address'])
|
||||
|
||||
def _get_net_id(neutron, module):
|
||||
kwargs = {
|
||||
'name': module.params['network_name'],
|
||||
}
|
||||
try:
|
||||
networks = neutron.list_networks(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json("Error in listing neutron networks: %s" % e.message)
|
||||
if not networks['networks']:
|
||||
return None
|
||||
return networks['networks'][0]['id']
|
||||
|
||||
def _update_floating_ip(neutron, module, port_id, floating_ip_id):
|
||||
kwargs = {
|
||||
'port_id': port_id
|
||||
}
|
||||
try:
|
||||
result = neutron.update_floatingip(floating_ip_id, {'floatingip': kwargs})
|
||||
except Exception, e:
|
||||
module.fail_json(msg="There was an error in updating the floating ip address: %s" % e.message)
|
||||
module.exit_json(changed=True, result=result)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
network_name = dict(required=True),
|
||||
instance_name = dict(required=True),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
internal_network_name = dict(default=None),
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
try:
|
||||
nova = nova_client.Client(module.params['login_username'], module.params['login_password'],
|
||||
module.params['login_tenant_name'], module.params['auth_url'], service_type='compute')
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error in authenticating to nova: %s" % e.message)
|
||||
|
||||
server_info, server_obj = _get_server_state(module, nova)
|
||||
if not server_info:
|
||||
module.fail_json(msg="The instance name provided cannot be found")
|
||||
|
||||
fixed_ip, port_id = _get_port_info(neutron, module, server_info['id'], module.params['internal_network_name'])
|
||||
if not port_id:
|
||||
module.fail_json(msg="Cannot find a port for this instance, maybe fixed ip is not assigned")
|
||||
|
||||
floating_id, floating_ip = _get_floating_ip(module, neutron, fixed_ip)
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
if floating_ip:
|
||||
module.exit_json(changed = False, public_ip=floating_ip)
|
||||
net_id = _get_net_id(neutron, module)
|
||||
if not net_id:
|
||||
module.fail_json(msg = "cannot find the network specified, please check")
|
||||
_create_floating_ip(neutron, module, port_id, net_id, fixed_ip)
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
if floating_ip:
|
||||
_update_floating_ip(neutron, module, None, floating_id)
|
||||
module.exit_json(changed=False)
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,218 +0,0 @@
|
||||
#!/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:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
import time
|
||||
except ImportError:
|
||||
print "failed=True msg='novaclient, keystone, and quantumclient (or neutronclient) client are required'"
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_floating_ip_associate
|
||||
version_added: "1.2"
|
||||
short_description: Associate or disassociate a particular floating IP with an instance
|
||||
description:
|
||||
- Associates or disassociates a specific floating IP with a particular instance
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- the tenant name of the login user
|
||||
required: true
|
||||
default: true
|
||||
auth_url:
|
||||
description:
|
||||
- the keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- indicates the desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
instance_name:
|
||||
description:
|
||||
- name of the instance to which the public IP should be assigned
|
||||
required: true
|
||||
default: None
|
||||
ip_address:
|
||||
description:
|
||||
- floating ip that should be assigned to the instance
|
||||
required: true
|
||||
default: None
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Associate a specific floating IP with an Instance
|
||||
- quantum_floating_ip_associate:
|
||||
state=present
|
||||
login_username=admin
|
||||
login_password=admin
|
||||
login_tenant_name=admin
|
||||
ip_address=1.1.1.1
|
||||
instance_name=vm1
|
||||
'''
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s " % e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in connecting to neutron: %s " % e.message)
|
||||
return neutron
|
||||
|
||||
def _get_server_state(module, nova):
|
||||
server_info = None
|
||||
server = None
|
||||
try:
|
||||
for server in nova.servers.list():
|
||||
if server:
|
||||
info = server._info
|
||||
if info['name'] == module.params['instance_name']:
|
||||
if info['status'] != 'ACTIVE' and module.params['state'] == 'present':
|
||||
module.fail_json(msg="The VM is available but not Active. state:" + info['status'])
|
||||
server_info = info
|
||||
break
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting the server list: %s" % e.message)
|
||||
return server_info, server
|
||||
|
||||
def _get_port_id(neutron, module, instance_id):
|
||||
kwargs = dict(device_id = instance_id)
|
||||
try:
|
||||
ports = neutron.list_ports(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in listing ports: %s" % e.message)
|
||||
if not ports['ports']:
|
||||
return None
|
||||
return ports['ports'][0]['id']
|
||||
|
||||
def _get_floating_ip_id(module, neutron):
|
||||
kwargs = {
|
||||
'floating_ip_address': module.params['ip_address']
|
||||
}
|
||||
try:
|
||||
ips = neutron.list_floatingips(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "error in fetching the floatingips's %s" % e.message)
|
||||
if not ips['floatingips']:
|
||||
module.fail_json(msg = "Could find the ip specified in parameter, Please check")
|
||||
ip = ips['floatingips'][0]['id']
|
||||
if not ips['floatingips'][0]['port_id']:
|
||||
state = "detached"
|
||||
else:
|
||||
state = "attached"
|
||||
return state, ip
|
||||
|
||||
def _update_floating_ip(neutron, module, port_id, floating_ip_id):
|
||||
kwargs = {
|
||||
'port_id': port_id
|
||||
}
|
||||
try:
|
||||
result = neutron.update_floatingip(floating_ip_id, {'floatingip': kwargs})
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "There was an error in updating the floating ip address: %s" % e.message)
|
||||
module.exit_json(changed = True, result = result, public_ip=module.params['ip_address'])
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
ip_address = dict(required=True),
|
||||
instance_name = dict(required=True),
|
||||
state = dict(default='present', choices=['absent', 'present'])
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
try:
|
||||
nova = nova_client.Client(module.params['login_username'], module.params['login_password'],
|
||||
module.params['login_tenant_name'], module.params['auth_url'], service_type='compute')
|
||||
except Exception, e:
|
||||
module.fail_json( msg = " Error in authenticating to nova: %s" % e.message)
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
state, floating_ip_id = _get_floating_ip_id(module, neutron)
|
||||
if module.params['state'] == 'present':
|
||||
if state == 'attached':
|
||||
module.exit_json(changed = False, result = 'attached', public_ip=module.params['ip_address'])
|
||||
server_info, server_obj = _get_server_state(module, nova)
|
||||
if not server_info:
|
||||
module.fail_json(msg = " The instance name provided cannot be found")
|
||||
port_id = _get_port_id(neutron, module, server_info['id'])
|
||||
if not port_id:
|
||||
module.fail_json(msg = "Cannot find a port for this instance, maybe fixed ip is not assigned")
|
||||
_update_floating_ip(neutron, module, port_id, floating_ip_id)
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
if state == 'detached':
|
||||
module.exit_json(changed = False, result = 'detached')
|
||||
if state == 'attached':
|
||||
_update_floating_ip(neutron, module, None, floating_ip_id)
|
||||
module.exit_json(changed = True, result = "detached")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,279 +0,0 @@
|
||||
#!/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:
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'")
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_network
|
||||
version_added: "1.4"
|
||||
short_description: Creates/Removes networks from OpenStack
|
||||
description:
|
||||
- Add or Remove network from OpenStack.
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
tenant_name:
|
||||
description:
|
||||
- The name of the tenant for whom the network is created
|
||||
required: false
|
||||
default: None
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- Name to be assigned to the nework
|
||||
required: true
|
||||
default: None
|
||||
provider_network_type:
|
||||
description:
|
||||
- The type of the network to be created, gre, vlan, local. Available types depend on the plugin. The Quantum service decides if not specified.
|
||||
required: false
|
||||
default: None
|
||||
provider_physical_network:
|
||||
description:
|
||||
- The physical network which would realize the virtual network for flat and vlan networks.
|
||||
required: false
|
||||
default: None
|
||||
provider_segmentation_id:
|
||||
description:
|
||||
- The id that has to be assigned to the network, in case of vlan networks that would be vlan id and for gre the tunnel id
|
||||
required: false
|
||||
default: None
|
||||
router_external:
|
||||
description:
|
||||
- If 'yes', specifies that the virtual network is a external network (public).
|
||||
required: false
|
||||
default: false
|
||||
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
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a GRE backed Quantum network with tunnel id 1 for tenant1
|
||||
- quantum_network: name=t1network tenant_name=tenant1 state=present
|
||||
provider_network_type=gre provider_segmentation_id=1
|
||||
login_username=admin login_password=admin login_tenant_name=admin
|
||||
|
||||
# Create an external network
|
||||
- quantum_network: name=external_network state=present
|
||||
provider_network_type=local router_external=yes
|
||||
login_username=admin login_password=admin login_tenant_name=admin
|
||||
'''
|
||||
|
||||
_os_keystone = None
|
||||
_os_tenant_id = None
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s" %e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s " %e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = " Error in connecting to neutron: %s " %e.message)
|
||||
return neutron
|
||||
|
||||
def _set_tenant_id(module):
|
||||
global _os_tenant_id
|
||||
if not module.params['tenant_name']:
|
||||
tenant_name = module.params['login_tenant_name']
|
||||
else:
|
||||
tenant_name = module.params['tenant_name']
|
||||
|
||||
for tenant in _os_keystone.tenants.list():
|
||||
if tenant.name == tenant_name:
|
||||
_os_tenant_id = tenant.id
|
||||
break
|
||||
if not _os_tenant_id:
|
||||
module.fail_json(msg = "The tenant id cannot be found, please check the parameters")
|
||||
|
||||
|
||||
def _get_net_id(neutron, module):
|
||||
kwargs = {
|
||||
'tenant_id': _os_tenant_id,
|
||||
'name': module.params['name'],
|
||||
}
|
||||
try:
|
||||
networks = neutron.list_networks(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in listing neutron networks: %s" % e.message)
|
||||
if not networks['networks']:
|
||||
return None
|
||||
return networks['networks'][0]['id']
|
||||
|
||||
def _create_network(module, neutron):
|
||||
|
||||
neutron.format = 'json'
|
||||
|
||||
network = {
|
||||
'name': module.params.get('name'),
|
||||
'tenant_id': _os_tenant_id,
|
||||
'provider:network_type': module.params.get('provider_network_type'),
|
||||
'provider:physical_network': module.params.get('provider_physical_network'),
|
||||
'provider:segmentation_id': module.params.get('provider_segmentation_id'),
|
||||
'router:external': module.params.get('router_external'),
|
||||
'shared': module.params.get('shared'),
|
||||
'admin_state_up': module.params.get('admin_state_up'),
|
||||
}
|
||||
|
||||
if module.params['provider_network_type'] == 'local':
|
||||
network.pop('provider:physical_network', None)
|
||||
network.pop('provider:segmentation_id', None)
|
||||
|
||||
if module.params['provider_network_type'] == 'flat':
|
||||
network.pop('provider:segmentation_id', None)
|
||||
|
||||
if module.params['provider_network_type'] == 'gre':
|
||||
network.pop('provider:physical_network', None)
|
||||
|
||||
if module.params['provider_network_type'] is None:
|
||||
network.pop('provider:network_type', None)
|
||||
network.pop('provider:physical_network', None)
|
||||
network.pop('provider:segmentation_id', None)
|
||||
|
||||
try:
|
||||
net = neutron.create_network({'network':network})
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in creating network: %s" % e.message)
|
||||
return net['network']['id']
|
||||
|
||||
def _delete_network(module, net_id, neutron):
|
||||
|
||||
try:
|
||||
id = neutron.delete_network(net_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in deleting the network: %s" % e.message)
|
||||
return True
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name = dict(required=True),
|
||||
tenant_name = dict(default=None),
|
||||
provider_network_type = dict(default=None, choices=['local', 'vlan', 'flat', 'gre']),
|
||||
provider_physical_network = dict(default=None),
|
||||
provider_segmentation_id = dict(default=None),
|
||||
router_external = dict(default=False, type='bool'),
|
||||
shared = dict(default=False, type='bool'),
|
||||
admin_state_up = dict(default=True, type='bool'),
|
||||
state = dict(default='present', choices=['absent', 'present'])
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
if module.params['provider_network_type'] in ['vlan' , 'flat']:
|
||||
if not module.params['provider_physical_network']:
|
||||
module.fail_json(msg = " for vlan and flat networks, variable provider_physical_network should be set.")
|
||||
|
||||
if module.params['provider_network_type'] in ['vlan', 'gre']:
|
||||
if not module.params['provider_segmentation_id']:
|
||||
module.fail_json(msg = " for vlan & gre networks, variable provider_segmentation_id should be set.")
|
||||
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
|
||||
_set_tenant_id(module)
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
network_id = _get_net_id(neutron, module)
|
||||
if not network_id:
|
||||
network_id = _create_network(module, neutron)
|
||||
module.exit_json(changed = True, result = "Created", id = network_id)
|
||||
else:
|
||||
module.exit_json(changed = False, result = "Success", id = network_id)
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
network_id = _get_net_id(neutron, module)
|
||||
if not network_id:
|
||||
module.exit_json(changed = False, result = "Success")
|
||||
else:
|
||||
_delete_network(module, network_id, neutron)
|
||||
module.exit_json(changed = True, result = "Deleted")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,210 +0,0 @@
|
||||
#!/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:
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'")
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_router
|
||||
version_added: "1.2"
|
||||
short_description: Create or Remove router from openstack
|
||||
description:
|
||||
- Create or Delete routers from OpenStack
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone url for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- Name to be give to the router
|
||||
required: true
|
||||
default: None
|
||||
tenant_name:
|
||||
description:
|
||||
- Name of the tenant for which the router has to be created, if none router would be created for the login tenant.
|
||||
required: false
|
||||
default: None
|
||||
admin_state_up:
|
||||
description:
|
||||
- desired admin state of the created router .
|
||||
required: false
|
||||
default: true
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Creates a router for tenant admin
|
||||
- quantum_router: state=present
|
||||
login_username=admin
|
||||
login_password=admin
|
||||
login_tenant_name=admin
|
||||
name=router1"
|
||||
'''
|
||||
|
||||
_os_keystone = None
|
||||
_os_tenant_id = None
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s " % e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in connecting to neutron: %s " % e.message)
|
||||
return neutron
|
||||
|
||||
def _set_tenant_id(module):
|
||||
global _os_tenant_id
|
||||
if not module.params['tenant_name']:
|
||||
login_tenant_name = module.params['login_tenant_name']
|
||||
else:
|
||||
login_tenant_name = module.params['tenant_name']
|
||||
|
||||
for tenant in _os_keystone.tenants.list():
|
||||
if tenant.name == login_tenant_name:
|
||||
_os_tenant_id = tenant.id
|
||||
break
|
||||
if not _os_tenant_id:
|
||||
module.fail_json(msg = "The tenant id cannot be found, please check the parameters")
|
||||
|
||||
|
||||
def _get_router_id(module, neutron):
|
||||
kwargs = {
|
||||
'name': module.params['name'],
|
||||
'tenant_id': _os_tenant_id,
|
||||
}
|
||||
try:
|
||||
routers = neutron.list_routers(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting the router list: %s " % e.message)
|
||||
if not routers['routers']:
|
||||
return None
|
||||
return routers['routers'][0]['id']
|
||||
|
||||
def _create_router(module, neutron):
|
||||
router = {
|
||||
'name': module.params['name'],
|
||||
'tenant_id': _os_tenant_id,
|
||||
'admin_state_up': module.params['admin_state_up'],
|
||||
}
|
||||
try:
|
||||
new_router = neutron.create_router(dict(router=router))
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in creating router: %s" % e.message)
|
||||
return new_router['router']['id']
|
||||
|
||||
def _delete_router(module, neutron, router_id):
|
||||
try:
|
||||
neutron.delete_router(router_id)
|
||||
except:
|
||||
module.fail_json("Error in deleting the router")
|
||||
return True
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name = dict(required=True),
|
||||
tenant_name = dict(default=None),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
admin_state_up = dict(type='bool', default=True),
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
_set_tenant_id(module)
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
router_id = _get_router_id(module, neutron)
|
||||
if not router_id:
|
||||
router_id = _create_router(module, neutron)
|
||||
module.exit_json(changed=True, result="Created", id=router_id)
|
||||
else:
|
||||
module.exit_json(changed=False, result="success" , id=router_id)
|
||||
|
||||
else:
|
||||
router_id = _get_router_id(module, neutron)
|
||||
if not router_id:
|
||||
module.exit_json(changed=False, result="success")
|
||||
else:
|
||||
_delete_router(module, neutron, router_id)
|
||||
module.exit_json(changed=True, result="deleted")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,213 +0,0 @@
|
||||
#!/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:
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'")
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_router_gateway
|
||||
version_added: "1.2"
|
||||
short_description: set/unset a gateway interface for the router with the specified external network
|
||||
description:
|
||||
- Creates/Removes a gateway interface from the router, used to associate a external network with a router to route external traffic.
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone URL for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
router_name:
|
||||
description:
|
||||
- Name of the router to which the gateway should be attached.
|
||||
required: true
|
||||
default: None
|
||||
network_name:
|
||||
description:
|
||||
- Name of the external network which should be attached to the router.
|
||||
required: true
|
||||
default: None
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Attach an external network with a router to allow flow of external traffic
|
||||
- quantum_router_gateway: state=present login_username=admin login_password=admin
|
||||
login_tenant_name=admin router_name=external_router
|
||||
network_name=external_network
|
||||
'''
|
||||
|
||||
_os_keystone = None
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s " % e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in connecting to neutron: %s " % e.message)
|
||||
return neutron
|
||||
|
||||
def _get_router_id(module, neutron):
|
||||
kwargs = {
|
||||
'name': module.params['router_name'],
|
||||
}
|
||||
try:
|
||||
routers = neutron.list_routers(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting the router list: %s " % e.message)
|
||||
if not routers['routers']:
|
||||
return None
|
||||
return routers['routers'][0]['id']
|
||||
|
||||
def _get_net_id(neutron, module):
|
||||
kwargs = {
|
||||
'name': module.params['network_name'],
|
||||
'router:external': True
|
||||
}
|
||||
try:
|
||||
networks = neutron.list_networks(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json("Error in listing neutron networks: %s" % e.message)
|
||||
if not networks['networks']:
|
||||
return None
|
||||
return networks['networks'][0]['id']
|
||||
|
||||
def _get_port_id(neutron, module, router_id, network_id):
|
||||
kwargs = {
|
||||
'device_id': router_id,
|
||||
'network_id': network_id,
|
||||
}
|
||||
try:
|
||||
ports = neutron.list_ports(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in listing ports: %s" % e.message)
|
||||
if not ports['ports']:
|
||||
return None
|
||||
return ports['ports'][0]['id']
|
||||
|
||||
def _add_gateway_router(neutron, module, router_id, network_id):
|
||||
kwargs = {
|
||||
'network_id': network_id
|
||||
}
|
||||
try:
|
||||
neutron.add_gateway_router(router_id, kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in adding gateway to router: %s" % e.message)
|
||||
return True
|
||||
|
||||
def _remove_gateway_router(neutron, module, router_id):
|
||||
try:
|
||||
neutron.remove_gateway_router(router_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in removing gateway to router: %s" % e.message)
|
||||
return True
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
router_name = dict(required=True),
|
||||
network_name = dict(required=True),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
router_id = _get_router_id(module, neutron)
|
||||
|
||||
if not router_id:
|
||||
module.fail_json(msg="failed to get the router id, please check the router name")
|
||||
|
||||
network_id = _get_net_id(neutron, module)
|
||||
if not network_id:
|
||||
module.fail_json(msg="failed to get the network id, please check the network name and make sure it is external")
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
port_id = _get_port_id(neutron, module, router_id, network_id)
|
||||
if not port_id:
|
||||
_add_gateway_router(neutron, module, router_id, network_id)
|
||||
module.exit_json(changed=True, result="created")
|
||||
module.exit_json(changed=False, result="success")
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
port_id = _get_port_id(neutron, module, router_id, network_id)
|
||||
if not port_id:
|
||||
module.exit_json(changed=False, result="Success")
|
||||
_remove_gateway_router(neutron, module, router_id)
|
||||
module.exit_json(changed=True, result="Deleted")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,249 +0,0 @@
|
||||
#!/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:
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'")
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_router_interface
|
||||
version_added: "1.2"
|
||||
short_description: Attach/Dettach a subnet's interface to a router
|
||||
description:
|
||||
- Attach/Dettach a subnet interface to a router, to provide a gateway for the subnet.
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: 'yes'
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone URL for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
router_name:
|
||||
description:
|
||||
- Name of the router to which the subnet's interface should be attached.
|
||||
required: true
|
||||
default: None
|
||||
subnet_name:
|
||||
description:
|
||||
- Name of the subnet to whose interface should be attached to the router.
|
||||
required: true
|
||||
default: None
|
||||
tenant_name:
|
||||
description:
|
||||
- Name of the tenant whose subnet has to be attached.
|
||||
required: false
|
||||
default: None
|
||||
requirements: ["quantumclient", "keystoneclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Attach tenant1's subnet to the external router
|
||||
- quantum_router_interface: state=present login_username=admin
|
||||
login_password=admin
|
||||
login_tenant_name=admin
|
||||
tenant_name=tenant1
|
||||
router_name=external_route
|
||||
subnet_name=t1subnet
|
||||
'''
|
||||
|
||||
|
||||
_os_keystone = None
|
||||
_os_tenant_id = None
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s " % e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in connecting to neutron: %s " % e.message)
|
||||
return neutron
|
||||
|
||||
def _set_tenant_id(module):
|
||||
global _os_tenant_id
|
||||
if not module.params['tenant_name']:
|
||||
login_tenant_name = module.params['login_tenant_name']
|
||||
else:
|
||||
login_tenant_name = module.params['tenant_name']
|
||||
|
||||
for tenant in _os_keystone.tenants.list():
|
||||
if tenant.name == login_tenant_name:
|
||||
_os_tenant_id = tenant.id
|
||||
break
|
||||
if not _os_tenant_id:
|
||||
module.fail_json(msg = "The tenant id cannot be found, please check the parameters")
|
||||
|
||||
|
||||
def _get_router_id(module, neutron):
|
||||
kwargs = {
|
||||
'name': module.params['router_name'],
|
||||
}
|
||||
try:
|
||||
routers = neutron.list_routers(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in getting the router list: %s " % e.message)
|
||||
if not routers['routers']:
|
||||
return None
|
||||
return routers['routers'][0]['id']
|
||||
|
||||
|
||||
def _get_subnet_id(module, neutron):
|
||||
subnet_id = None
|
||||
kwargs = {
|
||||
'tenant_id': _os_tenant_id,
|
||||
'name': module.params['subnet_name'],
|
||||
}
|
||||
try:
|
||||
subnets = neutron.list_subnets(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = " Error in getting the subnet list:%s " % e.message)
|
||||
if not subnets['subnets']:
|
||||
return None
|
||||
return subnets['subnets'][0]['id']
|
||||
|
||||
def _get_port_id(neutron, module, router_id, subnet_id):
|
||||
kwargs = {
|
||||
'tenant_id': _os_tenant_id,
|
||||
'device_id': router_id,
|
||||
}
|
||||
try:
|
||||
ports = neutron.list_ports(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in listing ports: %s" % e.message)
|
||||
if not ports['ports']:
|
||||
return None
|
||||
for port in ports['ports']:
|
||||
for subnet in port['fixed_ips']:
|
||||
if subnet['subnet_id'] == subnet_id:
|
||||
return port['id']
|
||||
return None
|
||||
|
||||
def _add_interface_router(neutron, module, router_id, subnet_id):
|
||||
kwargs = {
|
||||
'subnet_id': subnet_id
|
||||
}
|
||||
try:
|
||||
neutron.add_interface_router(router_id, kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error in adding interface to router: %s" % e.message)
|
||||
return True
|
||||
|
||||
def _remove_interface_router(neutron, module, router_id, subnet_id):
|
||||
kwargs = {
|
||||
'subnet_id': subnet_id
|
||||
}
|
||||
try:
|
||||
neutron.remove_interface_router(router_id, kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Error in removing interface from router: %s" % e.message)
|
||||
return True
|
||||
|
||||
def main():
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
router_name = dict(required=True),
|
||||
subnet_name = dict(required=True),
|
||||
tenant_name = dict(default=None),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
_set_tenant_id(module)
|
||||
|
||||
router_id = _get_router_id(module, neutron)
|
||||
if not router_id:
|
||||
module.fail_json(msg="failed to get the router id, please check the router name")
|
||||
|
||||
subnet_id = _get_subnet_id(module, neutron)
|
||||
if not subnet_id:
|
||||
module.fail_json(msg="failed to get the subnet id, please check the subnet name")
|
||||
|
||||
if module.params['state'] == 'present':
|
||||
port_id = _get_port_id(neutron, module, router_id, subnet_id)
|
||||
if not port_id:
|
||||
_add_interface_router(neutron, module, router_id, subnet_id)
|
||||
module.exit_json(changed=True, result="created", id=port_id)
|
||||
module.exit_json(changed=False, result="success", id=port_id)
|
||||
|
||||
if module.params['state'] == 'absent':
|
||||
port_id = _get_port_id(neutron, module, router_id, subnet_id)
|
||||
if not port_id:
|
||||
module.exit_json(changed = False, result = "Success")
|
||||
_remove_interface_router(neutron, module, router_id, subnet_id)
|
||||
module.exit_json(changed=True, result="Deleted")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,291 +0,0 @@
|
||||
#!/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:
|
||||
try:
|
||||
from neutronclient.neutron import client
|
||||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystoneclient are required'")
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: quantum_subnet
|
||||
version_added: "1.2"
|
||||
short_description: Add/remove subnet from a network
|
||||
description:
|
||||
- Add/remove subnet from a network
|
||||
options:
|
||||
login_username:
|
||||
description:
|
||||
- login username to authenticate to keystone
|
||||
required: true
|
||||
default: admin
|
||||
login_password:
|
||||
description:
|
||||
- Password of login user
|
||||
required: true
|
||||
default: True
|
||||
login_tenant_name:
|
||||
description:
|
||||
- The tenant name of the login user
|
||||
required: true
|
||||
default: True
|
||||
auth_url:
|
||||
description:
|
||||
- The keystone URL for authentication
|
||||
required: false
|
||||
default: 'http://127.0.0.1:35357/v2.0/'
|
||||
region_name:
|
||||
description:
|
||||
- Name of the region
|
||||
required: false
|
||||
default: None
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
network_name:
|
||||
description:
|
||||
- Name of the network to which the subnet should be attached
|
||||
required: true
|
||||
default: None
|
||||
name:
|
||||
description:
|
||||
- The name of the subnet that should be created
|
||||
required: true
|
||||
default: None
|
||||
cidr:
|
||||
description:
|
||||
- The CIDR representation of the subnet that should be assigned to the subnet
|
||||
required: true
|
||||
default: None
|
||||
tenant_name:
|
||||
description:
|
||||
- The name of the tenant for whom the subnet should be created
|
||||
required: false
|
||||
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:
|
||||
- DNS nameservers for this subnet, comma-separated
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.4"
|
||||
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
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a subnet for a tenant with the specified subnet
|
||||
- quantum_subnet: state=present login_username=admin login_password=admin
|
||||
login_tenant_name=admin tenant_name=tenant1
|
||||
network_name=network1 name=net1subnet cidr=192.168.0.0/24"
|
||||
'''
|
||||
|
||||
_os_keystone = None
|
||||
_os_tenant_id = None
|
||||
_os_network_id = None
|
||||
|
||||
def _get_ksclient(module, kwargs):
|
||||
try:
|
||||
kclient = ksclient.Client(username=kwargs.get('login_username'),
|
||||
password=kwargs.get('login_password'),
|
||||
tenant_name=kwargs.get('login_tenant_name'),
|
||||
auth_url=kwargs.get('auth_url'))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error authenticating to the keystone: %s" %e.message)
|
||||
global _os_keystone
|
||||
_os_keystone = kclient
|
||||
return kclient
|
||||
|
||||
|
||||
def _get_endpoint(module, ksclient):
|
||||
try:
|
||||
endpoint = ksclient.service_catalog.url_for(service_type='network', endpoint_type='publicURL')
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Error getting network endpoint: %s" % e.message)
|
||||
return endpoint
|
||||
|
||||
def _get_neutron_client(module, kwargs):
|
||||
_ksclient = _get_ksclient(module, kwargs)
|
||||
token = _ksclient.auth_token
|
||||
endpoint = _get_endpoint(module, _ksclient)
|
||||
kwargs = {
|
||||
'token': token,
|
||||
'endpoint_url': endpoint
|
||||
}
|
||||
try:
|
||||
neutron = client.Client('2.0', **kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg = " Error in connecting to neutron: %s" % e.message)
|
||||
return neutron
|
||||
|
||||
def _set_tenant_id(module):
|
||||
global _os_tenant_id
|
||||
if not module.params['tenant_name']:
|
||||
tenant_name = module.params['login_tenant_name']
|
||||
else:
|
||||
tenant_name = module.params['tenant_name']
|
||||
|
||||
for tenant in _os_keystone.tenants.list():
|
||||
if tenant.name == tenant_name:
|
||||
_os_tenant_id = tenant.id
|
||||
break
|
||||
if not _os_tenant_id:
|
||||
module.fail_json(msg = "The tenant id cannot be found, please check the parameters")
|
||||
|
||||
def _get_net_id(neutron, module):
|
||||
kwargs = {
|
||||
'tenant_id': _os_tenant_id,
|
||||
'name': module.params['network_name'],
|
||||
}
|
||||
try:
|
||||
networks = neutron.list_networks(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json("Error in listing neutron networks: %s" % e.message)
|
||||
if not networks['networks']:
|
||||
return None
|
||||
return networks['networks'][0]['id']
|
||||
|
||||
|
||||
def _get_subnet_id(module, neutron):
|
||||
global _os_network_id
|
||||
subnet_id = None
|
||||
_os_network_id = _get_net_id(neutron, module)
|
||||
if not _os_network_id:
|
||||
module.fail_json(msg = "network id of network not found.")
|
||||
else:
|
||||
kwargs = {
|
||||
'tenant_id': _os_tenant_id,
|
||||
'name': module.params['name'],
|
||||
}
|
||||
try:
|
||||
subnets = neutron.list_subnets(**kwargs)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = " Error in getting the subnet list:%s " % e.message)
|
||||
if not subnets['subnets']:
|
||||
return None
|
||||
return subnets['subnets'][0]['id']
|
||||
|
||||
def _create_subnet(module, neutron):
|
||||
neutron.format = 'json'
|
||||
subnet = {
|
||||
'name': module.params['name'],
|
||||
'ip_version': module.params['ip_version'],
|
||||
'enable_dhcp': module.params['enable_dhcp'],
|
||||
'tenant_id': _os_tenant_id,
|
||||
'gateway_ip': module.params['gateway_ip'],
|
||||
'dns_nameservers': module.params['dns_nameservers'],
|
||||
'network_id': _os_network_id,
|
||||
'cidr': module.params['cidr'],
|
||||
}
|
||||
if module.params['allocation_pool_start'] and module.params['allocation_pool_end']:
|
||||
allocation_pools = [
|
||||
{
|
||||
'start' : module.params['allocation_pool_start'],
|
||||
'end' : module.params['allocation_pool_end']
|
||||
}
|
||||
]
|
||||
subnet.update({'allocation_pools': allocation_pools})
|
||||
if not module.params['gateway_ip']:
|
||||
subnet.pop('gateway_ip')
|
||||
if module.params['dns_nameservers']:
|
||||
subnet['dns_nameservers'] = module.params['dns_nameservers'].split(',')
|
||||
else:
|
||||
subnet.pop('dns_nameservers')
|
||||
try:
|
||||
new_subnet = neutron.create_subnet(dict(subnet=subnet))
|
||||
except Exception, e:
|
||||
module.fail_json(msg = "Failure in creating subnet: %s" % e.message)
|
||||
return new_subnet['subnet']['id']
|
||||
|
||||
|
||||
def _delete_subnet(module, neutron, subnet_id):
|
||||
try:
|
||||
neutron.delete_subnet(subnet_id)
|
||||
except Exception, e:
|
||||
module.fail_json( msg = "Error in deleting subnet: %s" % e.message)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
name = dict(required=True),
|
||||
network_name = dict(required=True),
|
||||
cidr = dict(required=True),
|
||||
tenant_name = dict(default=None),
|
||||
state = dict(default='present', choices=['absent', 'present']),
|
||||
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),
|
||||
allocation_pool_start = dict(default=None),
|
||||
allocation_pool_end = dict(default=None),
|
||||
))
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
neutron = _get_neutron_client(module, module.params)
|
||||
_set_tenant_id(module)
|
||||
if module.params['state'] == 'present':
|
||||
subnet_id = _get_subnet_id(module, neutron)
|
||||
if not subnet_id:
|
||||
subnet_id = _create_subnet(module, neutron)
|
||||
module.exit_json(changed = True, result = "Created" , id = subnet_id)
|
||||
else:
|
||||
module.exit_json(changed = False, result = "success" , id = subnet_id)
|
||||
else:
|
||||
subnet_id = _get_subnet_id(module, neutron)
|
||||
if not subnet_id:
|
||||
module.exit_json(changed = False, result = "success")
|
||||
else:
|
||||
_delete_subnet(module, neutron, subnet_id)
|
||||
module.exit_json(changed = True, result = "deleted")
|
||||
|
||||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
|
@ -1,711 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax
|
||||
short_description: create / delete an instance in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud instance and optionally
|
||||
waits for it to be 'running'.
|
||||
version_added: "1.2"
|
||||
options:
|
||||
auto_increment:
|
||||
description:
|
||||
- Whether or not to increment a single number with the name of the
|
||||
created servers. Only applicable when used with the I(group) attribute
|
||||
or meta key.
|
||||
default: yes
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
version_added: 1.5
|
||||
config_drive:
|
||||
description:
|
||||
- Attach read-only configuration drive to server as label config-2
|
||||
default: no
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
version_added: 1.7
|
||||
count:
|
||||
description:
|
||||
- number of instances to launch
|
||||
default: 1
|
||||
version_added: 1.4
|
||||
count_offset:
|
||||
description:
|
||||
- number count to start at
|
||||
default: 1
|
||||
version_added: 1.4
|
||||
disk_config:
|
||||
description:
|
||||
- Disk partitioning strategy
|
||||
choices:
|
||||
- auto
|
||||
- manual
|
||||
version_added: '1.4'
|
||||
default: auto
|
||||
exact_count:
|
||||
description:
|
||||
- Explicitly ensure an exact count of instances, used with
|
||||
state=active/present
|
||||
default: no
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
version_added: 1.4
|
||||
extra_client_args:
|
||||
description:
|
||||
- A hash of key/value pairs to be used when creating the cloudservers
|
||||
client. This is considered an advanced option, use it wisely and
|
||||
with caution.
|
||||
version_added: 1.6
|
||||
extra_create_args:
|
||||
description:
|
||||
- A hash of key/value pairs to be used when creating a new server.
|
||||
This is considered an advanced option, use it wisely and with caution.
|
||||
version_added: 1.6
|
||||
files:
|
||||
description:
|
||||
- Files to insert into the instance. remotefilename:localcontent
|
||||
default: null
|
||||
flavor:
|
||||
description:
|
||||
- flavor to use for the instance
|
||||
default: null
|
||||
group:
|
||||
description:
|
||||
- host group to assign to server, is also used for idempotent operations
|
||||
to ensure a specific number of instances
|
||||
version_added: 1.4
|
||||
image:
|
||||
description:
|
||||
- image to use for the instance. Can be an C(id), C(human_id) or C(name)
|
||||
default: null
|
||||
instance_ids:
|
||||
description:
|
||||
- list of instance ids, currently only used when state='absent' to
|
||||
remove instances
|
||||
version_added: 1.4
|
||||
key_name:
|
||||
description:
|
||||
- key pair to use on the instance
|
||||
default: null
|
||||
aliases:
|
||||
- keypair
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- Name to give the instance
|
||||
default: null
|
||||
networks:
|
||||
description:
|
||||
- The network to attach to the instances. If specified, you must include
|
||||
ALL networks including the public and private interfaces. Can be C(id)
|
||||
or C(label).
|
||||
default:
|
||||
- public
|
||||
- private
|
||||
version_added: 1.4
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
user_data:
|
||||
description:
|
||||
- Data to be uploaded to the servers config drive. This option implies
|
||||
I(config_drive). Can be a file path or a string
|
||||
version_added: 1.7
|
||||
wait:
|
||||
description:
|
||||
- wait for the instance to be in state 'running' before returning
|
||||
default: "no"
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: Jesse Keating, Matt Martz
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Cloud Server
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Server build request
|
||||
local_action:
|
||||
module: rax
|
||||
credentials: ~/.raxpub
|
||||
name: rax-test1
|
||||
flavor: 5
|
||||
image: b11d9567-e412-4255-96b9-bd63ab23bcfe
|
||||
key_name: my_rackspace_key
|
||||
files:
|
||||
/root/test.txt: /home/localuser/test.txt
|
||||
wait: yes
|
||||
state: present
|
||||
networks:
|
||||
- private
|
||||
- public
|
||||
register: rax
|
||||
|
||||
- name: Build an exact count of cloud servers with incremented names
|
||||
hosts: local
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Server build requests
|
||||
local_action:
|
||||
module: rax
|
||||
credentials: ~/.raxpub
|
||||
name: test%03d.example.org
|
||||
flavor: performance1-1
|
||||
image: ubuntu-1204-lts-precise-pangolin
|
||||
state: present
|
||||
count: 10
|
||||
count_offset: 10
|
||||
exact_count: yes
|
||||
group: test
|
||||
wait: yes
|
||||
register: rax
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def create(module, names=[], flavor=None, image=None, meta={}, key_name=None,
|
||||
files={}, wait=True, wait_timeout=300, disk_config=None,
|
||||
group=None, nics=[], extra_create_args={}, user_data=None,
|
||||
config_drive=False, existing=[]):
|
||||
cs = pyrax.cloudservers
|
||||
changed = False
|
||||
|
||||
if user_data:
|
||||
config_drive = True
|
||||
|
||||
if user_data and os.path.isfile(user_data):
|
||||
try:
|
||||
f = open(user_data)
|
||||
user_data = f.read()
|
||||
f.close()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='Failed to load %s' % user_data)
|
||||
|
||||
# Handle the file contents
|
||||
for rpath in files.keys():
|
||||
lpath = os.path.expanduser(files[rpath])
|
||||
try:
|
||||
fileobj = open(lpath, 'r')
|
||||
files[rpath] = fileobj.read()
|
||||
fileobj.close()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='Failed to load %s' % lpath)
|
||||
try:
|
||||
servers = []
|
||||
for name in names:
|
||||
servers.append(cs.servers.create(name=name, image=image,
|
||||
flavor=flavor, meta=meta,
|
||||
key_name=key_name,
|
||||
files=files, nics=nics,
|
||||
disk_config=disk_config,
|
||||
config_drive=config_drive,
|
||||
userdata=user_data,
|
||||
**extra_create_args))
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
if wait:
|
||||
end_time = time.time() + wait_timeout
|
||||
infinite = wait_timeout == 0
|
||||
while infinite or time.time() < end_time:
|
||||
for server in servers:
|
||||
try:
|
||||
server.get()
|
||||
except:
|
||||
server.status == 'ERROR'
|
||||
|
||||
if not filter(lambda s: s.status not in FINAL_STATUSES,
|
||||
servers):
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
success = []
|
||||
error = []
|
||||
timeout = []
|
||||
for server in servers:
|
||||
try:
|
||||
server.get()
|
||||
except:
|
||||
server.status == 'ERROR'
|
||||
instance = rax_to_dict(server, 'server')
|
||||
if server.status == 'ACTIVE' or not wait:
|
||||
success.append(instance)
|
||||
elif server.status == 'ERROR':
|
||||
error.append(instance)
|
||||
elif wait:
|
||||
timeout.append(instance)
|
||||
|
||||
untouched = [rax_to_dict(s, 'server') for s in existing]
|
||||
instances = success + untouched
|
||||
|
||||
results = {
|
||||
'changed': changed,
|
||||
'action': 'create',
|
||||
'instances': instances,
|
||||
'success': success,
|
||||
'error': error,
|
||||
'timeout': timeout,
|
||||
'instance_ids': {
|
||||
'instances': [i['id'] for i in instances],
|
||||
'success': [i['id'] for i in success],
|
||||
'error': [i['id'] for i in error],
|
||||
'timeout': [i['id'] for i in timeout]
|
||||
}
|
||||
}
|
||||
|
||||
if timeout:
|
||||
results['msg'] = 'Timeout waiting for all servers to build'
|
||||
elif error:
|
||||
results['msg'] = 'Failed to build all servers'
|
||||
|
||||
if 'msg' in results:
|
||||
module.fail_json(**results)
|
||||
else:
|
||||
module.exit_json(**results)
|
||||
|
||||
|
||||
def delete(module, instance_ids=[], wait=True, wait_timeout=300, kept=[]):
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
changed = False
|
||||
instances = {}
|
||||
servers = []
|
||||
|
||||
for instance_id in instance_ids:
|
||||
servers.append(cs.servers.get(instance_id))
|
||||
|
||||
for server in servers:
|
||||
try:
|
||||
server.delete()
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
instance = rax_to_dict(server, 'server')
|
||||
instances[instance['id']] = instance
|
||||
|
||||
# If requested, wait for server deletion
|
||||
if wait:
|
||||
end_time = time.time() + wait_timeout
|
||||
infinite = wait_timeout == 0
|
||||
while infinite or time.time() < end_time:
|
||||
for server in servers:
|
||||
instance_id = server.id
|
||||
try:
|
||||
server.get()
|
||||
except:
|
||||
instances[instance_id]['status'] = 'DELETED'
|
||||
instances[instance_id]['rax_status'] = 'DELETED'
|
||||
|
||||
if not filter(lambda s: s['status'] not in ('', 'DELETED',
|
||||
'ERROR'),
|
||||
instances.values()):
|
||||
break
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
timeout = filter(lambda s: s['status'] not in ('', 'DELETED', 'ERROR'),
|
||||
instances.values())
|
||||
error = filter(lambda s: s['status'] in ('ERROR'),
|
||||
instances.values())
|
||||
success = filter(lambda s: s['status'] in ('', 'DELETED'),
|
||||
instances.values())
|
||||
|
||||
instances = [rax_to_dict(s, 'server') for s in kept]
|
||||
|
||||
results = {
|
||||
'changed': changed,
|
||||
'action': 'delete',
|
||||
'instances': instances,
|
||||
'success': success,
|
||||
'error': error,
|
||||
'timeout': timeout,
|
||||
'instance_ids': {
|
||||
'instances': [i['id'] for i in instances],
|
||||
'success': [i['id'] for i in success],
|
||||
'error': [i['id'] for i in error],
|
||||
'timeout': [i['id'] for i in timeout]
|
||||
}
|
||||
}
|
||||
|
||||
if timeout:
|
||||
results['msg'] = 'Timeout waiting for all servers to delete'
|
||||
elif error:
|
||||
results['msg'] = 'Failed to delete all servers'
|
||||
|
||||
if 'msg' in results:
|
||||
module.fail_json(**results)
|
||||
else:
|
||||
module.exit_json(**results)
|
||||
|
||||
|
||||
def cloudservers(module, state=None, name=None, flavor=None, image=None,
|
||||
meta={}, key_name=None, files={}, wait=True, wait_timeout=300,
|
||||
disk_config=None, count=1, group=None, instance_ids=[],
|
||||
exact_count=False, networks=[], count_offset=0,
|
||||
auto_increment=False, extra_create_args={}, user_data=None,
|
||||
config_drive=False):
|
||||
cs = pyrax.cloudservers
|
||||
cnw = pyrax.cloud_networks
|
||||
if not cnw:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
servers = []
|
||||
|
||||
# Add the group meta key
|
||||
if group and 'group' not in meta:
|
||||
meta['group'] = group
|
||||
elif 'group' in meta and group is None:
|
||||
group = meta['group']
|
||||
|
||||
# Normalize and ensure all metadata values are strings
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, list):
|
||||
meta[k] = ','.join(['%s' % i for i in v])
|
||||
elif isinstance(v, dict):
|
||||
meta[k] = json.dumps(v)
|
||||
elif not isinstance(v, basestring):
|
||||
meta[k] = '%s' % v
|
||||
|
||||
# When using state=absent with group, the absent block won't match the
|
||||
# names properly. Use the exact_count functionality to decrease the count
|
||||
# to the desired level
|
||||
was_absent = False
|
||||
if group is not None and state == 'absent':
|
||||
exact_count = True
|
||||
state = 'present'
|
||||
was_absent = True
|
||||
|
||||
if image:
|
||||
image = rax_find_image(module, pyrax, image)
|
||||
|
||||
nics = []
|
||||
if networks:
|
||||
for network in networks:
|
||||
nics.extend(rax_find_network(module, pyrax, network))
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
for arg, value in dict(name=name, flavor=flavor,
|
||||
image=image).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax" module' %
|
||||
arg)
|
||||
|
||||
# Idempotent ensurance of a specific count of servers
|
||||
if exact_count is not False:
|
||||
# See if we can find servers that match our options
|
||||
if group is None:
|
||||
module.fail_json(msg='"group" must be provided when using '
|
||||
'"exact_count"')
|
||||
else:
|
||||
if auto_increment:
|
||||
numbers = set()
|
||||
|
||||
try:
|
||||
name % 0
|
||||
except TypeError, e:
|
||||
if e.message.startswith('not all'):
|
||||
name = '%s%%d' % name
|
||||
else:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
pattern = re.sub(r'%\d*[sd]', r'(\d+)', name)
|
||||
for server in cs.servers.list():
|
||||
if server.metadata.get('group') == group:
|
||||
servers.append(server)
|
||||
match = re.search(pattern, server.name)
|
||||
if match:
|
||||
number = int(match.group(1))
|
||||
numbers.add(number)
|
||||
|
||||
number_range = xrange(count_offset, count_offset + count)
|
||||
available_numbers = list(set(number_range)
|
||||
.difference(numbers))
|
||||
else:
|
||||
for server in cs.servers.list():
|
||||
if server.metadata.get('group') == group:
|
||||
servers.append(server)
|
||||
|
||||
# If state was absent but the count was changed,
|
||||
# assume we only wanted to remove that number of instances
|
||||
if was_absent:
|
||||
diff = len(servers) - count
|
||||
if diff < 0:
|
||||
count = 0
|
||||
else:
|
||||
count = diff
|
||||
|
||||
if len(servers) > count:
|
||||
state = 'absent'
|
||||
kept = servers[:count]
|
||||
del servers[:count]
|
||||
instance_ids = []
|
||||
for server in servers:
|
||||
instance_ids.append(server.id)
|
||||
delete(module, instance_ids=instance_ids, wait=wait,
|
||||
wait_timeout=wait_timeout, kept=kept)
|
||||
elif len(servers) < count:
|
||||
if auto_increment:
|
||||
names = []
|
||||
name_slice = count - len(servers)
|
||||
numbers_to_use = available_numbers[:name_slice]
|
||||
for number in numbers_to_use:
|
||||
names.append(name % number)
|
||||
else:
|
||||
names = [name] * (count - len(servers))
|
||||
else:
|
||||
instances = []
|
||||
instance_ids = []
|
||||
for server in servers:
|
||||
instances.append(rax_to_dict(server, 'server'))
|
||||
instance_ids.append(server.id)
|
||||
module.exit_json(changed=False, action=None,
|
||||
instances=instances,
|
||||
success=[], error=[], timeout=[],
|
||||
instance_ids={'instances': instance_ids,
|
||||
'success': [], 'error': [],
|
||||
'timeout': []})
|
||||
else:
|
||||
if group is not None:
|
||||
if auto_increment:
|
||||
numbers = set()
|
||||
|
||||
try:
|
||||
name % 0
|
||||
except TypeError, e:
|
||||
if e.message.startswith('not all'):
|
||||
name = '%s%%d' % name
|
||||
else:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
pattern = re.sub(r'%\d*[sd]', r'(\d+)', name)
|
||||
for server in cs.servers.list():
|
||||
if server.metadata.get('group') == group:
|
||||
servers.append(server)
|
||||
match = re.search(pattern, server.name)
|
||||
if match:
|
||||
number = int(match.group(1))
|
||||
numbers.add(number)
|
||||
|
||||
number_range = xrange(count_offset,
|
||||
count_offset + count + len(numbers))
|
||||
available_numbers = list(set(number_range)
|
||||
.difference(numbers))
|
||||
names = []
|
||||
numbers_to_use = available_numbers[:count]
|
||||
for number in numbers_to_use:
|
||||
names.append(name % number)
|
||||
else:
|
||||
names = [name] * count
|
||||
else:
|
||||
search_opts = {
|
||||
'name': '^%s$' % name,
|
||||
'image': image,
|
||||
'flavor': flavor
|
||||
}
|
||||
servers = []
|
||||
for server in cs.servers.list(search_opts=search_opts):
|
||||
if server.metadata != meta:
|
||||
continue
|
||||
servers.append(server)
|
||||
|
||||
if len(servers) >= count:
|
||||
instances = []
|
||||
for server in servers:
|
||||
instances.append(rax_to_dict(server, 'server'))
|
||||
|
||||
instance_ids = [i['id'] for i in instances]
|
||||
module.exit_json(changed=False, action=None,
|
||||
instances=instances, success=[], error=[],
|
||||
timeout=[],
|
||||
instance_ids={'instances': instance_ids,
|
||||
'success': [], 'error': [],
|
||||
'timeout': []})
|
||||
|
||||
names = [name] * (count - len(servers))
|
||||
|
||||
create(module, names=names, flavor=flavor, image=image,
|
||||
meta=meta, key_name=key_name, files=files, wait=wait,
|
||||
wait_timeout=wait_timeout, disk_config=disk_config, group=group,
|
||||
nics=nics, extra_create_args=extra_create_args,
|
||||
user_data=user_data, config_drive=config_drive,
|
||||
existing=servers)
|
||||
|
||||
elif state == 'absent':
|
||||
if instance_ids is None:
|
||||
for arg, value in dict(name=name, flavor=flavor,
|
||||
image=image).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax" '
|
||||
'module' % arg)
|
||||
search_opts = {
|
||||
'name': '^%s$' % name,
|
||||
'image': image,
|
||||
'flavor': flavor
|
||||
}
|
||||
for server in cs.servers.list(search_opts=search_opts):
|
||||
if meta != server.metadata:
|
||||
continue
|
||||
servers.append(server)
|
||||
|
||||
instance_ids = []
|
||||
for server in servers:
|
||||
if len(instance_ids) < count:
|
||||
instance_ids.append(server.id)
|
||||
else:
|
||||
break
|
||||
|
||||
if not instance_ids:
|
||||
module.exit_json(changed=False, action=None, instances=[],
|
||||
success=[], error=[], timeout=[],
|
||||
instance_ids={'instances': [],
|
||||
'success': [], 'error': [],
|
||||
'timeout': []})
|
||||
|
||||
delete(module, instance_ids=instance_ids, wait=wait,
|
||||
wait_timeout=wait_timeout)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
auto_increment=dict(default=True, type='bool'),
|
||||
config_drive=dict(default=False, type='bool'),
|
||||
count=dict(default=1, type='int'),
|
||||
count_offset=dict(default=1, type='int'),
|
||||
disk_config=dict(choices=['auto', 'manual']),
|
||||
exact_count=dict(default=False, type='bool'),
|
||||
extra_client_args=dict(type='dict', default={}),
|
||||
extra_create_args=dict(type='dict', default={}),
|
||||
files=dict(type='dict', default={}),
|
||||
flavor=dict(),
|
||||
group=dict(),
|
||||
image=dict(),
|
||||
instance_ids=dict(type='list'),
|
||||
key_name=dict(aliases=['keypair']),
|
||||
meta=dict(type='dict', default={}),
|
||||
name=dict(),
|
||||
networks=dict(type='list', default=['public', 'private']),
|
||||
service=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
user_data=dict(no_log=True),
|
||||
wait=dict(default=False, type='bool'),
|
||||
wait_timeout=dict(default=300),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
service = module.params.get('service')
|
||||
|
||||
if service is not None:
|
||||
module.fail_json(msg='The "service" attribute has been deprecated, '
|
||||
'please remove "service: cloudservers" from your '
|
||||
'playbook pertaining to the "rax" module')
|
||||
|
||||
auto_increment = module.params.get('auto_increment')
|
||||
config_drive = module.params.get('config_drive')
|
||||
count = module.params.get('count')
|
||||
count_offset = module.params.get('count_offset')
|
||||
disk_config = module.params.get('disk_config')
|
||||
if disk_config:
|
||||
disk_config = disk_config.upper()
|
||||
exact_count = module.params.get('exact_count', False)
|
||||
extra_client_args = module.params.get('extra_client_args')
|
||||
extra_create_args = module.params.get('extra_create_args')
|
||||
files = module.params.get('files')
|
||||
flavor = module.params.get('flavor')
|
||||
group = module.params.get('group')
|
||||
image = module.params.get('image')
|
||||
instance_ids = module.params.get('instance_ids')
|
||||
key_name = module.params.get('key_name')
|
||||
meta = module.params.get('meta')
|
||||
name = module.params.get('name')
|
||||
networks = module.params.get('networks')
|
||||
state = module.params.get('state')
|
||||
user_data = module.params.get('user_data')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
if extra_client_args:
|
||||
pyrax.cloudservers = pyrax.connect_to_cloudservers(
|
||||
region=pyrax.cloudservers.client.region_name,
|
||||
**extra_client_args)
|
||||
client = pyrax.cloudservers.client
|
||||
if 'bypass_url' in extra_client_args:
|
||||
client.management_url = extra_client_args['bypass_url']
|
||||
|
||||
if pyrax.cloudservers is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
cloudservers(module, state=state, name=name, flavor=flavor,
|
||||
image=image, meta=meta, key_name=key_name, files=files,
|
||||
wait=wait, wait_timeout=wait_timeout, disk_config=disk_config,
|
||||
count=count, group=group, instance_ids=instance_ids,
|
||||
exact_count=exact_count, networks=networks,
|
||||
count_offset=count_offset, auto_increment=auto_increment,
|
||||
extra_create_args=extra_create_args, user_data=user_data,
|
||||
config_drive=config_drive)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
# invoke the module
|
||||
main()
|
@ -1,220 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cbs
|
||||
short_description: Manipulate Rackspace Cloud Block Storage Volumes
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Block Storage Volumes
|
||||
version_added: 1.6
|
||||
options:
|
||||
description:
|
||||
description:
|
||||
- Description to give the volume being created
|
||||
default: null
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the volume
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- Name to give the volume being created
|
||||
default: null
|
||||
required: true
|
||||
size:
|
||||
description:
|
||||
- Size of the volume to create in Gigabytes
|
||||
default: 100
|
||||
required: true
|
||||
snapshot_id:
|
||||
description:
|
||||
- The id of the snapshot to create the volume from
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
required: true
|
||||
volume_type:
|
||||
description:
|
||||
- Type of the volume being created
|
||||
choices:
|
||||
- SATA
|
||||
- SSD
|
||||
default: SATA
|
||||
required: true
|
||||
wait:
|
||||
description:
|
||||
- wait for the volume to be in state 'available' before returning
|
||||
default: "no"
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: Christopher H. Laco, Matt Martz
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Block Storage Volume
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Storage volume create request
|
||||
local_action:
|
||||
module: rax_cbs
|
||||
credentials: ~/.raxpub
|
||||
name: my-volume
|
||||
description: My Volume
|
||||
volume_type: SSD
|
||||
size: 150
|
||||
region: DFW
|
||||
wait: yes
|
||||
state: present
|
||||
meta:
|
||||
app: my-cool-app
|
||||
register: my_volume
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def cloud_block_storage(module, state, name, description, meta, size,
|
||||
snapshot_id, volume_type, wait, wait_timeout):
|
||||
for arg in (state, name, size, volume_type):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for rax_cbs' % arg)
|
||||
|
||||
if size < 100:
|
||||
module.fail_json(msg='"size" must be greater than or equal to 100')
|
||||
|
||||
changed = False
|
||||
volume = None
|
||||
instance = {}
|
||||
|
||||
cbs = pyrax.cloud_blockstorage
|
||||
|
||||
if cbs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
volume = rax_find_volume(module, pyrax, name)
|
||||
|
||||
if state == 'present':
|
||||
if not volume:
|
||||
try:
|
||||
volume = cbs.create(name, size=size, volume_type=volume_type,
|
||||
description=description,
|
||||
metadata=meta,
|
||||
snapshot_id=snapshot_id)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
if wait:
|
||||
attempts = wait_timeout / 5
|
||||
pyrax.utils.wait_for_build(volume, interval=5,
|
||||
attempts=attempts)
|
||||
|
||||
volume.get()
|
||||
for key, value in vars(volume).iteritems():
|
||||
if (isinstance(value, NON_CALLABLES) and
|
||||
not key.startswith('_')):
|
||||
instance[key] = value
|
||||
|
||||
result = dict(changed=changed, volume=instance)
|
||||
|
||||
if volume.status == 'error':
|
||||
result['msg'] = '%s failed to build' % volume.id
|
||||
elif wait and volume.status not in VOLUME_STATUS:
|
||||
result['msg'] = 'Timeout waiting on %s' % volume.id
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
if volume:
|
||||
try:
|
||||
volume.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, volume=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
description=dict(),
|
||||
meta=dict(type='dict', default={}),
|
||||
name=dict(required=True),
|
||||
size=dict(type='int', default=100),
|
||||
snapshot_id=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
volume_type=dict(choices=['SSD', 'SATA'], default='SATA'),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300)
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
description = module.params.get('description')
|
||||
meta = module.params.get('meta')
|
||||
name = module.params.get('name')
|
||||
size = module.params.get('size')
|
||||
snapshot_id = module.params.get('snapshot_id')
|
||||
state = module.params.get('state')
|
||||
volume_type = module.params.get('volume_type')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_block_storage(module, state, name, description, meta, size,
|
||||
snapshot_id, volume_type, wait, wait_timeout)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,226 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cbs_attachments
|
||||
short_description: Manipulate Rackspace Cloud Block Storage Volume Attachments
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Block Storage Volume Attachments
|
||||
version_added: 1.6
|
||||
options:
|
||||
device:
|
||||
description:
|
||||
- The device path to attach the volume to, e.g. /dev/xvde
|
||||
default: null
|
||||
required: true
|
||||
volume:
|
||||
description:
|
||||
- Name or id of the volume to attach/detach
|
||||
default: null
|
||||
required: true
|
||||
server:
|
||||
description:
|
||||
- Name or id of the server to attach/detach
|
||||
default: null
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
required: true
|
||||
wait:
|
||||
description:
|
||||
- wait for the volume to be in 'in-use'/'available' state before returning
|
||||
default: "no"
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: Christopher H. Laco, Matt Martz
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Attach a Block Storage Volume
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Storage volume attach request
|
||||
local_action:
|
||||
module: rax_cbs_attachments
|
||||
credentials: ~/.raxpub
|
||||
volume: my-volume
|
||||
server: my-server
|
||||
device: /dev/xvdd
|
||||
region: DFW
|
||||
wait: yes
|
||||
state: present
|
||||
register: my_volume
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def cloud_block_storage_attachments(module, state, volume, server, device,
|
||||
wait, wait_timeout):
|
||||
for arg in (state, volume, server, device):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for rax_cbs_attachments' %
|
||||
arg)
|
||||
|
||||
cbs = pyrax.cloud_blockstorage
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cbs is None or cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
changed = False
|
||||
instance = {}
|
||||
|
||||
volume = rax_find_volume(module, pyrax, volume)
|
||||
|
||||
if not volume:
|
||||
module.fail_json(msg='No matching storage volumes were found')
|
||||
|
||||
if state == 'present':
|
||||
server = rax_find_server(module, pyrax, server)
|
||||
|
||||
if (volume.attachments and
|
||||
volume.attachments[0]['server_id'] == server.id):
|
||||
changed = False
|
||||
elif volume.attachments:
|
||||
module.fail_json(msg='Volume is attached to another server')
|
||||
else:
|
||||
try:
|
||||
volume.attach_to_instance(server, mountpoint=device)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
volume.get()
|
||||
|
||||
for key, value in vars(volume).iteritems():
|
||||
if (isinstance(value, NON_CALLABLES) and
|
||||
not key.startswith('_')):
|
||||
instance[key] = value
|
||||
|
||||
result = dict(changed=changed, volume=instance)
|
||||
|
||||
if volume.status == 'error':
|
||||
result['msg'] = '%s failed to build' % volume.id
|
||||
elif wait:
|
||||
attempts = wait_timeout / 5
|
||||
pyrax.utils.wait_until(volume, 'status', 'in-use',
|
||||
interval=5, attempts=attempts)
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
server = rax_find_server(module, pyrax, server)
|
||||
|
||||
if (volume.attachments and
|
||||
volume.attachments[0]['server_id'] == server.id):
|
||||
try:
|
||||
volume.detach()
|
||||
if wait:
|
||||
pyrax.utils.wait_until(volume, 'status', 'available',
|
||||
interval=3, attempts=0,
|
||||
verbose=False)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
volume.get()
|
||||
changed = True
|
||||
elif volume.attachments:
|
||||
module.fail_json(msg='Volume is attached to another server')
|
||||
|
||||
for key, value in vars(volume).iteritems():
|
||||
if (isinstance(value, NON_CALLABLES) and
|
||||
not key.startswith('_')):
|
||||
instance[key] = value
|
||||
|
||||
result = dict(changed=changed, volume=instance)
|
||||
|
||||
if volume.status == 'error':
|
||||
result['msg'] = '%s failed to build' % volume.id
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
module.exit_json(changed=changed, volume=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
device=dict(required=True),
|
||||
volume=dict(required=True),
|
||||
server=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300)
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
device = module.params.get('device')
|
||||
volume = module.params.get('volume')
|
||||
server = module.params.get('server')
|
||||
state = module.params.get('state')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_block_storage_attachments(module, state, volume, server, device,
|
||||
wait, wait_timeout)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,238 +0,0 @@
|
||||
#!/usr/bin/python -tt
|
||||
# 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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cdb
|
||||
short_description: create/delete or resize a Rackspace Cloud Databases instance
|
||||
description:
|
||||
- creates / deletes or resize a Rackspace Cloud Databases instance
|
||||
and optionally waits for it to be 'running'. The name option needs to be
|
||||
unique since it's used to identify the instance.
|
||||
version_added: "1.8"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the databases server instance
|
||||
default: null
|
||||
flavor:
|
||||
description:
|
||||
- flavor to use for the instance 1 to 6 (i.e. 512MB to 16GB)
|
||||
default: 1
|
||||
volume:
|
||||
description:
|
||||
- Volume size of the database 1-150GB
|
||||
default: 2
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
wait:
|
||||
description:
|
||||
- wait for the instance to be in state 'running' before returning
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: Simon JAILLET
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Cloud Databases
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Server build request
|
||||
local_action:
|
||||
module: rax_cdb
|
||||
credentials: ~/.raxpub
|
||||
region: IAD
|
||||
name: db-server1
|
||||
flavor: 1
|
||||
volume: 2
|
||||
wait: yes
|
||||
state: present
|
||||
register: rax_db_server
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def find_instance(name):
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
instances = cdb.list()
|
||||
if instances:
|
||||
for instance in instances:
|
||||
if instance.name == name:
|
||||
return instance
|
||||
return False
|
||||
|
||||
|
||||
def save_instance(module, name, flavor, volume, wait, wait_timeout):
|
||||
|
||||
for arg, value in dict(name=name, flavor=flavor,
|
||||
volume=volume).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb"'
|
||||
' module' % arg)
|
||||
|
||||
if not (volume >= 1 and volume <= 150):
|
||||
module.fail_json(msg='volume is required to be between 1 and 150')
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
flavors = []
|
||||
for item in cdb.list_flavors():
|
||||
flavors.append(item.id)
|
||||
|
||||
if not (flavor in flavors):
|
||||
module.fail_json(msg='unexisting flavor reference "%s"' % str(flavor))
|
||||
|
||||
changed = False
|
||||
|
||||
instance = find_instance(name)
|
||||
|
||||
if not instance:
|
||||
action = 'create'
|
||||
try:
|
||||
instance = cdb.create(name=name, flavor=flavor, volume=volume)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
else:
|
||||
action = None
|
||||
|
||||
if instance.volume.size != volume:
|
||||
action = 'resize'
|
||||
if instance.volume.size > volume:
|
||||
module.fail_json(changed=False, action=action,
|
||||
msg='The new volume size must be larger than '
|
||||
'the current volume size',
|
||||
cdb=rax_to_dict(instance))
|
||||
instance.resize_volume(volume)
|
||||
changed = True
|
||||
|
||||
if int(instance.flavor.id) != flavor:
|
||||
action = 'resize'
|
||||
pyrax.utils.wait_until(instance, 'status', 'ACTIVE',
|
||||
attempts=wait_timeout)
|
||||
instance.resize(flavor)
|
||||
changed = True
|
||||
|
||||
if wait:
|
||||
pyrax.utils.wait_until(instance, 'status', 'ACTIVE',
|
||||
attempts=wait_timeout)
|
||||
|
||||
if wait and instance.status != 'ACTIVE':
|
||||
module.fail_json(changed=changed, action=action,
|
||||
cdb=rax_to_dict(instance),
|
||||
msg='Timeout waiting for "%s" databases instance to '
|
||||
'be created' % name)
|
||||
|
||||
module.exit_json(changed=changed, action=action, cdb=rax_to_dict(instance))
|
||||
|
||||
|
||||
def delete_instance(module, name, wait, wait_timeout):
|
||||
|
||||
if not name:
|
||||
module.fail_json(msg='name is required for the "rax_cdb" module')
|
||||
|
||||
changed = False
|
||||
|
||||
instance = find_instance(name)
|
||||
if not instance:
|
||||
module.exit_json(changed=False, action='delete')
|
||||
|
||||
try:
|
||||
instance.delete()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
if wait:
|
||||
pyrax.utils.wait_until(instance, 'status', 'SHUTDOWN',
|
||||
attempts=wait_timeout)
|
||||
|
||||
if wait and instance.status != 'SHUTDOWN':
|
||||
module.fail_json(changed=changed, action='delete',
|
||||
cdb=rax_to_dict(instance),
|
||||
msg='Timeout waiting for "%s" databases instance to '
|
||||
'be deleted' % name)
|
||||
|
||||
module.exit_json(changed=changed, action='delete',
|
||||
cdb=rax_to_dict(instance))
|
||||
|
||||
|
||||
def rax_cdb(module, state, name, flavor, volume, wait, wait_timeout):
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
save_instance(module, name, flavor, volume, wait, wait_timeout)
|
||||
elif state == 'absent':
|
||||
delete_instance(module, name, wait, wait_timeout)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(type='str', required=True),
|
||||
flavor=dict(type='int', default=1),
|
||||
volume=dict(type='int', default=2),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int', default=300),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
name = module.params.get('name')
|
||||
flavor = module.params.get('flavor')
|
||||
volume = module.params.get('volume')
|
||||
state = module.params.get('state')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
rax_cdb(module, state, name, flavor, volume, wait, wait_timeout)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
# invoke the module
|
||||
main()
|
@ -1,186 +0,0 @@
|
||||
#!/usr/bin/python -tt
|
||||
# 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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
module: rax_cdb_database
|
||||
short_description: 'create / delete a database in the Cloud Databases'
|
||||
description:
|
||||
- create / delete a database in the Cloud Databases.
|
||||
version_added: "1.8"
|
||||
options:
|
||||
cdb_id:
|
||||
description:
|
||||
- The databases server UUID
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- Name to give to the database
|
||||
default: null
|
||||
character_set:
|
||||
description:
|
||||
- Set of symbols and encodings
|
||||
default: 'utf8'
|
||||
collate:
|
||||
description:
|
||||
- Set of rules for comparing characters in a character set
|
||||
default: 'utf8_general_ci'
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
author: Simon JAILLET
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a database in Cloud Databases
|
||||
tasks:
|
||||
- name: Database build request
|
||||
local_action:
|
||||
module: rax_cdb_database
|
||||
credentials: ~/.raxpub
|
||||
region: IAD
|
||||
cdb_id: 323e7ce0-9cb0-11e3-a5e2-0800200c9a66
|
||||
name: db1
|
||||
state: present
|
||||
register: rax_db_database
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def find_database(instance, name):
|
||||
try:
|
||||
database = instance.get_database(name)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return database
|
||||
|
||||
|
||||
def save_database(module, cdb_id, name, character_set, collate):
|
||||
|
||||
for arg, value in dict(cdb_id=cdb_id, name=name).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb_database" '
|
||||
'module' % arg)
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
database = find_database(instance, name)
|
||||
|
||||
if not database:
|
||||
try:
|
||||
database = instance.create_database(name=name,
|
||||
character_set=character_set,
|
||||
collate=collate)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action='create',
|
||||
database=rax_to_dict(database))
|
||||
|
||||
|
||||
def delete_database(module, cdb_id, name):
|
||||
|
||||
for arg, value in dict(cdb_id=cdb_id, name=name).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb_database" '
|
||||
'module' % arg)
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
database = find_database(instance, name)
|
||||
|
||||
if database:
|
||||
try:
|
||||
database.delete()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action='delete')
|
||||
|
||||
|
||||
def rax_cdb_database(module, state, cdb_id, name, character_set, collate):
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
save_database(module, cdb_id, name, character_set, collate)
|
||||
elif state == 'absent':
|
||||
delete_database(module, cdb_id, name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
cdb_id=dict(type='str', required=True),
|
||||
name=dict(type='str', required=True),
|
||||
character_set=dict(type='str', default='utf8'),
|
||||
collate=dict(type='str', default='utf8_general_ci'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
cdb_id = module.params.get('cdb_id')
|
||||
name = module.params.get('name')
|
||||
character_set = module.params.get('character_set')
|
||||
collate = module.params.get('collate')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
rax_cdb_database(module, state, cdb_id, name, character_set, collate)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
# invoke the module
|
||||
main()
|
@ -1,220 +0,0 @@
|
||||
#!/usr/bin/python -tt
|
||||
# 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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_cdb_user
|
||||
short_description: create / delete a Rackspace Cloud Database
|
||||
description:
|
||||
- create / delete a database in the Cloud Databases.
|
||||
version_added: "1.8"
|
||||
options:
|
||||
cdb_id:
|
||||
description:
|
||||
- The databases server UUID
|
||||
default: null
|
||||
db_username:
|
||||
description:
|
||||
- Name of the database user
|
||||
default: null
|
||||
db_password:
|
||||
description:
|
||||
- Database user password
|
||||
default: null
|
||||
databases:
|
||||
description:
|
||||
- Name of the databases that the user can access
|
||||
default: []
|
||||
host:
|
||||
description:
|
||||
- Specifies the host from which a user is allowed to connect to
|
||||
the database. Possible values are a string containing an IPv4 address
|
||||
or "%" to allow connecting from any host
|
||||
default: '%'
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
author: Simon JAILLET
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a user in Cloud Databases
|
||||
tasks:
|
||||
- name: User build request
|
||||
local_action:
|
||||
module: rax_cdb_user
|
||||
credentials: ~/.raxpub
|
||||
region: IAD
|
||||
cdb_id: 323e7ce0-9cb0-11e3-a5e2-0800200c9a66
|
||||
db_username: user1
|
||||
db_password: user1
|
||||
databases: ['db1']
|
||||
state: present
|
||||
register: rax_db_user
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def find_user(instance, name):
|
||||
try:
|
||||
user = instance.get_user(name)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def save_user(module, cdb_id, name, password, databases, host):
|
||||
|
||||
for arg, value in dict(cdb_id=cdb_id, name=name).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb_user" '
|
||||
'module' % arg)
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
user = find_user(instance, name)
|
||||
|
||||
if not user:
|
||||
action = 'create'
|
||||
try:
|
||||
user = instance.create_user(name=name,
|
||||
password=password,
|
||||
database_names=databases,
|
||||
host=host)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
else:
|
||||
action = 'update'
|
||||
|
||||
if user.host != host:
|
||||
changed = True
|
||||
|
||||
user.update(password=password, host=host)
|
||||
|
||||
former_dbs = set([item.name for item in user.list_user_access()])
|
||||
databases = set(databases)
|
||||
|
||||
if databases != former_dbs:
|
||||
try:
|
||||
revoke_dbs = [db for db in former_dbs if db not in databases]
|
||||
user.revoke_user_access(db_names=revoke_dbs)
|
||||
|
||||
new_dbs = [db for db in databases if db not in former_dbs]
|
||||
user.grant_user_access(db_names=new_dbs)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action=action, user=rax_to_dict(user))
|
||||
|
||||
|
||||
def delete_user(module, cdb_id, name):
|
||||
|
||||
for arg, value in dict(cdb_id=cdb_id, name=name).iteritems():
|
||||
if not value:
|
||||
module.fail_json(msg='%s is required for the "rax_cdb_user"'
|
||||
' module' % arg)
|
||||
|
||||
cdb = pyrax.cloud_databases
|
||||
|
||||
try:
|
||||
instance = cdb.get(cdb_id)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
changed = False
|
||||
|
||||
user = find_user(instance, name)
|
||||
|
||||
if user:
|
||||
try:
|
||||
user.delete()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, action='delete')
|
||||
|
||||
|
||||
def rax_cdb_user(module, state, cdb_id, name, password, databases, host):
|
||||
|
||||
# act on the state
|
||||
if state == 'present':
|
||||
save_user(module, cdb_id, name, password, databases, host)
|
||||
elif state == 'absent':
|
||||
delete_user(module, cdb_id, name)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
cdb_id=dict(type='str', required=True),
|
||||
db_username=dict(type='str', required=True),
|
||||
db_password=dict(type='str', required=True, no_log=True),
|
||||
databases=dict(type='list', default=[]),
|
||||
host=dict(type='str', default='%'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
cdb_id = module.params.get('cdb_id')
|
||||
name = module.params.get('db_username')
|
||||
password = module.params.get('db_password')
|
||||
databases = module.params.get('databases')
|
||||
host = unicode(module.params.get('host'))
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
rax_cdb_user(module, state, cdb_id, name, password, databases, host)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
# invoke the module
|
||||
main()
|
@ -1,303 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_clb
|
||||
short_description: create / delete a load balancer in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud load balancer.
|
||||
version_added: "1.4"
|
||||
options:
|
||||
algorithm:
|
||||
description:
|
||||
- algorithm for the balancer being created
|
||||
choices:
|
||||
- RANDOM
|
||||
- LEAST_CONNECTIONS
|
||||
- ROUND_ROBIN
|
||||
- WEIGHTED_LEAST_CONNECTIONS
|
||||
- WEIGHTED_ROUND_ROBIN
|
||||
default: LEAST_CONNECTIONS
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- Name to give the load balancer
|
||||
default: null
|
||||
port:
|
||||
description:
|
||||
- Port for the balancer being created
|
||||
default: 80
|
||||
protocol:
|
||||
description:
|
||||
- Protocol for the balancer being created
|
||||
choices:
|
||||
- DNS_TCP
|
||||
- DNS_UDP
|
||||
- FTP
|
||||
- HTTP
|
||||
- HTTPS
|
||||
- IMAPS
|
||||
- IMAPv4
|
||||
- LDAP
|
||||
- LDAPS
|
||||
- MYSQL
|
||||
- POP3
|
||||
- POP3S
|
||||
- SMTP
|
||||
- TCP
|
||||
- TCP_CLIENT_FIRST
|
||||
- UDP
|
||||
- UDP_STREAM
|
||||
- SFTP
|
||||
default: HTTP
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
timeout:
|
||||
description:
|
||||
- timeout for communication between the balancer and the node
|
||||
default: 30
|
||||
type:
|
||||
description:
|
||||
- type of interface for the balancer being created
|
||||
choices:
|
||||
- PUBLIC
|
||||
- SERVICENET
|
||||
default: PUBLIC
|
||||
vip_id:
|
||||
description:
|
||||
- Virtual IP ID to use when creating the load balancer for purposes of
|
||||
sharing an IP with another load balancer of another protocol
|
||||
version_added: 1.5
|
||||
wait:
|
||||
description:
|
||||
- wait for the balancer to be in state 'running' before returning
|
||||
default: "no"
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: Christopher H. Laco, Matt Martz
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Load Balancer
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Load Balancer create request
|
||||
local_action:
|
||||
module: rax_clb
|
||||
credentials: ~/.raxpub
|
||||
name: my-lb
|
||||
port: 8080
|
||||
protocol: HTTP
|
||||
type: SERVICENET
|
||||
timeout: 30
|
||||
region: DFW
|
||||
wait: yes
|
||||
state: present
|
||||
meta:
|
||||
app: my-cool-app
|
||||
register: my_lb
|
||||
'''
|
||||
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def cloud_load_balancer(module, state, name, meta, algorithm, port, protocol,
|
||||
vip_type, timeout, wait, wait_timeout, vip_id):
|
||||
for arg in (state, name, port, protocol, vip_type):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for rax_clb' % arg)
|
||||
|
||||
if int(timeout) < 30:
|
||||
module.fail_json(msg='"timeout" must be greater than or equal to 30')
|
||||
|
||||
changed = False
|
||||
balancers = []
|
||||
|
||||
clb = pyrax.cloud_loadbalancers
|
||||
if not clb:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
for balancer in clb.list():
|
||||
if name != balancer.name and name != balancer.id:
|
||||
continue
|
||||
|
||||
balancers.append(balancer)
|
||||
|
||||
if len(balancers) > 1:
|
||||
module.fail_json(msg='Multiple Load Balancers were matched by name, '
|
||||
'try using the Load Balancer ID instead')
|
||||
|
||||
if state == 'present':
|
||||
if isinstance(meta, dict):
|
||||
metadata = [dict(key=k, value=v) for k, v in meta.items()]
|
||||
|
||||
if not balancers:
|
||||
try:
|
||||
virtual_ips = [clb.VirtualIP(type=vip_type, id=vip_id)]
|
||||
balancer = clb.create(name, metadata=metadata, port=port,
|
||||
algorithm=algorithm, protocol=protocol,
|
||||
timeout=timeout, virtual_ips=virtual_ips)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
balancer = balancers[0]
|
||||
setattr(balancer, 'metadata',
|
||||
[dict(key=k, value=v) for k, v in
|
||||
balancer.get_metadata().items()])
|
||||
atts = {
|
||||
'name': name,
|
||||
'algorithm': algorithm,
|
||||
'port': port,
|
||||
'protocol': protocol,
|
||||
'timeout': timeout
|
||||
}
|
||||
for att, value in atts.iteritems():
|
||||
current = getattr(balancer, att)
|
||||
if current != value:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
balancer.update(**atts)
|
||||
|
||||
if balancer.metadata != metadata:
|
||||
balancer.set_metadata(meta)
|
||||
changed = True
|
||||
|
||||
virtual_ips = [clb.VirtualIP(type=vip_type)]
|
||||
current_vip_types = set([v.type for v in balancer.virtual_ips])
|
||||
vip_types = set([v.type for v in virtual_ips])
|
||||
if current_vip_types != vip_types:
|
||||
module.fail_json(msg='Load balancer Virtual IP type cannot '
|
||||
'be changed')
|
||||
|
||||
if wait:
|
||||
attempts = wait_timeout / 5
|
||||
pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts)
|
||||
|
||||
balancer.get()
|
||||
instance = rax_to_dict(balancer, 'clb')
|
||||
|
||||
result = dict(changed=changed, balancer=instance)
|
||||
|
||||
if balancer.status == 'ERROR':
|
||||
result['msg'] = '%s failed to build' % balancer.id
|
||||
elif wait and balancer.status not in ('ACTIVE', 'ERROR'):
|
||||
result['msg'] = 'Timeout waiting on %s' % balancer.id
|
||||
|
||||
if 'msg' in result:
|
||||
module.fail_json(**result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
if balancers:
|
||||
balancer = balancers[0]
|
||||
try:
|
||||
balancer.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
instance = rax_to_dict(balancer, 'clb')
|
||||
|
||||
if wait:
|
||||
attempts = wait_timeout / 5
|
||||
pyrax.utils.wait_until(balancer, 'status', ('DELETED'),
|
||||
interval=5, attempts=attempts)
|
||||
else:
|
||||
instance = {}
|
||||
|
||||
module.exit_json(changed=changed, balancer=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
algorithm=dict(choices=CLB_ALGORITHMS,
|
||||
default='LEAST_CONNECTIONS'),
|
||||
meta=dict(type='dict', default={}),
|
||||
name=dict(),
|
||||
port=dict(type='int', default=80),
|
||||
protocol=dict(choices=CLB_PROTOCOLS, default='HTTP'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
timeout=dict(type='int', default=30),
|
||||
type=dict(choices=['PUBLIC', 'SERVICENET'], default='PUBLIC'),
|
||||
vip_id=dict(),
|
||||
wait=dict(type='bool'),
|
||||
wait_timeout=dict(default=300),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
algorithm = module.params.get('algorithm')
|
||||
meta = module.params.get('meta')
|
||||
name = module.params.get('name')
|
||||
port = module.params.get('port')
|
||||
protocol = module.params.get('protocol')
|
||||
state = module.params.get('state')
|
||||
timeout = int(module.params.get('timeout'))
|
||||
vip_id = module.params.get('vip_id')
|
||||
vip_type = module.params.get('type')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_load_balancer(module, state, name, meta, algorithm, port, protocol,
|
||||
vip_type, timeout, wait, wait_timeout, vip_id)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,303 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_clb_nodes
|
||||
short_description: add, modify and remove nodes from a Rackspace Cloud Load Balancer
|
||||
description:
|
||||
- Adds, modifies and removes nodes from a Rackspace Cloud Load Balancer
|
||||
version_added: "1.4"
|
||||
options:
|
||||
address:
|
||||
required: false
|
||||
description:
|
||||
- IP address or domain name of the node
|
||||
condition:
|
||||
required: false
|
||||
choices:
|
||||
- enabled
|
||||
- disabled
|
||||
- draining
|
||||
description:
|
||||
- Condition for the node, which determines its role within the load
|
||||
balancer
|
||||
load_balancer_id:
|
||||
required: true
|
||||
type: integer
|
||||
description:
|
||||
- Load balancer id
|
||||
node_id:
|
||||
required: false
|
||||
type: integer
|
||||
description:
|
||||
- Node id
|
||||
port:
|
||||
required: false
|
||||
type: integer
|
||||
description:
|
||||
- Port number of the load balanced service on the node
|
||||
state:
|
||||
required: false
|
||||
default: "present"
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
description:
|
||||
- Indicate desired state of the node
|
||||
type:
|
||||
required: false
|
||||
choices:
|
||||
- primary
|
||||
- secondary
|
||||
description:
|
||||
- Type of node
|
||||
wait:
|
||||
required: false
|
||||
default: "no"
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
description:
|
||||
- Wait for the load balancer to become active before returning
|
||||
wait_timeout:
|
||||
required: false
|
||||
type: integer
|
||||
default: 30
|
||||
description:
|
||||
- How long to wait before giving up and returning an error
|
||||
weight:
|
||||
required: false
|
||||
description:
|
||||
- Weight of node
|
||||
author: Lukasz Kawczynski
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add a new node to the load balancer
|
||||
- local_action:
|
||||
module: rax_clb_nodes
|
||||
load_balancer_id: 71
|
||||
address: 10.2.2.3
|
||||
port: 80
|
||||
condition: enabled
|
||||
type: primary
|
||||
wait: yes
|
||||
credentials: /path/to/credentials
|
||||
|
||||
# Drain connections from a node
|
||||
- local_action:
|
||||
module: rax_clb_nodes
|
||||
load_balancer_id: 71
|
||||
node_id: 410
|
||||
condition: draining
|
||||
wait: yes
|
||||
credentials: /path/to/credentials
|
||||
|
||||
# Remove a node from the load balancer
|
||||
- local_action:
|
||||
module: rax_clb_nodes
|
||||
load_balancer_id: 71
|
||||
node_id: 410
|
||||
state: absent
|
||||
wait: yes
|
||||
credentials: /path/to/credentials
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def _activate_virtualenv(path):
|
||||
path = os.path.expanduser(path)
|
||||
activate_this = os.path.join(path, 'bin', 'activate_this.py')
|
||||
execfile(activate_this, dict(__file__=activate_this))
|
||||
|
||||
|
||||
def _get_node(lb, node_id=None, address=None, port=None):
|
||||
"""Return a matching node"""
|
||||
for node in getattr(lb, 'nodes', []):
|
||||
match_list = []
|
||||
if node_id is not None:
|
||||
match_list.append(getattr(node, 'id', None) == node_id)
|
||||
if address is not None:
|
||||
match_list.append(getattr(node, 'address', None) == address)
|
||||
if port is not None:
|
||||
match_list.append(getattr(node, 'port', None) == port)
|
||||
|
||||
if match_list and all(match_list):
|
||||
return node
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_primary(node):
|
||||
"""Return True if node is primary and enabled"""
|
||||
return (node.type.lower() == 'primary' and
|
||||
node.condition.lower() == 'enabled')
|
||||
|
||||
|
||||
def _get_primary_nodes(lb):
|
||||
"""Return a list of primary and enabled nodes"""
|
||||
nodes = []
|
||||
for node in lb.nodes:
|
||||
if _is_primary(node):
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
address=dict(),
|
||||
condition=dict(choices=['enabled', 'disabled', 'draining']),
|
||||
load_balancer_id=dict(required=True, type='int'),
|
||||
node_id=dict(type='int'),
|
||||
port=dict(type='int'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
type=dict(choices=['primary', 'secondary']),
|
||||
virtualenv=dict(),
|
||||
wait=dict(default=False, type='bool'),
|
||||
wait_timeout=dict(default=30, type='int'),
|
||||
weight=dict(type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
address = module.params['address']
|
||||
condition = (module.params['condition'] and
|
||||
module.params['condition'].upper())
|
||||
load_balancer_id = module.params['load_balancer_id']
|
||||
node_id = module.params['node_id']
|
||||
port = module.params['port']
|
||||
state = module.params['state']
|
||||
typ = module.params['type'] and module.params['type'].upper()
|
||||
virtualenv = module.params['virtualenv']
|
||||
wait = module.params['wait']
|
||||
wait_timeout = module.params['wait_timeout'] or 1
|
||||
weight = module.params['weight']
|
||||
|
||||
if virtualenv:
|
||||
try:
|
||||
_activate_virtualenv(virtualenv)
|
||||
except IOError, e:
|
||||
module.fail_json(msg='Failed to activate virtualenv %s (%s)' % (
|
||||
virtualenv, e))
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
if not pyrax.cloud_loadbalancers:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
try:
|
||||
lb = pyrax.cloud_loadbalancers.get(load_balancer_id)
|
||||
except pyrax.exc.PyraxException, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
node = _get_node(lb, node_id, address, port)
|
||||
|
||||
result = rax_clb_node_to_dict(node)
|
||||
|
||||
if state == 'absent':
|
||||
if not node: # Removing a non-existent node
|
||||
module.exit_json(changed=False, state=state)
|
||||
|
||||
# The API detects this as well but currently pyrax does not return a
|
||||
# meaningful error message
|
||||
if _is_primary(node) and len(_get_primary_nodes(lb)) == 1:
|
||||
module.fail_json(
|
||||
msg='At least one primary node has to be enabled')
|
||||
|
||||
try:
|
||||
lb.delete_node(node)
|
||||
result = {}
|
||||
except pyrax.exc.NotFound:
|
||||
module.exit_json(changed=False, state=state)
|
||||
except pyrax.exc.PyraxException, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else: # present
|
||||
if not node:
|
||||
if node_id: # Updating a non-existent node
|
||||
msg = 'Node %d not found' % node_id
|
||||
if lb.nodes:
|
||||
msg += (' (available nodes: %s)' %
|
||||
', '.join([str(x.id) for x in lb.nodes]))
|
||||
module.fail_json(msg=msg)
|
||||
else: # Creating a new node
|
||||
try:
|
||||
node = pyrax.cloudloadbalancers.Node(
|
||||
address=address, port=port, condition=condition,
|
||||
weight=weight, type=typ)
|
||||
resp, body = lb.add_nodes([node])
|
||||
result.update(body['nodes'][0])
|
||||
except pyrax.exc.PyraxException, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else: # Updating an existing node
|
||||
mutable = {
|
||||
'condition': condition,
|
||||
'type': typ,
|
||||
'weight': weight,
|
||||
}
|
||||
|
||||
for name, value in mutable.items():
|
||||
if value is None or value == getattr(node, name):
|
||||
mutable.pop(name)
|
||||
|
||||
if not mutable:
|
||||
module.exit_json(changed=False, state=state, node=result)
|
||||
|
||||
try:
|
||||
# The diff has to be set explicitly to update node's weight and
|
||||
# type; this should probably be fixed in pyrax
|
||||
lb.update_node(node, diff=mutable)
|
||||
result.update(mutable)
|
||||
except pyrax.exc.PyraxException, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if wait:
|
||||
pyrax.utils.wait_until(lb, "status", "ACTIVE", interval=1,
|
||||
attempts=wait_timeout)
|
||||
if lb.status != 'ACTIVE':
|
||||
module.fail_json(
|
||||
msg='Load balancer not active after %ds (current status: %s)' %
|
||||
(wait_timeout, lb.status.lower()))
|
||||
|
||||
kwargs = {'node': result} if result else {}
|
||||
module.exit_json(changed=True, state=state, **kwargs)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,173 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_dns
|
||||
short_description: Manage domains on Rackspace Cloud DNS
|
||||
description:
|
||||
- Manage domains on Rackspace Cloud DNS
|
||||
version_added: 1.5
|
||||
options:
|
||||
comment:
|
||||
description:
|
||||
- Brief description of the domain. Maximum length of 160 characters
|
||||
email:
|
||||
desctiption:
|
||||
- Email address of the domain administrator
|
||||
name:
|
||||
description:
|
||||
- Domain name to create
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
ttl:
|
||||
description:
|
||||
- Time to live of domain in seconds
|
||||
default: 3600
|
||||
notes:
|
||||
- "It is recommended that plays utilizing this module be run with
|
||||
C(serial: 1) to avoid exceeding the API request limit imposed by
|
||||
the Rackspace CloudDNS API"
|
||||
author: Matt Martz
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create domain
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Domain create request
|
||||
local_action:
|
||||
module: rax_dns
|
||||
credentials: ~/.raxpub
|
||||
name: example.org
|
||||
email: admin@example.org
|
||||
register: rax_dns
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_dns(module, comment, email, name, state, ttl):
|
||||
changed = False
|
||||
|
||||
dns = pyrax.cloud_dns
|
||||
if not dns:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
if not email:
|
||||
module.fail_json(msg='An "email" attribute is required for '
|
||||
'creating a domain')
|
||||
|
||||
try:
|
||||
domain = dns.find(name=name)
|
||||
except pyrax.exceptions.NoUniqueMatch, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except pyrax.exceptions.NotFound:
|
||||
try:
|
||||
domain = dns.create(name=name, emailAddress=email, ttl=ttl,
|
||||
comment=comment)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
update = {}
|
||||
if comment != getattr(domain, 'comment', None):
|
||||
update['comment'] = comment
|
||||
if ttl != getattr(domain, 'ttl', None):
|
||||
update['ttl'] = ttl
|
||||
if email != getattr(domain, 'emailAddress', None):
|
||||
update['emailAddress'] = email
|
||||
|
||||
if update:
|
||||
try:
|
||||
domain.update(**update)
|
||||
changed = True
|
||||
domain.get()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
domain = dns.find(name=name)
|
||||
except pyrax.exceptions.NotFound:
|
||||
domain = {}
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if domain:
|
||||
try:
|
||||
domain.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, domain=rax_to_dict(domain))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
comment=dict(),
|
||||
email=dict(),
|
||||
name=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
ttl=dict(type='int', default=3600),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
comment = module.params.get('comment')
|
||||
email = module.params.get('email')
|
||||
name = module.params.get('name')
|
||||
state = module.params.get('state')
|
||||
ttl = module.params.get('ttl')
|
||||
|
||||
setup_rax_module(module, pyrax, False)
|
||||
|
||||
rax_dns(module, comment, email, name, state, ttl)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,335 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_dns_record
|
||||
short_description: Manage DNS records on Rackspace Cloud DNS
|
||||
description:
|
||||
- Manage DNS records on Rackspace Cloud DNS
|
||||
version_added: 1.5
|
||||
options:
|
||||
comment:
|
||||
description:
|
||||
- Brief description of the domain. Maximum length of 160 characters
|
||||
data:
|
||||
description:
|
||||
- IP address for A/AAAA record, FQDN for CNAME/MX/NS, or text data for
|
||||
SRV/TXT
|
||||
required: True
|
||||
domain:
|
||||
description:
|
||||
- Domain name to create the record in. This is an invalid option when
|
||||
type=PTR
|
||||
loadbalancer:
|
||||
description:
|
||||
- Load Balancer ID to create a PTR record for. Only used with type=PTR
|
||||
version_added: 1.7
|
||||
name:
|
||||
description:
|
||||
- FQDN record name to create
|
||||
required: True
|
||||
priority:
|
||||
description:
|
||||
- Required for MX and SRV records, but forbidden for other record types.
|
||||
If specified, must be an integer from 0 to 65535.
|
||||
server:
|
||||
description:
|
||||
- Server ID to create a PTR record for. Only used with type=PTR
|
||||
version_added: 1.7
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
ttl:
|
||||
description:
|
||||
- Time to live of record in seconds
|
||||
default: 3600
|
||||
type:
|
||||
description:
|
||||
- DNS record type
|
||||
choices:
|
||||
- A
|
||||
- AAAA
|
||||
- CNAME
|
||||
- MX
|
||||
- NS
|
||||
- SRV
|
||||
- TXT
|
||||
- PTR
|
||||
required: true
|
||||
notes:
|
||||
- "It is recommended that plays utilizing this module be run with
|
||||
C(serial: 1) to avoid exceeding the API request limit imposed by
|
||||
the Rackspace CloudDNS API"
|
||||
- To manipulate a C(PTR) record either C(loadbalancer) or C(server) must be
|
||||
supplied
|
||||
- As of version 1.7, the C(type) field is required and no longer defaults to an C(A) record.
|
||||
- C(PTR) record support was added in version 1.7
|
||||
author: Matt Martz
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create DNS Records
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Create A record
|
||||
local_action:
|
||||
module: rax_dns_record
|
||||
credentials: ~/.raxpub
|
||||
domain: example.org
|
||||
name: www.example.org
|
||||
data: "{{ rax_accessipv4 }}"
|
||||
type: A
|
||||
register: a_record
|
||||
|
||||
- name: Create PTR record
|
||||
local_action:
|
||||
module: rax_dns_record
|
||||
credentials: ~/.raxpub
|
||||
server: "{{ rax_id }}"
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
register: ptr_record
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_dns_record_ptr(module, data=None, comment=None, loadbalancer=None,
|
||||
name=None, server=None, state='present', ttl=7200):
|
||||
changed = False
|
||||
results = []
|
||||
|
||||
dns = pyrax.cloud_dns
|
||||
|
||||
if not dns:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if loadbalancer:
|
||||
item = rax_find_loadbalancer(module, pyrax, loadbalancer)
|
||||
elif server:
|
||||
item = rax_find_server(module, pyrax, server)
|
||||
|
||||
if state == 'present':
|
||||
current = dns.list_ptr_records(item)
|
||||
for record in current:
|
||||
if record.data == data:
|
||||
if record.ttl != ttl or record.name != name:
|
||||
try:
|
||||
dns.update_ptr_record(item, record, name, data, ttl)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
record.ttl = ttl
|
||||
record.name = name
|
||||
results.append(rax_to_dict(record))
|
||||
break
|
||||
else:
|
||||
results.append(rax_to_dict(record))
|
||||
break
|
||||
|
||||
if not results:
|
||||
record = dict(name=name, type='PTR', data=data, ttl=ttl,
|
||||
comment=comment)
|
||||
try:
|
||||
results = dns.add_ptr_records(item, [record])
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, records=results)
|
||||
|
||||
elif state == 'absent':
|
||||
current = dns.list_ptr_records(item)
|
||||
for record in current:
|
||||
if record.data == data:
|
||||
results.append(rax_to_dict(record))
|
||||
break
|
||||
|
||||
if results:
|
||||
try:
|
||||
dns.delete_ptr_records(item, data)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, records=results)
|
||||
|
||||
|
||||
def rax_dns_record(module, comment=None, data=None, domain=None, name=None,
|
||||
priority=None, record_type='A', state='present', ttl=7200):
|
||||
"""Function for manipulating record types other than PTR"""
|
||||
|
||||
changed = False
|
||||
|
||||
dns = pyrax.cloud_dns
|
||||
if not dns:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
if not priority and record_type in ['MX', 'SRV']:
|
||||
module.fail_json(msg='A "priority" attribute is required for '
|
||||
'creating a MX or SRV record')
|
||||
|
||||
try:
|
||||
domain = dns.find(name=domain)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
try:
|
||||
record = domain.find_record(record_type, name=name)
|
||||
except pyrax.exceptions.DomainRecordNotUnique, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except pyrax.exceptions.DomainRecordNotFound, e:
|
||||
try:
|
||||
record_data = {
|
||||
'type': record_type,
|
||||
'name': name,
|
||||
'data': data,
|
||||
'ttl': ttl
|
||||
}
|
||||
if comment:
|
||||
record_data.update(dict(comment=comment))
|
||||
if priority and record_type.upper() in ['MX', 'SRV']:
|
||||
record_data.update(dict(priority=priority))
|
||||
|
||||
record = domain.add_records([record_data])[0]
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
update = {}
|
||||
if comment != getattr(record, 'comment', None):
|
||||
update['comment'] = comment
|
||||
if ttl != getattr(record, 'ttl', None):
|
||||
update['ttl'] = ttl
|
||||
if priority != getattr(record, 'priority', None):
|
||||
update['priority'] = priority
|
||||
if data != getattr(record, 'data', None):
|
||||
update['data'] = data
|
||||
|
||||
if update:
|
||||
try:
|
||||
record.update(**update)
|
||||
changed = True
|
||||
record.get()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
domain = dns.find(name=domain)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
try:
|
||||
record = domain.find_record(record_type, name=name, data=data)
|
||||
except pyrax.exceptions.DomainRecordNotFound, e:
|
||||
record = {}
|
||||
pass
|
||||
except pyrax.exceptions.DomainRecordNotUnique, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if record:
|
||||
try:
|
||||
record.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, record=rax_to_dict(record))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
comment=dict(),
|
||||
data=dict(required=True),
|
||||
domain=dict(),
|
||||
loadbalancer=dict(),
|
||||
name=dict(required=True),
|
||||
priority=dict(type='int'),
|
||||
server=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
ttl=dict(type='int', default=3600),
|
||||
type=dict(required=True, choices=['A', 'AAAA', 'CNAME', 'MX', 'NS',
|
||||
'SRV', 'TXT', 'PTR'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[
|
||||
['server', 'loadbalancer', 'domain'],
|
||||
],
|
||||
required_one_of=[
|
||||
['server', 'loadbalancer', 'domain'],
|
||||
],
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
comment = module.params.get('comment')
|
||||
data = module.params.get('data')
|
||||
domain = module.params.get('domain')
|
||||
loadbalancer = module.params.get('loadbalancer')
|
||||
name = module.params.get('name')
|
||||
priority = module.params.get('priority')
|
||||
server = module.params.get('server')
|
||||
state = module.params.get('state')
|
||||
ttl = module.params.get('ttl')
|
||||
record_type = module.params.get('type')
|
||||
|
||||
setup_rax_module(module, pyrax, False)
|
||||
|
||||
if record_type.upper() == 'PTR':
|
||||
if not server and not loadbalancer:
|
||||
module.fail_json(msg='one of the following is required: '
|
||||
'server,loadbalancer')
|
||||
rax_dns_record_ptr(module, data=data, comment=comment,
|
||||
loadbalancer=loadbalancer, name=name, server=server,
|
||||
state=state, ttl=ttl)
|
||||
else:
|
||||
rax_dns_record(module, comment=comment, data=data, domain=domain,
|
||||
name=name, priority=priority, record_type=record_type,
|
||||
state=state, ttl=ttl)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,144 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_facts
|
||||
short_description: Gather facts for Rackspace Cloud Servers
|
||||
description:
|
||||
- Gather facts for Rackspace Cloud Servers.
|
||||
version_added: "1.4"
|
||||
options:
|
||||
address:
|
||||
description:
|
||||
- Server IP address to retrieve facts for, will match any IP assigned to
|
||||
the server
|
||||
id:
|
||||
description:
|
||||
- Server ID to retrieve facts for
|
||||
name:
|
||||
description:
|
||||
- Server name to retrieve facts for
|
||||
default: null
|
||||
author: Matt Martz
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Gather info about servers
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Get facts about servers
|
||||
local_action:
|
||||
module: rax_facts
|
||||
credentials: ~/.raxpub
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
- name: Map some facts
|
||||
set_fact:
|
||||
ansible_ssh_host: "{{ rax_accessipv4 }}"
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_facts(module, address, name, server_id):
|
||||
changed = False
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
ansible_facts = {}
|
||||
|
||||
search_opts = {}
|
||||
if name:
|
||||
search_opts = dict(name='^%s$' % name)
|
||||
try:
|
||||
servers = cs.servers.list(search_opts=search_opts)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif address:
|
||||
servers = []
|
||||
try:
|
||||
for server in cs.servers.list():
|
||||
for addresses in server.networks.values():
|
||||
if address in addresses:
|
||||
servers.append(server)
|
||||
break
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif server_id:
|
||||
servers = []
|
||||
try:
|
||||
servers.append(cs.servers.get(server_id))
|
||||
except Exception, e:
|
||||
pass
|
||||
|
||||
if len(servers) > 1:
|
||||
module.fail_json(msg='Multiple servers found matching provided '
|
||||
'search parameters')
|
||||
elif len(servers) == 1:
|
||||
ansible_facts = rax_to_dict(servers[0], 'server')
|
||||
|
||||
module.exit_json(changed=changed, ansible_facts=ansible_facts)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
address=dict(),
|
||||
id=dict(),
|
||||
name=dict(),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[['address', 'id', 'name']],
|
||||
required_one_of=[['address', 'id', 'name']],
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
address = module.params.get('address')
|
||||
server_id = module.params.get('id')
|
||||
name = module.params.get('name')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_facts(module, address, name, server_id)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,379 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Paul Durivage <paul.durivage@rackspace.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_files
|
||||
short_description: Manipulate Rackspace Cloud Files Containers
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Files Containers
|
||||
version_added: "1.5"
|
||||
options:
|
||||
clear_meta:
|
||||
description:
|
||||
- Optionally clear existing metadata when applying metadata to existing containers.
|
||||
Selecting this option is only appropriate when setting type=meta
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
default: "no"
|
||||
container:
|
||||
description:
|
||||
- The container to use for container or metadata operations.
|
||||
required: true
|
||||
meta:
|
||||
description:
|
||||
- A hash of items to set as metadata values on a container
|
||||
private:
|
||||
description:
|
||||
- Used to set a container as private, removing it from the CDN. B(Warning!)
|
||||
Private containers, if previously made public, can have live objects
|
||||
available until the TTL on cached objects expires
|
||||
public:
|
||||
description:
|
||||
- Used to set a container as public, available via the Cloud Files CDN
|
||||
region:
|
||||
description:
|
||||
- Region to create an instance in
|
||||
default: DFW
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
ttl:
|
||||
description:
|
||||
- In seconds, set a container-wide TTL for all objects cached on CDN edge nodes.
|
||||
Setting a TTL is only appropriate for containers that are public
|
||||
type:
|
||||
description:
|
||||
- Type of object to do work on, i.e. metadata object or a container object
|
||||
choices:
|
||||
- file
|
||||
- meta
|
||||
default: file
|
||||
web_error:
|
||||
description:
|
||||
- Sets an object to be presented as the HTTP error page when accessed by the CDN URL
|
||||
web_index:
|
||||
description:
|
||||
- Sets an object to be presented as the HTTP index page when accessed by the CDN URL
|
||||
author: Paul Durivage
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "Test Cloud Files Containers"
|
||||
hosts: local
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "List all containers"
|
||||
rax_files: state=list
|
||||
|
||||
- name: "Create container called 'mycontainer'"
|
||||
rax_files: container=mycontainer
|
||||
|
||||
- name: "Create container 'mycontainer2' with metadata"
|
||||
rax_files:
|
||||
container: mycontainer2
|
||||
meta:
|
||||
key: value
|
||||
file_for: someuser@example.com
|
||||
|
||||
- name: "Set a container's web index page"
|
||||
rax_files: container=mycontainer web_index=index.html
|
||||
|
||||
- name: "Set a container's web error page"
|
||||
rax_files: container=mycontainer web_error=error.html
|
||||
|
||||
- name: "Make container public"
|
||||
rax_files: container=mycontainer public=yes
|
||||
|
||||
- name: "Make container public with a 24 hour TTL"
|
||||
rax_files: container=mycontainer public=yes ttl=86400
|
||||
|
||||
- name: "Make container private"
|
||||
rax_files: container=mycontainer private=yes
|
||||
|
||||
- name: "Test Cloud Files Containers Metadata Storage"
|
||||
hosts: local
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Get mycontainer2 metadata"
|
||||
rax_files:
|
||||
container: mycontainer2
|
||||
type: meta
|
||||
|
||||
- name: "Set mycontainer2 metadata"
|
||||
rax_files:
|
||||
container: mycontainer2
|
||||
type: meta
|
||||
meta:
|
||||
uploaded_by: someuser@example.com
|
||||
|
||||
- name: "Remove mycontainer2 metadata"
|
||||
rax_files:
|
||||
container: "mycontainer2"
|
||||
type: meta
|
||||
state: absent
|
||||
meta:
|
||||
key: ""
|
||||
file_for: ""
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError, e:
|
||||
HAS_PYRAX = False
|
||||
|
||||
EXIT_DICT = dict(success=True)
|
||||
META_PREFIX = 'x-container-meta-'
|
||||
|
||||
|
||||
def _get_container(module, cf, container):
|
||||
try:
|
||||
return cf.get_container(container)
|
||||
except pyrax.exc.NoSuchContainer, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
def _fetch_meta(module, container):
|
||||
EXIT_DICT['meta'] = dict()
|
||||
try:
|
||||
for k, v in container.get_metadata().items():
|
||||
split_key = k.split(META_PREFIX)[-1]
|
||||
EXIT_DICT['meta'][split_key] = v
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
def meta(cf, module, container_, state, meta_, clear_meta):
|
||||
c = _get_container(module, cf, container_)
|
||||
|
||||
if meta_ and state == 'present':
|
||||
try:
|
||||
meta_set = c.set_metadata(meta_, clear=clear_meta)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
elif meta_ and state == 'absent':
|
||||
remove_results = []
|
||||
for k, v in meta_.items():
|
||||
c.remove_metadata_key(k)
|
||||
remove_results.append(k)
|
||||
EXIT_DICT['deleted_meta_keys'] = remove_results
|
||||
elif state == 'absent':
|
||||
remove_results = []
|
||||
for k, v in c.get_metadata().items():
|
||||
c.remove_metadata_key(k)
|
||||
remove_results.append(k)
|
||||
EXIT_DICT['deleted_meta_keys'] = remove_results
|
||||
|
||||
_fetch_meta(module, c)
|
||||
_locals = locals().keys()
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
if 'meta_set' in _locals or 'remove_results' in _locals:
|
||||
EXIT_DICT['changed'] = True
|
||||
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def container(cf, module, container_, state, meta_, clear_meta, ttl, public,
|
||||
private, web_index, web_error):
|
||||
if public and private:
|
||||
module.fail_json(msg='container cannot be simultaneously '
|
||||
'set to public and private')
|
||||
|
||||
if state == 'absent' and (meta_ or clear_meta or public or private or web_index or web_error):
|
||||
module.fail_json(msg='state cannot be omitted when setting/removing '
|
||||
'attributes on a container')
|
||||
|
||||
if state == 'list':
|
||||
# We don't care if attributes are specified, let's list containers
|
||||
EXIT_DICT['containers'] = cf.list_containers()
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
try:
|
||||
c = cf.get_container(container_)
|
||||
except pyrax.exc.NoSuchContainer, e:
|
||||
# Make the container if state=present, otherwise bomb out
|
||||
if state == 'present':
|
||||
try:
|
||||
c = cf.create_container(container_)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['changed'] = True
|
||||
EXIT_DICT['created'] = True
|
||||
else:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
# Successfully grabbed a container object
|
||||
# Delete if state is absent
|
||||
if state == 'absent':
|
||||
try:
|
||||
cont_deleted = c.delete()
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['deleted'] = True
|
||||
|
||||
if meta_:
|
||||
try:
|
||||
meta_set = c.set_metadata(meta_, clear=clear_meta)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
finally:
|
||||
_fetch_meta(module, c)
|
||||
|
||||
if ttl:
|
||||
try:
|
||||
c.cdn_ttl = ttl
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['ttl'] = c.cdn_ttl
|
||||
|
||||
if public:
|
||||
try:
|
||||
cont_public = c.make_public()
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['container_urls'] = dict(url=c.cdn_uri,
|
||||
ssl_url=c.cdn_ssl_uri,
|
||||
streaming_url=c.cdn_streaming_uri,
|
||||
ios_uri=c.cdn_ios_uri)
|
||||
|
||||
if private:
|
||||
try:
|
||||
cont_private = c.make_private()
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['set_private'] = True
|
||||
|
||||
if web_index:
|
||||
try:
|
||||
cont_web_index = c.set_web_index_page(web_index)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['set_index'] = True
|
||||
finally:
|
||||
_fetch_meta(module, c)
|
||||
|
||||
if web_error:
|
||||
try:
|
||||
cont_err_index = c.set_web_error_page(web_error)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
EXIT_DICT['set_error'] = True
|
||||
finally:
|
||||
_fetch_meta(module, c)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['objs_in_container'] = c.object_count
|
||||
EXIT_DICT['total_bytes'] = c.total_bytes
|
||||
|
||||
_locals = locals().keys()
|
||||
if ('cont_deleted' in _locals
|
||||
or 'meta_set' in _locals
|
||||
or 'cont_public' in _locals
|
||||
or 'cont_private' in _locals
|
||||
or 'cont_web_index' in _locals
|
||||
or 'cont_err_index' in _locals):
|
||||
EXIT_DICT['changed'] = True
|
||||
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public,
|
||||
private, web_index, web_error):
|
||||
""" Dispatch from here to work with metadata or file objects """
|
||||
cf = pyrax.cloudfiles
|
||||
|
||||
if cf is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if typ == "container":
|
||||
container(cf, module, container_, state, meta_, clear_meta, ttl,
|
||||
public, private, web_index, web_error)
|
||||
else:
|
||||
meta(cf, module, container_, state, meta_, clear_meta)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
container=dict(),
|
||||
state=dict(choices=['present', 'absent', 'list'],
|
||||
default='present'),
|
||||
meta=dict(type='dict', default=dict()),
|
||||
clear_meta=dict(default=False, type='bool'),
|
||||
type=dict(choices=['container', 'meta'], default='container'),
|
||||
ttl=dict(type='int'),
|
||||
public=dict(default=False, type='bool'),
|
||||
private=dict(default=False, type='bool'),
|
||||
web_index=dict(),
|
||||
web_error=dict()
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
container_ = module.params.get('container')
|
||||
state = module.params.get('state')
|
||||
meta_ = module.params.get('meta')
|
||||
clear_meta = module.params.get('clear_meta')
|
||||
typ = module.params.get('type')
|
||||
ttl = module.params.get('ttl')
|
||||
public = module.params.get('public')
|
||||
private = module.params.get('private')
|
||||
web_index = module.params.get('web_index')
|
||||
web_error = module.params.get('web_error')
|
||||
|
||||
if state in ['present', 'absent'] and not container_:
|
||||
module.fail_json(msg='please specify a container name')
|
||||
if clear_meta and not typ == 'meta':
|
||||
module.fail_json(msg='clear_meta can only be used when setting '
|
||||
'metadata')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public,
|
||||
private, web_index, web_error)
|
||||
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
main()
|
@ -1,603 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Paul Durivage <paul.durivage@rackspace.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_files_objects
|
||||
short_description: Upload, download, and delete objects in Rackspace Cloud Files
|
||||
description:
|
||||
- Upload, download, and delete objects in Rackspace Cloud Files
|
||||
version_added: "1.5"
|
||||
options:
|
||||
clear_meta:
|
||||
description:
|
||||
- Optionally clear existing metadata when applying metadata to existing objects.
|
||||
Selecting this option is only appropriate when setting type=meta
|
||||
choices:
|
||||
- "yes"
|
||||
- "no"
|
||||
default: "no"
|
||||
container:
|
||||
description:
|
||||
- The container to use for file object operations.
|
||||
required: true
|
||||
default: null
|
||||
dest:
|
||||
description:
|
||||
- The destination of a "get" operation; i.e. a local directory, "/home/user/myfolder".
|
||||
Used to specify the destination of an operation on a remote object; i.e. a file name,
|
||||
"file1", or a comma-separated list of remote objects, "file1,file2,file17"
|
||||
expires:
|
||||
description:
|
||||
- Used to set an expiration on a file or folder uploaded to Cloud Files.
|
||||
Requires an integer, specifying expiration in seconds
|
||||
default: null
|
||||
meta:
|
||||
description:
|
||||
- A hash of items to set as metadata values on an uploaded file or folder
|
||||
default: null
|
||||
method:
|
||||
description:
|
||||
- The method of operation to be performed. For example, put to upload files
|
||||
to Cloud Files, get to download files from Cloud Files or delete to delete
|
||||
remote objects in Cloud Files
|
||||
choices:
|
||||
- get
|
||||
- put
|
||||
- delete
|
||||
default: get
|
||||
src:
|
||||
description:
|
||||
- Source from which to upload files. Used to specify a remote object as a source for
|
||||
an operation, i.e. a file name, "file1", or a comma-separated list of remote objects,
|
||||
"file1,file2,file17". src and dest are mutually exclusive on remote-only object operations
|
||||
default: null
|
||||
structure:
|
||||
description:
|
||||
- Used to specify whether to maintain nested directory structure when downloading objects
|
||||
from Cloud Files. Setting to false downloads the contents of a container to a single,
|
||||
flat directory
|
||||
choices:
|
||||
- yes
|
||||
- "no"
|
||||
default: "yes"
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type:
|
||||
description:
|
||||
- Type of object to do work on
|
||||
- Metadata object or a file object
|
||||
choices:
|
||||
- file
|
||||
- meta
|
||||
default: file
|
||||
author: Paul Durivage
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: "Test Cloud Files Objects"
|
||||
hosts: local
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: "Get objects from test container"
|
||||
rax_files_objects: container=testcont dest=~/Downloads/testcont
|
||||
|
||||
- name: "Get single object from test container"
|
||||
rax_files_objects: container=testcont src=file1 dest=~/Downloads/testcont
|
||||
|
||||
- name: "Get several objects from test container"
|
||||
rax_files_objects: container=testcont src=file1,file2,file3 dest=~/Downloads/testcont
|
||||
|
||||
- name: "Delete one object in test container"
|
||||
rax_files_objects: container=testcont method=delete dest=file1
|
||||
|
||||
- name: "Delete several objects in test container"
|
||||
rax_files_objects: container=testcont method=delete dest=file2,file3,file4
|
||||
|
||||
- name: "Delete all objects in test container"
|
||||
rax_files_objects: container=testcont method=delete
|
||||
|
||||
- name: "Upload all files to test container"
|
||||
rax_files_objects: container=testcont method=put src=~/Downloads/onehundred
|
||||
|
||||
- name: "Upload one file to test container"
|
||||
rax_files_objects: container=testcont method=put src=~/Downloads/testcont/file1
|
||||
|
||||
- name: "Upload one file to test container with metadata"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
src: ~/Downloads/testcont/file2
|
||||
method: put
|
||||
meta:
|
||||
testkey: testdata
|
||||
who_uploaded_this: someuser@example.com
|
||||
|
||||
- name: "Upload one file to test container with TTL of 60 seconds"
|
||||
rax_files_objects: container=testcont method=put src=~/Downloads/testcont/file3 expires=60
|
||||
|
||||
- name: "Attempt to get remote object that does not exist"
|
||||
rax_files_objects: container=testcont method=get src=FileThatDoesNotExist.jpg dest=~/Downloads/testcont
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Attempt to delete remote object that does not exist"
|
||||
rax_files_objects: container=testcont method=delete dest=FileThatDoesNotExist.jpg
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Test Cloud Files Objects Metadata"
|
||||
hosts: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: "Get metadata on one object"
|
||||
rax_files_objects: container=testcont type=meta dest=file2
|
||||
|
||||
- name: "Get metadata on several objects"
|
||||
rax_files_objects: container=testcont type=meta src=file2,file1
|
||||
|
||||
- name: "Set metadata on an object"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
dest: file17
|
||||
method: put
|
||||
meta:
|
||||
key1: value1
|
||||
key2: value2
|
||||
clear_meta: true
|
||||
|
||||
- name: "Verify metadata is set"
|
||||
rax_files_objects: container=testcont type=meta src=file17
|
||||
|
||||
- name: "Delete metadata"
|
||||
rax_files_objects:
|
||||
container: testcont
|
||||
type: meta
|
||||
dest: file17
|
||||
method: delete
|
||||
meta:
|
||||
key1: ''
|
||||
key2: ''
|
||||
|
||||
- name: "Get metadata on all objects"
|
||||
rax_files_objects: container=testcont type=meta
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
EXIT_DICT = dict(success=False)
|
||||
META_PREFIX = 'x-object-meta-'
|
||||
|
||||
|
||||
def _get_container(module, cf, container):
|
||||
try:
|
||||
return cf.get_container(container)
|
||||
except pyrax.exc.NoSuchContainer, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
|
||||
def upload(module, cf, container, src, dest, meta, expires):
|
||||
""" Uploads a single object or a folder to Cloud Files Optionally sets an
|
||||
metadata, TTL value (expires), or Content-Disposition and Content-Encoding
|
||||
headers.
|
||||
"""
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
num_objs_before = len(c.get_object_names())
|
||||
|
||||
if not src:
|
||||
module.fail_json(msg='src must be specified when uploading')
|
||||
|
||||
src = os.path.abspath(os.path.expanduser(src))
|
||||
is_dir = os.path.isdir(src)
|
||||
|
||||
if not is_dir and not os.path.isfile(src) or not os.path.exists(src):
|
||||
module.fail_json(msg='src must be a file or a directory')
|
||||
if dest and is_dir:
|
||||
module.fail_json(msg='dest cannot be set when whole '
|
||||
'directories are uploaded')
|
||||
|
||||
cont_obj = None
|
||||
if dest and not is_dir:
|
||||
try:
|
||||
cont_obj = c.upload_file(src, obj_name=dest, ttl=expires)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
elif is_dir:
|
||||
try:
|
||||
id, total_bytes = cf.upload_folder(src, container=c.name, ttl=expires)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
while True:
|
||||
bytes = cf.get_uploaded(id)
|
||||
if bytes == total_bytes:
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
try:
|
||||
cont_obj = c.upload_file(src, ttl=expires)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
num_objs_after = len(c.get_object_names())
|
||||
|
||||
if not meta:
|
||||
meta = dict()
|
||||
|
||||
meta_result = dict()
|
||||
if meta:
|
||||
if cont_obj:
|
||||
meta_result = cont_obj.set_metadata(meta)
|
||||
else:
|
||||
def _set_meta(objs, meta):
|
||||
""" Sets metadata on a list of objects specified by name """
|
||||
for obj in objs:
|
||||
try:
|
||||
result = c.get_object(obj).set_metadata(meta)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
meta_result[obj] = result
|
||||
return meta_result
|
||||
|
||||
def _walker(objs, path, filenames):
|
||||
""" Callback func for os.path.walk """
|
||||
prefix = ''
|
||||
if path != src:
|
||||
prefix = path.split(src)[-1].lstrip('/')
|
||||
filenames = [os.path.join(prefix, name) for name in filenames
|
||||
if not os.path.isdir(name)]
|
||||
objs += filenames
|
||||
|
||||
_objs = []
|
||||
os.path.walk(src, _walker, _objs)
|
||||
meta_result = _set_meta(_objs, meta)
|
||||
|
||||
EXIT_DICT['success'] = True
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['msg'] = "Uploaded %s to container: %s" % (src, c.name)
|
||||
if cont_obj or locals().get('bytes'):
|
||||
EXIT_DICT['changed'] = True
|
||||
if meta_result:
|
||||
EXIT_DICT['meta'] = dict(updated=True)
|
||||
|
||||
if cont_obj:
|
||||
EXIT_DICT['bytes'] = cont_obj.total_bytes
|
||||
EXIT_DICT['etag'] = cont_obj.etag
|
||||
else:
|
||||
EXIT_DICT['bytes'] = total_bytes
|
||||
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def download(module, cf, container, src, dest, structure):
|
||||
""" Download objects from Cloud Files to a local path specified by "dest".
|
||||
Optionally disable maintaining a directory structure by by passing a
|
||||
false value to "structure".
|
||||
"""
|
||||
# Looking for an explicit destination
|
||||
if not dest:
|
||||
module.fail_json(msg='dest is a required argument when '
|
||||
'downloading from Cloud Files')
|
||||
|
||||
# Attempt to fetch the container by name
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
# Accept a single object name or a comma-separated list of objs
|
||||
# If not specified, get the entire container
|
||||
if src:
|
||||
objs = src.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
else:
|
||||
objs = c.get_object_names()
|
||||
|
||||
dest = os.path.abspath(os.path.expanduser(dest))
|
||||
is_dir = os.path.isdir(dest)
|
||||
|
||||
if not is_dir:
|
||||
module.fail_json(msg='dest must be a directory')
|
||||
|
||||
results = []
|
||||
for obj in objs:
|
||||
try:
|
||||
c.download_object(obj, dest, structure=structure)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(obj)
|
||||
|
||||
len_results = len(results)
|
||||
len_objs = len(objs)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['requested_downloaded'] = results
|
||||
if results:
|
||||
EXIT_DICT['changed'] = True
|
||||
if len_results == len_objs:
|
||||
EXIT_DICT['success'] = True
|
||||
EXIT_DICT['msg'] = "%s objects downloaded to %s" % (len_results, dest)
|
||||
else:
|
||||
EXIT_DICT['msg'] = "Error: only %s of %s objects were " \
|
||||
"downloaded" % (len_results, len_objs)
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def delete(module, cf, container, src, dest):
|
||||
""" Delete specific objects by proving a single file name or a
|
||||
comma-separated list to src OR dest (but not both). Omitting file name(s)
|
||||
assumes the entire container is to be deleted.
|
||||
"""
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; files to be deleted "
|
||||
"have been specified on both src and dest args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
if objs:
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
else:
|
||||
objs = c.get_object_names()
|
||||
|
||||
num_objs = len(objs)
|
||||
|
||||
results = []
|
||||
for obj in objs:
|
||||
try:
|
||||
result = c.delete_object(obj)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(result)
|
||||
|
||||
num_deleted = results.count(True)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['deleted'] = num_deleted
|
||||
EXIT_DICT['requested_deleted'] = objs
|
||||
|
||||
if num_deleted:
|
||||
EXIT_DICT['changed'] = True
|
||||
|
||||
if num_objs == num_deleted:
|
||||
EXIT_DICT['success'] = True
|
||||
EXIT_DICT['msg'] = "%s objects deleted" % num_deleted
|
||||
else:
|
||||
EXIT_DICT['msg'] = ("Error: only %s of %s objects "
|
||||
"deleted" % (num_deleted, num_objs))
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def get_meta(module, cf, container, src, dest):
|
||||
""" Get metadata for a single file, comma-separated list, or entire
|
||||
container
|
||||
"""
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; files to be deleted "
|
||||
"have been specified on both src and dest args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
if objs:
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
else:
|
||||
objs = c.get_object_names()
|
||||
|
||||
results = dict()
|
||||
for obj in objs:
|
||||
try:
|
||||
meta = c.get_object(obj).get_metadata()
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results[obj] = dict()
|
||||
for k, v in meta.items():
|
||||
meta_key = k.split(META_PREFIX)[-1]
|
||||
results[obj][meta_key] = v
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
if results:
|
||||
EXIT_DICT['meta_results'] = results
|
||||
EXIT_DICT['success'] = True
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def put_meta(module, cf, container, src, dest, meta, clear_meta):
|
||||
""" Set metadata on a container, single file, or comma-separated list.
|
||||
Passing a true value to clear_meta clears the metadata stored in Cloud
|
||||
Files before setting the new metadata to the value of "meta".
|
||||
"""
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; files to set meta"
|
||||
" have been specified on both src and dest args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
results = []
|
||||
for obj in objs:
|
||||
try:
|
||||
result = c.get_object(obj).set_metadata(meta, clear=clear_meta)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(result)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['success'] = True
|
||||
if results:
|
||||
EXIT_DICT['changed'] = True
|
||||
EXIT_DICT['num_changed'] = True
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def delete_meta(module, cf, container, src, dest, meta):
|
||||
""" Removes metadata keys and values specified in meta, if any. Deletes on
|
||||
all objects specified by src or dest (but not both), if any; otherwise it
|
||||
deletes keys on all objects in the container
|
||||
"""
|
||||
objs = None
|
||||
if src and dest:
|
||||
module.fail_json(msg="Error: ambiguous instructions; meta keys to be "
|
||||
"deleted have been specified on both src and dest"
|
||||
" args")
|
||||
elif dest:
|
||||
objs = dest
|
||||
else:
|
||||
objs = src
|
||||
|
||||
objs = objs.split(',')
|
||||
objs = map(str.strip, objs)
|
||||
|
||||
c = _get_container(module, cf, container)
|
||||
|
||||
results = [] # Num of metadata keys removed, not objects affected
|
||||
for obj in objs:
|
||||
if meta:
|
||||
for k, v in meta.items():
|
||||
try:
|
||||
result = c.get_object(obj).remove_metadata_key(k)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
else:
|
||||
results.append(result)
|
||||
else:
|
||||
try:
|
||||
o = c.get_object(obj)
|
||||
except pyrax.exc.NoSuchObject, e:
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
for k, v in o.get_metadata().items():
|
||||
try:
|
||||
result = o.remove_metadata_key(k)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.message)
|
||||
results.append(result)
|
||||
|
||||
EXIT_DICT['container'] = c.name
|
||||
EXIT_DICT['success'] = True
|
||||
if results:
|
||||
EXIT_DICT['changed'] = True
|
||||
EXIT_DICT['num_deleted'] = len(results)
|
||||
module.exit_json(**EXIT_DICT)
|
||||
|
||||
|
||||
def cloudfiles(module, container, src, dest, method, typ, meta, clear_meta,
|
||||
structure, expires):
|
||||
""" Dispatch from here to work with metadata or file objects """
|
||||
cf = pyrax.cloudfiles
|
||||
|
||||
if cf is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if typ == "file":
|
||||
if method == 'put':
|
||||
upload(module, cf, container, src, dest, meta, expires)
|
||||
|
||||
elif method == 'get':
|
||||
download(module, cf, container, src, dest, structure)
|
||||
|
||||
elif method == 'delete':
|
||||
delete(module, cf, container, src, dest)
|
||||
|
||||
else:
|
||||
if method == 'get':
|
||||
get_meta(module, cf, container, src, dest)
|
||||
|
||||
if method == 'put':
|
||||
put_meta(module, cf, container, src, dest, meta, clear_meta)
|
||||
|
||||
if method == 'delete':
|
||||
delete_meta(module, cf, container, src, dest, meta)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
container=dict(required=True),
|
||||
src=dict(),
|
||||
dest=dict(),
|
||||
method=dict(default='get', choices=['put', 'get', 'delete']),
|
||||
type=dict(default='file', choices=['file', 'meta']),
|
||||
meta=dict(type='dict', default=dict()),
|
||||
clear_meta=dict(default=False, type='bool'),
|
||||
structure=dict(default=True, type='bool'),
|
||||
expires=dict(type='int'),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
container = module.params.get('container')
|
||||
src = module.params.get('src')
|
||||
dest = module.params.get('dest')
|
||||
method = module.params.get('method')
|
||||
typ = module.params.get('type')
|
||||
meta = module.params.get('meta')
|
||||
clear_meta = module.params.get('clear_meta')
|
||||
structure = module.params.get('structure')
|
||||
expires = module.params.get('expires')
|
||||
|
||||
if clear_meta and not typ == 'meta':
|
||||
module.fail_json(msg='clear_meta can only be used when setting metadata')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
cloudfiles(module, container, src, dest, method, typ, meta, clear_meta, structure, expires)
|
||||
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
main()
|
@ -1,110 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_identity
|
||||
short_description: Load Rackspace Cloud Identity
|
||||
description:
|
||||
- Verifies Rackspace Cloud credentials and returns identity information
|
||||
version_added: "1.5"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
author: Christopher H. Laco, Matt Martz
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Load Rackspace Cloud Identity
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Load Identity
|
||||
local_action:
|
||||
module: rax_identity
|
||||
credentials: ~/.raxpub
|
||||
region: DFW
|
||||
register: rackspace_identity
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def cloud_identity(module, state, identity):
|
||||
for arg in (state, identity):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for rax_identity' % arg)
|
||||
|
||||
instance = dict(
|
||||
authenticated=identity.authenticated,
|
||||
credentials=identity._creds_file
|
||||
)
|
||||
changed = False
|
||||
|
||||
instance.update(rax_to_dict(identity))
|
||||
instance['services'] = instance.get('services', {}).keys()
|
||||
|
||||
if state == 'present':
|
||||
if not identity.authenticated:
|
||||
module.fail_json(msg='Credentials could not be verified!')
|
||||
|
||||
module.exit_json(changed=changed, identity=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
if pyrax.identity is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
cloud_identity(module, state, pyrax.identity)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,174 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_keypair
|
||||
short_description: Create a keypair for use with Rackspace Cloud Servers
|
||||
description:
|
||||
- Create a keypair for use with Rackspace Cloud Servers
|
||||
version_added: 1.5
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of keypair
|
||||
required: true
|
||||
public_key:
|
||||
description:
|
||||
- Public Key string to upload. Can be a file path or string
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author: Matt Martz
|
||||
notes:
|
||||
- Keypairs cannot be manipulated, only created and deleted. To "update" a
|
||||
keypair you must first delete and then recreate.
|
||||
- The ability to specify a file path for the public key was added in 1.7
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a keypair
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: keypair request
|
||||
local_action:
|
||||
module: rax_keypair
|
||||
credentials: ~/.raxpub
|
||||
name: my_keypair
|
||||
region: DFW
|
||||
register: keypair
|
||||
- name: Create local public key
|
||||
local_action:
|
||||
module: copy
|
||||
content: "{{ keypair.keypair.public_key }}"
|
||||
dest: "{{ inventory_dir }}/{{ keypair.keypair.name }}.pub"
|
||||
- name: Create local private key
|
||||
local_action:
|
||||
module: copy
|
||||
content: "{{ keypair.keypair.private_key }}"
|
||||
dest: "{{ inventory_dir }}/{{ keypair.keypair.name }}"
|
||||
|
||||
- name: Create a keypair
|
||||
hosts: localhost
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: keypair request
|
||||
local_action:
|
||||
module: rax_keypair
|
||||
credentials: ~/.raxpub
|
||||
name: my_keypair
|
||||
public_key: "{{ lookup('file', 'authorized_keys/id_rsa.pub') }}"
|
||||
region: DFW
|
||||
register: keypair
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_keypair(module, name, public_key, state):
|
||||
changed = False
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
keypair = {}
|
||||
|
||||
if state == 'present':
|
||||
if os.path.isfile(public_key):
|
||||
try:
|
||||
f = open(public_key)
|
||||
public_key = f.read()
|
||||
f.close()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='Failed to load %s' % public_key)
|
||||
|
||||
try:
|
||||
keypair = cs.keypairs.find(name=name)
|
||||
except cs.exceptions.NotFound:
|
||||
try:
|
||||
keypair = cs.keypairs.create(name, public_key)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
keypair = cs.keypairs.find(name=name)
|
||||
except:
|
||||
pass
|
||||
|
||||
if keypair:
|
||||
try:
|
||||
keypair.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, keypair=rax_to_dict(keypair))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(),
|
||||
public_key=dict(),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
name = module.params.get('name')
|
||||
public_key = module.params.get('public_key')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_keypair(module, name, public_key, state)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,178 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_meta
|
||||
short_description: Manipulate metadata for Rackspace Cloud Servers
|
||||
description:
|
||||
- Manipulate metadata for Rackspace Cloud Servers
|
||||
version_added: 1.7
|
||||
options:
|
||||
address:
|
||||
description:
|
||||
- Server IP address to modify metadata for, will match any IP assigned to
|
||||
the server
|
||||
id:
|
||||
description:
|
||||
- Server ID to modify metadata for
|
||||
name:
|
||||
description:
|
||||
- Server name to modify metadata for
|
||||
default: null
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
default: null
|
||||
author: Matt Martz
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Set metadata for a server
|
||||
hosts: all
|
||||
gather_facts: False
|
||||
tasks:
|
||||
- name: Set metadata
|
||||
local_action:
|
||||
module: rax_meta
|
||||
credentials: ~/.raxpub
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
meta:
|
||||
group: primary_group
|
||||
groups:
|
||||
- group_two
|
||||
- group_three
|
||||
app: my_app
|
||||
|
||||
- name: Clear metadata
|
||||
local_action:
|
||||
module: rax_meta
|
||||
credentials: ~/.raxpub
|
||||
name: "{{ inventory_hostname }}"
|
||||
region: DFW
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_meta(module, address, name, server_id, meta):
|
||||
changed = False
|
||||
|
||||
cs = pyrax.cloudservers
|
||||
|
||||
if cs is None:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
search_opts = {}
|
||||
if name:
|
||||
search_opts = dict(name='^%s$' % name)
|
||||
try:
|
||||
servers = cs.servers.list(search_opts=search_opts)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif address:
|
||||
servers = []
|
||||
try:
|
||||
for server in cs.servers.list():
|
||||
for addresses in server.networks.values():
|
||||
if address in addresses:
|
||||
servers.append(server)
|
||||
break
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
elif server_id:
|
||||
servers = []
|
||||
try:
|
||||
servers.append(cs.servers.get(server_id))
|
||||
except Exception, e:
|
||||
pass
|
||||
|
||||
if len(servers) > 1:
|
||||
module.fail_json(msg='Multiple servers found matching provided '
|
||||
'search parameters')
|
||||
elif not servers:
|
||||
module.fail_json(msg='Failed to find a server matching provided '
|
||||
'search parameters')
|
||||
|
||||
# Normalize and ensure all metadata values are strings
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, list):
|
||||
meta[k] = ','.join(['%s' % i for i in v])
|
||||
elif isinstance(v, dict):
|
||||
meta[k] = json.dumps(v)
|
||||
elif not isinstance(v, basestring):
|
||||
meta[k] = '%s' % v
|
||||
|
||||
server = servers[0]
|
||||
if server.metadata == meta:
|
||||
changed = False
|
||||
else:
|
||||
changed = True
|
||||
removed = set(server.metadata.keys()).difference(meta.keys())
|
||||
cs.servers.delete_meta(server, list(removed))
|
||||
cs.servers.set_meta(server, meta)
|
||||
server.get()
|
||||
|
||||
module.exit_json(changed=changed, meta=server.metadata)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
address=dict(),
|
||||
id=dict(),
|
||||
name=dict(),
|
||||
meta=dict(type='dict', default=dict()),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[['address', 'id', 'name']],
|
||||
required_one_of=[['address', 'id', 'name']],
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
address = module.params.get('address')
|
||||
server_id = module.params.get('id')
|
||||
name = module.params.get('name')
|
||||
meta = module.params.get('meta')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_meta(module, address, name, server_id, meta)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,145 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_network
|
||||
short_description: create / delete an isolated network in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud isolated network.
|
||||
version_added: "1.4"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
label:
|
||||
description:
|
||||
- Label (name) to give the network
|
||||
default: null
|
||||
cidr:
|
||||
description:
|
||||
- cidr of the network being created
|
||||
default: null
|
||||
author: Christopher H. Laco, Jesse Keating
|
||||
extends_documentation_fragment: rackspace.openstack
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build an Isolated Network
|
||||
gather_facts: False
|
||||
|
||||
tasks:
|
||||
- name: Network create request
|
||||
local_action:
|
||||
module: rax_network
|
||||
credentials: ~/.raxpub
|
||||
label: my-net
|
||||
cidr: 192.168.3.0/24
|
||||
state: present
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def cloud_network(module, state, label, cidr):
|
||||
for arg in (state, label, cidr):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for cloud_networks' % arg)
|
||||
|
||||
changed = False
|
||||
network = None
|
||||
networks = []
|
||||
|
||||
if not pyrax.cloud_networks:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
try:
|
||||
network = pyrax.cloud_networks.find_network_by_label(label)
|
||||
except pyrax.exceptions.NetworkNotFound:
|
||||
try:
|
||||
network = pyrax.cloud_networks.create(label, cidr=cidr)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
network = pyrax.cloud_networks.find_network_by_label(label)
|
||||
network.delete()
|
||||
changed = True
|
||||
except pyrax.exceptions.NetworkNotFound:
|
||||
pass
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if network:
|
||||
instance = dict(id=network.id,
|
||||
label=network.label,
|
||||
cidr=network.cidr)
|
||||
networks.append(instance)
|
||||
|
||||
module.exit_json(changed=changed, networks=networks)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent']),
|
||||
label=dict(),
|
||||
cidr=dict()
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
state = module.params.get('state')
|
||||
label = module.params.get('label')
|
||||
cidr = module.params.get('cidr')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_network(module, state, label, cidr)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,145 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_queue
|
||||
short_description: create / delete a queue in Rackspace Public Cloud
|
||||
description:
|
||||
- creates / deletes a Rackspace Public Cloud queue.
|
||||
version_added: "1.5"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name to give the queue
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author: Christopher H. Laco, Matt Martz
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Build a Queue
|
||||
gather_facts: False
|
||||
hosts: local
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Queue create request
|
||||
local_action:
|
||||
module: rax_queue
|
||||
credentials: ~/.raxpub
|
||||
name: my-queue
|
||||
region: DFW
|
||||
state: present
|
||||
register: my_queue
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def cloud_queue(module, state, name):
|
||||
for arg in (state, name):
|
||||
if not arg:
|
||||
module.fail_json(msg='%s is required for rax_queue' % arg)
|
||||
|
||||
changed = False
|
||||
queues = []
|
||||
instance = {}
|
||||
|
||||
cq = pyrax.queues
|
||||
if not cq:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
for queue in cq.list():
|
||||
if name != queue.name:
|
||||
continue
|
||||
|
||||
queues.append(queue)
|
||||
|
||||
if len(queues) > 1:
|
||||
module.fail_json(msg='Multiple Queues were matched by name')
|
||||
|
||||
if state == 'present':
|
||||
if not queues:
|
||||
try:
|
||||
queue = cq.create(name)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
queue = queues[0]
|
||||
|
||||
instance = dict(name=queue.name)
|
||||
result = dict(changed=changed, queue=instance)
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == 'absent':
|
||||
if queues:
|
||||
queue = queues[0]
|
||||
try:
|
||||
queue.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, queue=instance)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
name=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together()
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
name = module.params.get('name')
|
||||
state = module.params.get('state')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
cloud_queue(module, state, name)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
### invoke the module
|
||||
main()
|
@ -1,351 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_scaling_group
|
||||
short_description: Manipulate Rackspace Cloud Autoscale Groups
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Autoscale Groups
|
||||
version_added: 1.7
|
||||
options:
|
||||
cooldown:
|
||||
description:
|
||||
- The period of time, in seconds, that must pass before any scaling can
|
||||
occur after the previous scaling. Must be an integer between 0 and
|
||||
86400 (24 hrs).
|
||||
disk_config:
|
||||
description:
|
||||
- Disk partitioning strategy
|
||||
choices:
|
||||
- auto
|
||||
- manual
|
||||
default: auto
|
||||
files:
|
||||
description:
|
||||
- 'Files to insert into the instance. Hash of C(remotepath: localpath)'
|
||||
default: null
|
||||
flavor:
|
||||
description:
|
||||
- flavor to use for the instance
|
||||
required: true
|
||||
image:
|
||||
description:
|
||||
- image to use for the instance. Can be an C(id), C(human_id) or C(name)
|
||||
required: true
|
||||
key_name:
|
||||
description:
|
||||
- key pair to use on the instance
|
||||
default: null
|
||||
loadbalancers:
|
||||
description:
|
||||
- List of load balancer C(id) and C(port) hashes
|
||||
max_entities:
|
||||
description:
|
||||
- The maximum number of entities that are allowed in the scaling group.
|
||||
Must be an integer between 0 and 1000.
|
||||
required: true
|
||||
meta:
|
||||
description:
|
||||
- A hash of metadata to associate with the instance
|
||||
default: null
|
||||
min_entities:
|
||||
description:
|
||||
- The minimum number of entities that are allowed in the scaling group.
|
||||
Must be an integer between 0 and 1000.
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- Name to give the scaling group
|
||||
required: true
|
||||
networks:
|
||||
description:
|
||||
- The network to attach to the instances. If specified, you must include
|
||||
ALL networks including the public and private interfaces. Can be C(id)
|
||||
or C(label).
|
||||
default:
|
||||
- public
|
||||
- private
|
||||
server_name:
|
||||
description:
|
||||
- The base name for servers created by Autoscale
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author: Matt Martz
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
---
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tasks:
|
||||
- rax_scaling_group:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
cooldown: 300
|
||||
flavor: performance1-1
|
||||
image: bb02b1a3-bc77-4d17-ab5b-421d89850fca
|
||||
min_entities: 5
|
||||
max_entities: 10
|
||||
name: ASG Test
|
||||
server_name: asgtest
|
||||
loadbalancers:
|
||||
- id: 228385
|
||||
port: 80
|
||||
register: asg
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_asg(module, cooldown=300, disk_config=None, files={}, flavor=None,
|
||||
image=None, key_name=None, loadbalancers=[], meta={},
|
||||
min_entities=0, max_entities=0, name=None, networks=[],
|
||||
server_name=None, state='present'):
|
||||
changed = False
|
||||
|
||||
au = pyrax.autoscale
|
||||
cnw = pyrax.cloud_networks
|
||||
cs = pyrax.cloudservers
|
||||
if not au or not cnw or not cs:
|
||||
module.fail_json(msg='Failed to instantiate clients. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
if state == 'present':
|
||||
# Normalize and ensure all metadata values are strings
|
||||
if meta:
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, list):
|
||||
meta[k] = ','.join(['%s' % i for i in v])
|
||||
elif isinstance(v, dict):
|
||||
meta[k] = json.dumps(v)
|
||||
elif not isinstance(v, basestring):
|
||||
meta[k] = '%s' % v
|
||||
|
||||
if image:
|
||||
image = rax_find_image(module, pyrax, image)
|
||||
|
||||
nics = []
|
||||
if networks:
|
||||
for network in networks:
|
||||
nics.extend(rax_find_network(module, pyrax, network))
|
||||
|
||||
for nic in nics:
|
||||
# pyrax is currently returning net-id, but we need uuid
|
||||
# this check makes this forward compatible for a time when
|
||||
# pyrax uses uuid instead
|
||||
if nic.get('net-id'):
|
||||
nic.update(uuid=nic['net-id'])
|
||||
del nic['net-id']
|
||||
|
||||
# Handle the file contents
|
||||
personality = []
|
||||
if files:
|
||||
for rpath in files.keys():
|
||||
lpath = os.path.expanduser(files[rpath])
|
||||
try:
|
||||
f = open(lpath, 'r')
|
||||
personality.append({
|
||||
'path': rpath,
|
||||
'contents': f.read()
|
||||
})
|
||||
f.close()
|
||||
except Exception, e:
|
||||
module.fail_json(msg='Failed to load %s' % lpath)
|
||||
|
||||
lbs = []
|
||||
if loadbalancers:
|
||||
for lb in loadbalancers:
|
||||
lb_id = lb.get('id')
|
||||
port = lb.get('port')
|
||||
if not lb_id or not port:
|
||||
continue
|
||||
lbs.append((lb_id, port))
|
||||
|
||||
try:
|
||||
sg = au.find(name=name)
|
||||
except pyrax.exceptions.NoUniqueMatch, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
except pyrax.exceptions.NotFound:
|
||||
try:
|
||||
sg = au.create(name, cooldown=cooldown,
|
||||
min_entities=min_entities,
|
||||
max_entities=max_entities,
|
||||
launch_config_type='launch_server',
|
||||
server_name=server_name, image=image,
|
||||
flavor=flavor, disk_config=disk_config,
|
||||
metadata=meta, personality=files,
|
||||
networks=nics, load_balancers=lbs,
|
||||
key_name=key_name)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if not changed:
|
||||
# Scaling Group Updates
|
||||
group_args = {}
|
||||
if cooldown != sg.cooldown:
|
||||
group_args['cooldown'] = cooldown
|
||||
|
||||
if min_entities != sg.min_entities:
|
||||
group_args['min_entities'] = min_entities
|
||||
|
||||
if max_entities != sg.max_entities:
|
||||
group_args['max_entities'] = max_entities
|
||||
|
||||
if group_args:
|
||||
changed = True
|
||||
sg.update(**group_args)
|
||||
|
||||
# Launch Configuration Updates
|
||||
lc = sg.get_launch_config()
|
||||
lc_args = {}
|
||||
if server_name != lc.get('name'):
|
||||
lc_args['name'] = server_name
|
||||
|
||||
if image != lc.get('image'):
|
||||
lc_args['image'] = image
|
||||
|
||||
if flavor != lc.get('flavor'):
|
||||
lc_args['flavor'] = flavor
|
||||
|
||||
if disk_config != lc.get('disk_config'):
|
||||
lc_args['disk_config'] = disk_config
|
||||
|
||||
if meta != lc.get('metadata'):
|
||||
lc_args['metadata'] = meta
|
||||
|
||||
if files != lc.get('personality'):
|
||||
lc_args['personality'] = files
|
||||
|
||||
if nics != lc.get('networks'):
|
||||
lc_args['networks'] = nics
|
||||
|
||||
if lbs != lc.get('load_balancers'):
|
||||
# Work around for https://github.com/rackspace/pyrax/pull/393
|
||||
lc_args['load_balancers'] = sg.manager._resolve_lbs(lbs)
|
||||
|
||||
if key_name != lc.get('key_name'):
|
||||
lc_args['key_name'] = key_name
|
||||
|
||||
if lc_args:
|
||||
# Work around for https://github.com/rackspace/pyrax/pull/389
|
||||
if 'flavor' not in lc_args:
|
||||
lc_args['flavor'] = lc.get('flavor')
|
||||
changed = True
|
||||
sg.update_launch_config(**lc_args)
|
||||
|
||||
sg.get()
|
||||
|
||||
module.exit_json(changed=changed, autoscale_group=rax_to_dict(sg))
|
||||
|
||||
else:
|
||||
try:
|
||||
sg = au.find(name=name)
|
||||
sg.delete()
|
||||
changed = True
|
||||
except pyrax.exceptions.NotFound, e:
|
||||
sg = {}
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, autoscale_group=rax_to_dict(sg))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
cooldown=dict(type='int', default=300),
|
||||
disk_config=dict(choices=['auto', 'manual']),
|
||||
files=dict(type='list', default=[]),
|
||||
flavor=dict(required=True),
|
||||
image=dict(required=True),
|
||||
key_name=dict(),
|
||||
loadbalancers=dict(type='list'),
|
||||
meta=dict(type='dict', default={}),
|
||||
min_entities=dict(type='int', required=True),
|
||||
max_entities=dict(type='int', required=True),
|
||||
name=dict(required=True),
|
||||
networks=dict(type='list', default=['public', 'private']),
|
||||
server_name=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
cooldown = module.params.get('cooldown')
|
||||
disk_config = module.params.get('disk_config')
|
||||
if disk_config:
|
||||
disk_config = disk_config.upper()
|
||||
files = module.params.get('files')
|
||||
flavor = module.params.get('flavor')
|
||||
image = module.params.get('image')
|
||||
key_name = module.params.get('key_name')
|
||||
loadbalancers = module.params.get('loadbalancers')
|
||||
meta = module.params.get('meta')
|
||||
min_entities = module.params.get('min_entities')
|
||||
max_entities = module.params.get('max_entities')
|
||||
name = module.params.get('name')
|
||||
networks = module.params.get('networks')
|
||||
server_name = module.params.get('server_name')
|
||||
state = module.params.get('state')
|
||||
|
||||
if not 0 <= min_entities <= 1000 or not 0 <= max_entities <= 1000:
|
||||
module.fail_json(msg='min_entities and max_entities must be an '
|
||||
'integer between 0 and 1000')
|
||||
|
||||
if not 0 <= cooldown <= 86400:
|
||||
module.fail_json(msg='cooldown must be an integer between 0 and 86400')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_asg(module, cooldown=cooldown, disk_config=disk_config,
|
||||
files=files, flavor=flavor, image=image, meta=meta,
|
||||
key_name=key_name, loadbalancers=loadbalancers,
|
||||
min_entities=min_entities, max_entities=max_entities,
|
||||
name=name, networks=networks, server_name=server_name,
|
||||
state=state)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
# invoke the module
|
||||
main()
|
@ -1,283 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rax_scaling_policy
|
||||
short_description: Manipulate Rackspace Cloud Autoscale Scaling Policy
|
||||
description:
|
||||
- Manipulate Rackspace Cloud Autoscale Scaling Policy
|
||||
version_added: 1.7
|
||||
options:
|
||||
at:
|
||||
description:
|
||||
- The UTC time when this policy will be executed. The time must be
|
||||
formatted according to C(yyyy-MM-dd'T'HH:mm:ss.SSS) such as
|
||||
C(2013-05-19T08:07:08Z)
|
||||
change:
|
||||
description:
|
||||
- The change, either as a number of servers or as a percentage, to make
|
||||
in the scaling group. If this is a percentage, you must set
|
||||
I(is_percent) to C(true) also.
|
||||
cron:
|
||||
description:
|
||||
- The time when the policy will be executed, as a cron entry. For
|
||||
example, if this is parameter is set to C(1 0 * * *)
|
||||
cooldown:
|
||||
description:
|
||||
- The period of time, in seconds, that must pass before any scaling can
|
||||
occur after the previous scaling. Must be an integer between 0 and
|
||||
86400 (24 hrs).
|
||||
desired_capacity:
|
||||
description:
|
||||
- The desired server capacity of the scaling the group; that is, how
|
||||
many servers should be in the scaling group.
|
||||
is_percent:
|
||||
description:
|
||||
- Whether the value in I(change) is a percent value
|
||||
default: false
|
||||
name:
|
||||
description:
|
||||
- Name to give the policy
|
||||
required: true
|
||||
policy_type:
|
||||
description:
|
||||
- The type of policy that will be executed for the current release.
|
||||
choices:
|
||||
- webhook
|
||||
- schedule
|
||||
required: true
|
||||
scaling_group:
|
||||
description:
|
||||
- Name of the scaling group that this policy will be added to
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
author: Matt Martz
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
---
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tasks:
|
||||
- rax_scaling_policy:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
at: '2013-05-19T08:07:08Z'
|
||||
change: 25
|
||||
cooldown: 300
|
||||
is_percent: true
|
||||
name: ASG Test Policy - at
|
||||
policy_type: schedule
|
||||
scaling_group: ASG Test
|
||||
register: asps_at
|
||||
|
||||
- rax_scaling_policy:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
cron: '1 0 * * *'
|
||||
change: 25
|
||||
cooldown: 300
|
||||
is_percent: true
|
||||
name: ASG Test Policy - cron
|
||||
policy_type: schedule
|
||||
scaling_group: ASG Test
|
||||
register: asp_cron
|
||||
|
||||
- rax_scaling_policy:
|
||||
credentials: ~/.raxpub
|
||||
region: ORD
|
||||
cooldown: 300
|
||||
desired_capacity: 5
|
||||
name: ASG Test Policy - webhook
|
||||
policy_type: webhook
|
||||
scaling_group: ASG Test
|
||||
register: asp_webhook
|
||||
'''
|
||||
|
||||
try:
|
||||
import pyrax
|
||||
HAS_PYRAX = True
|
||||
except ImportError:
|
||||
HAS_PYRAX = False
|
||||
|
||||
|
||||
def rax_asp(module, at=None, change=0, cron=None, cooldown=300,
|
||||
desired_capacity=0, is_percent=False, name=None,
|
||||
policy_type=None, scaling_group=None, state='present'):
|
||||
changed = False
|
||||
|
||||
au = pyrax.autoscale
|
||||
if not au:
|
||||
module.fail_json(msg='Failed to instantiate client. This '
|
||||
'typically indicates an invalid region or an '
|
||||
'incorrectly capitalized region name.')
|
||||
|
||||
try:
|
||||
UUID(scaling_group)
|
||||
except ValueError:
|
||||
try:
|
||||
sg = au.find(name=scaling_group)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
else:
|
||||
try:
|
||||
sg = au.get(scaling_group)
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
if state == 'present':
|
||||
policies = filter(lambda p: name == p.name, sg.list_policies())
|
||||
if len(policies) > 1:
|
||||
module.fail_json(msg='No unique policy match found by name')
|
||||
if at:
|
||||
args = dict(at=at)
|
||||
elif cron:
|
||||
args = dict(cron=cron)
|
||||
else:
|
||||
args = None
|
||||
|
||||
if not policies:
|
||||
try:
|
||||
policy = sg.add_policy(name, policy_type=policy_type,
|
||||
cooldown=cooldown, change=change,
|
||||
is_percent=is_percent,
|
||||
desired_capacity=desired_capacity,
|
||||
args=args)
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
else:
|
||||
policy = policies[0]
|
||||
kwargs = {}
|
||||
if policy_type != policy.type:
|
||||
kwargs['policy_type'] = policy_type
|
||||
|
||||
if cooldown != policy.cooldown:
|
||||
kwargs['cooldown'] = cooldown
|
||||
|
||||
if hasattr(policy, 'change') and change != policy.change:
|
||||
kwargs['change'] = change
|
||||
|
||||
if hasattr(policy, 'changePercent') and is_percent is False:
|
||||
kwargs['change'] = change
|
||||
kwargs['is_percent'] = False
|
||||
elif hasattr(policy, 'change') and is_percent is True:
|
||||
kwargs['change'] = change
|
||||
kwargs['is_percent'] = True
|
||||
|
||||
if hasattr(policy, 'desiredCapacity') and change:
|
||||
kwargs['change'] = change
|
||||
elif ((hasattr(policy, 'change') or
|
||||
hasattr(policy, 'changePercent')) and desired_capacity):
|
||||
kwargs['desired_capacity'] = desired_capacity
|
||||
|
||||
if hasattr(policy, 'args') and args != policy.args:
|
||||
kwargs['args'] = args
|
||||
|
||||
if kwargs:
|
||||
policy.update(**kwargs)
|
||||
changed = True
|
||||
|
||||
policy.get()
|
||||
|
||||
module.exit_json(changed=changed, autoscale_policy=rax_to_dict(policy))
|
||||
|
||||
else:
|
||||
try:
|
||||
policies = filter(lambda p: name == p.name, sg.list_policies())
|
||||
if len(policies) > 1:
|
||||
module.fail_json(msg='No unique policy match found by name')
|
||||
elif not policies:
|
||||
policy = {}
|
||||
else:
|
||||
policy.delete()
|
||||
changed = True
|
||||
except Exception, e:
|
||||
module.fail_json(msg='%s' % e.message)
|
||||
|
||||
module.exit_json(changed=changed, autoscale_policy=rax_to_dict(policy))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = rax_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
at=dict(),
|
||||
change=dict(type='int'),
|
||||
cron=dict(),
|
||||
cooldown=dict(type='int', default=300),
|
||||
desired_capacity=dict(type='int'),
|
||||
is_percent=dict(type='bool', default=False),
|
||||
name=dict(required=True),
|
||||
policy_type=dict(required=True, choices=['webhook', 'schedule']),
|
||||
scaling_group=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_together=rax_required_together(),
|
||||
mutually_exclusive=[
|
||||
['cron', 'at'],
|
||||
['change', 'desired_capacity'],
|
||||
]
|
||||
)
|
||||
|
||||
if not HAS_PYRAX:
|
||||
module.fail_json(msg='pyrax is required for this module')
|
||||
|
||||
at = module.params.get('at')
|
||||
change = module.params.get('change')
|
||||
cron = module.params.get('cron')
|
||||
cooldown = module.params.get('cooldown')
|
||||
desired_capacity = module.params.get('desired_capacity')
|
||||
is_percent = module.params.get('is_percent')
|
||||
name = module.params.get('name')
|
||||
policy_type = module.params.get('policy_type')
|
||||
scaling_group = module.params.get('scaling_group')
|
||||
state = module.params.get('state')
|
||||
|
||||
if (at or cron) and policy_type == 'webhook':
|
||||
module.fail_json(msg='policy_type=schedule is required for a time '
|
||||
'based policy')
|
||||
|
||||
setup_rax_module(module, pyrax)
|
||||
|
||||
rax_asp(module, at=at, change=change, cron=cron, cooldown=cooldown,
|
||||
desired_capacity=desired_capacity, is_percent=is_percent,
|
||||
name=name, policy_type=policy_type, scaling_group=scaling_group,
|
||||
state=state)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.rax import *
|
||||
|
||||
# invoke the module
|
||||
main()
|
@ -1,650 +0,0 @@
|
||||
#!/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: rds
|
||||
version_added: "1.3"
|
||||
short_description: create, delete, or modify an Amazon rds instance
|
||||
description:
|
||||
- Creates, deletes, or modifies rds instances. When creating an instance it can be either a new instance or a read-only replica of an existing instance. This module has a dependency on python-boto >= 2.5. The 'promote' command requires boto >= 2.18.0.
|
||||
options:
|
||||
command:
|
||||
description:
|
||||
- Specifies the action to take.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'restore' ]
|
||||
instance_name:
|
||||
description:
|
||||
- Database instance identifier.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
source_instance:
|
||||
description:
|
||||
- Name of the database to replicate. Used only when command=replicate.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
db_engine:
|
||||
description:
|
||||
- The type of database. Used only when command=create.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'MySQL', 'oracle-se1', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres']
|
||||
size:
|
||||
description:
|
||||
- Size in gigabytes of the initial storage for the DB instance. Used only when command=create or command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
instance_type:
|
||||
description:
|
||||
- The instance type of the database. Must be specified when command=create. Optional when command=replicate, command=modify or command=restore. If not specified then the replica inherits the same instance type as the source instance.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
username:
|
||||
description:
|
||||
- Master database username. Used only when command=create.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
password:
|
||||
description:
|
||||
- Password for the master database username. Used only when command=create or command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: true
|
||||
default: null
|
||||
aliases: [ 'aws_region', 'ec2_region' ]
|
||||
db_name:
|
||||
description:
|
||||
- Name of a database to create within the instance. If not specified then no database is created. Used only when command=create.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
engine_version:
|
||||
description:
|
||||
- Version number of the database engine to use. Used only when command=create. If not specified then the current Amazon RDS default engine version is used.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
parameter_group:
|
||||
description:
|
||||
- Name of the DB parameter group to associate with this instance. If omitted then the RDS default DBParameterGroup will be used. Used only when command=create or command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
license_model:
|
||||
description:
|
||||
- The license model for this DB instance. Used only when command=create or command=restore.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'license-included', 'bring-your-own-license', 'general-public-license' ]
|
||||
multi_zone:
|
||||
description:
|
||||
- Specifies if this is a Multi-availability-zone deployment. Can not be used in conjunction with zone parameter. Used only when command=create or command=modify.
|
||||
choices: [ "yes", "no" ]
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
iops:
|
||||
description:
|
||||
- Specifies the number of IOPS for the instance. Used only when command=create or command=modify. Must be an integer greater than 1000.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
security_groups:
|
||||
description:
|
||||
- Comma separated list of one or more security groups. Used only when command=create or command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
vpc_security_groups:
|
||||
description:
|
||||
- Comma separated list of one or more vpc security group ids. Also requires `subnet` to be specified. Used only when command=create or command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
port:
|
||||
description:
|
||||
- Port number that the DB instance uses for connections. Defaults to 3306 for mysql. Must be changed to 1521 for Oracle, 1443 for SQL Server, 5432 for PostgreSQL. Used only when command=create or command=replicate.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
upgrade:
|
||||
description:
|
||||
- Indicates that minor version upgrades should be applied automatically. Used only when command=create or command=replicate.
|
||||
required: false
|
||||
default: no
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
option_group:
|
||||
description:
|
||||
- The name of the option group to use. If not specified then the default option group is used. Used only when command=create.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
maint_window:
|
||||
description:
|
||||
- "Maintenance window in format of ddd:hh24:mi-ddd:hh24:mi. (Example: Mon:22:00-Mon:23:15) If not specified then a random maintenance window is assigned. Used only when command=create or command=modify."
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
backup_window:
|
||||
description:
|
||||
- Backup window in format of hh24:mi-hh24:mi. If not specified then a random backup window is assigned. Used only when command=create or command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
backup_retention:
|
||||
description:
|
||||
- "Number of days backups are retained. Set to 0 to disable backups. Default is 1 day. Valid range: 0-35. Used only when command=create or command=modify."
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
zone:
|
||||
description:
|
||||
- availability zone in which to launch the instance. Used only when command=create, command=replicate or command=restore.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['aws_zone', 'ec2_zone']
|
||||
subnet:
|
||||
description:
|
||||
- VPC subnet group. If specified then a VPC instance is created. Used only when command=create.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
snapshot:
|
||||
description:
|
||||
- Name of snapshot to take. When command=delete, if no snapshot name is provided then no snapshot is taken. Used only when command=delete or command=snapshot.
|
||||
required: false
|
||||
default: null
|
||||
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' ]
|
||||
wait:
|
||||
description:
|
||||
- When command=create, replicate, modify or restore then wait for the database to enter the 'available' state. When command=delete wait for the database to be terminated.
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
wait_timeout:
|
||||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
aliases: []
|
||||
apply_immediately:
|
||||
description:
|
||||
- Used only when command=modify. If enabled, the modifications will be applied as soon as possible rather than waiting for the next preferred maintenance window.
|
||||
default: no
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
new_instance_name:
|
||||
description:
|
||||
- Name to rename an instance to. Used only when command=modify.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: 1.5
|
||||
requirements: [ "boto" ]
|
||||
author: Bruce Pennypacker
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Basic mysql provisioning example
|
||||
- rds: >
|
||||
command=create
|
||||
instance_name=new_database
|
||||
db_engine=MySQL
|
||||
size=10
|
||||
instance_type=db.m1.small
|
||||
username=mysql_admin
|
||||
password=1nsecure
|
||||
|
||||
# Create a read-only replica and wait for it to become available
|
||||
- rds: >
|
||||
command=replicate
|
||||
instance_name=new_database_replica
|
||||
source_instance=new_database
|
||||
wait=yes
|
||||
wait_timeout=600
|
||||
|
||||
# Delete an instance, but create a snapshot before doing so
|
||||
- rds: >
|
||||
command=delete
|
||||
instance_name=new_database
|
||||
snapshot=new_database_snapshot
|
||||
|
||||
# Get facts about an instance
|
||||
- rds: >
|
||||
command=facts
|
||||
instance_name=new_database
|
||||
register: new_database_facts
|
||||
|
||||
# Rename an instance and wait for the change to take effect
|
||||
- rds: >
|
||||
command=modify
|
||||
instance_name=new_database
|
||||
new_instance_name=renamed_database
|
||||
wait=yes
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto.rds
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def get_current_resource(conn, resource, command):
|
||||
# There will be exceptions but we want the calling code to handle them
|
||||
if command == 'snapshot':
|
||||
return conn.get_all_dbsnapshots(snapshot_id=resource)[0]
|
||||
else:
|
||||
return conn.get_all_dbinstances(resource)[0]
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
command = dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote', 'snapshot', 'restore'], required=True),
|
||||
instance_name = dict(required=True),
|
||||
source_instance = dict(required=False),
|
||||
db_engine = dict(choices=['MySQL', 'oracle-se1', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres'], required=False),
|
||||
size = dict(required=False),
|
||||
instance_type = dict(aliases=['type'], required=False),
|
||||
username = dict(required=False),
|
||||
password = dict(no_log=True, required=False),
|
||||
db_name = dict(required=False),
|
||||
engine_version = dict(required=False),
|
||||
parameter_group = dict(required=False),
|
||||
license_model = dict(choices=['license-included', 'bring-your-own-license', 'general-public-license'], required=False),
|
||||
multi_zone = dict(type='bool', default=False),
|
||||
iops = dict(required=False),
|
||||
security_groups = dict(required=False),
|
||||
vpc_security_groups = dict(type='list', required=False),
|
||||
port = dict(required=False),
|
||||
upgrade = dict(type='bool', default=False),
|
||||
option_group = dict(required=False),
|
||||
maint_window = dict(required=False),
|
||||
backup_window = dict(required=False),
|
||||
backup_retention = dict(required=False),
|
||||
zone = dict(aliases=['aws_zone', 'ec2_zone'], required=False),
|
||||
subnet = dict(required=False),
|
||||
wait = dict(type='bool', default=False),
|
||||
wait_timeout = dict(default=300),
|
||||
snapshot = dict(required=False),
|
||||
apply_immediately = dict(type='bool', default=False),
|
||||
new_instance_name = dict(required=False),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
)
|
||||
|
||||
command = module.params.get('command')
|
||||
instance_name = module.params.get('instance_name')
|
||||
source_instance = module.params.get('source_instance')
|
||||
db_engine = module.params.get('db_engine')
|
||||
size = module.params.get('size')
|
||||
instance_type = module.params.get('instance_type')
|
||||
username = module.params.get('username')
|
||||
password = module.params.get('password')
|
||||
db_name = module.params.get('db_name')
|
||||
engine_version = module.params.get('engine_version')
|
||||
parameter_group = module.params.get('parameter_group')
|
||||
license_model = module.params.get('license_model')
|
||||
multi_zone = module.params.get('multi_zone')
|
||||
iops = module.params.get('iops')
|
||||
security_groups = module.params.get('security_groups')
|
||||
vpc_security_groups = module.params.get('vpc_security_groups')
|
||||
port = module.params.get('port')
|
||||
upgrade = module.params.get('upgrade')
|
||||
option_group = module.params.get('option_group')
|
||||
maint_window = module.params.get('maint_window')
|
||||
subnet = module.params.get('subnet')
|
||||
backup_window = module.params.get('backup_window')
|
||||
backup_retention = module.params.get('backup_retention')
|
||||
region = module.params.get('region')
|
||||
zone = module.params.get('zone')
|
||||
aws_secret_key = module.params.get('aws_secret_key')
|
||||
aws_access_key = module.params.get('aws_access_key')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
snapshot = module.params.get('snapshot')
|
||||
apply_immediately = module.params.get('apply_immediately')
|
||||
new_instance_name = module.params.get('new_instance_name')
|
||||
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||
if not region:
|
||||
module.fail_json(msg = str("region not specified and unable to determine region from EC2_REGION."))
|
||||
|
||||
# connect to the rds endpoint
|
||||
try:
|
||||
conn = connect_to_aws(boto.rds, region, **aws_connect_params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
def invalid_security_group_type(subnet):
|
||||
if subnet:
|
||||
return 'security_groups'
|
||||
else:
|
||||
return 'vpc_security_groups'
|
||||
|
||||
# Package up the optional parameters
|
||||
params = {}
|
||||
|
||||
# Validate parameters for each command
|
||||
if command == 'create':
|
||||
required_vars = [ 'instance_name', 'db_engine', 'size', 'instance_type', 'username', 'password' ]
|
||||
invalid_vars = [ 'source_instance', 'snapshot', 'apply_immediately', 'new_instance_name' ] + [invalid_security_group_type(subnet)]
|
||||
|
||||
elif command == 'replicate':
|
||||
required_vars = [ 'instance_name', 'source_instance' ]
|
||||
invalid_vars = [ 'db_engine', 'size', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'backup_window', 'backup_retention', 'subnet', 'snapshot', 'apply_immediately', 'new_instance_name' ]
|
||||
|
||||
elif command == 'delete':
|
||||
required_vars = [ 'instance_name' ]
|
||||
invalid_vars = [ 'db_engine', 'size', 'instance_type', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups' ,'security_groups', 'option_group', 'maint_window', 'backup_window', 'backup_retention', 'port', 'upgrade', 'subnet', 'zone' , 'source_instance', 'apply_immediately', 'new_instance_name' ]
|
||||
|
||||
elif command == 'facts':
|
||||
required_vars = [ 'instance_name' ]
|
||||
invalid_vars = [ 'db_engine', 'size', 'instance_type', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'backup_window', 'backup_retention', 'port', 'upgrade', 'subnet', 'zone', 'wait', 'source_instance' 'apply_immediately', 'new_instance_name' ]
|
||||
|
||||
elif command == 'modify':
|
||||
required_vars = [ 'instance_name' ]
|
||||
if password:
|
||||
params["master_password"] = password
|
||||
invalid_vars = [ 'db_engine', 'username', 'db_name', 'engine_version', 'license_model', 'option_group', 'port', 'upgrade', 'subnet', 'zone', 'source_instance']
|
||||
|
||||
elif command == 'promote':
|
||||
required_vars = [ 'instance_name' ]
|
||||
invalid_vars = [ 'db_engine', 'size', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'subnet', 'source_instance', 'snapshot', 'apply_immediately', 'new_instance_name' ]
|
||||
|
||||
elif command == 'snapshot':
|
||||
required_vars = [ 'instance_name', 'snapshot']
|
||||
invalid_vars = [ 'db_engine', 'size', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'subnet', 'source_instance', 'apply_immediately', 'new_instance_name' ]
|
||||
|
||||
elif command == 'restore':
|
||||
required_vars = [ 'instance_name', 'snapshot', 'instance_type' ]
|
||||
invalid_vars = [ 'db_engine', 'db_name', 'username', 'password', 'engine_version', 'option_group', 'source_instance', 'apply_immediately', 'new_instance_name', 'vpc_security_groups', 'security_groups' ]
|
||||
|
||||
for v in required_vars:
|
||||
if not module.params.get(v):
|
||||
module.fail_json(msg = str("Parameter %s required for %s command" % (v, command)))
|
||||
|
||||
for v in invalid_vars:
|
||||
if module.params.get(v):
|
||||
module.fail_json(msg = str("Parameter %s invalid for %s command" % (v, command)))
|
||||
|
||||
if db_engine:
|
||||
params["engine"] = db_engine
|
||||
|
||||
if port:
|
||||
params["port"] = port
|
||||
|
||||
if db_name:
|
||||
params["db_name"] = db_name
|
||||
|
||||
if parameter_group:
|
||||
params["param_group"] = parameter_group
|
||||
|
||||
if zone:
|
||||
params["availability_zone"] = zone
|
||||
|
||||
if maint_window:
|
||||
params["preferred_maintenance_window"] = maint_window
|
||||
|
||||
if backup_window:
|
||||
params["preferred_backup_window"] = backup_window
|
||||
|
||||
if backup_retention:
|
||||
params["backup_retention_period"] = backup_retention
|
||||
|
||||
if multi_zone:
|
||||
params["multi_az"] = multi_zone
|
||||
|
||||
if engine_version:
|
||||
params["engine_version"] = engine_version
|
||||
|
||||
if upgrade:
|
||||
params["auto_minor_version_upgrade"] = upgrade
|
||||
|
||||
if subnet:
|
||||
params["db_subnet_group_name"] = subnet
|
||||
|
||||
if license_model:
|
||||
params["license_model"] = license_model
|
||||
|
||||
if option_group:
|
||||
params["option_group_name"] = option_group
|
||||
|
||||
if iops:
|
||||
params["iops"] = iops
|
||||
|
||||
if security_groups:
|
||||
params["security_groups"] = security_groups.split(',')
|
||||
|
||||
if vpc_security_groups:
|
||||
groups_list = []
|
||||
for x in vpc_security_groups:
|
||||
groups_list.append(boto.rds.VPCSecurityGroupMembership(vpc_group=x))
|
||||
params["vpc_security_groups"] = groups_list
|
||||
|
||||
if new_instance_name:
|
||||
params["new_instance_id"] = new_instance_name
|
||||
|
||||
changed = True
|
||||
|
||||
if command in ['create', 'restore', 'facts']:
|
||||
try:
|
||||
result = conn.get_all_dbinstances(instance_name)[0]
|
||||
changed = False
|
||||
except boto.exception.BotoServerError, e:
|
||||
try:
|
||||
if command == 'create':
|
||||
result = conn.create_dbinstance(instance_name, size, instance_type, username, password, **params)
|
||||
if command == 'restore':
|
||||
result = conn.restore_dbinstance_from_dbsnapshot(snapshot, instance_name, instance_type, **params)
|
||||
if command == 'facts':
|
||||
module.fail_json(msg = "DB Instance %s does not exist" % instance_name)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
if command == 'snapshot':
|
||||
try:
|
||||
result = conn.get_all_dbsnapshots(snapshot)[0]
|
||||
changed = False
|
||||
except boto.exception.BotoServerError, e:
|
||||
try:
|
||||
result = conn.create_dbsnapshot(snapshot, instance_name)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
if command == 'delete':
|
||||
try:
|
||||
result = conn.get_all_dbinstances(instance_name)[0]
|
||||
if result.status == 'deleting':
|
||||
module.exit_json(changed=False)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.exit_json(changed=False)
|
||||
try:
|
||||
if snapshot:
|
||||
params["skip_final_snapshot"] = False
|
||||
params["final_snapshot_id"] = snapshot
|
||||
else:
|
||||
params["skip_final_snapshot"] = True
|
||||
result = conn.delete_dbinstance(instance_name, **params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
if command == 'replicate':
|
||||
try:
|
||||
if instance_type:
|
||||
params["instance_class"] = instance_type
|
||||
result = conn.create_dbinstance_read_replica(instance_name, source_instance, **params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
if command == 'modify':
|
||||
try:
|
||||
params["apply_immediately"] = apply_immediately
|
||||
result = conn.modify_dbinstance(instance_name, **params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
if apply_immediately:
|
||||
if new_instance_name:
|
||||
# Wait until the new instance name is valid
|
||||
found = 0
|
||||
while found == 0:
|
||||
instances = conn.get_all_dbinstances()
|
||||
for i in instances:
|
||||
if i.id == new_instance_name:
|
||||
instance_name = new_instance_name
|
||||
found = 1
|
||||
if found == 0:
|
||||
time.sleep(5)
|
||||
|
||||
# The name of the database has now changed, so we have
|
||||
# to force result to contain the new instance, otherwise
|
||||
# the call below to get_current_resource will fail since it
|
||||
# will be looking for the old instance name.
|
||||
result.id = new_instance_name
|
||||
else:
|
||||
# Wait for a few seconds since it takes a while for AWS
|
||||
# to change the instance from 'available' to 'modifying'
|
||||
time.sleep(5)
|
||||
|
||||
if command == 'promote':
|
||||
try:
|
||||
result = conn.promote_read_replica(instance_name, **params)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
# If we're not waiting for a delete to complete then we're all done
|
||||
# so just return
|
||||
if command == 'delete' and not wait:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
try:
|
||||
resource = get_current_resource(conn, result.id, command)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
# Wait for the resource to be available if requested
|
||||
if wait:
|
||||
try:
|
||||
wait_timeout = time.time() + wait_timeout
|
||||
time.sleep(5)
|
||||
|
||||
while wait_timeout > time.time() and resource.status != 'available':
|
||||
time.sleep(5)
|
||||
if wait_timeout <= time.time():
|
||||
module.fail_json(msg = "Timeout waiting for resource %s" % resource.id)
|
||||
resource = get_current_resource(conn, result.id, command)
|
||||
except boto.exception.BotoServerError, e:
|
||||
# If we're waiting for an instance to be deleted then
|
||||
# get_all_dbinstances will eventually throw a
|
||||
# DBInstanceNotFound error.
|
||||
if command == 'delete' and e.error_code == 'DBInstanceNotFound':
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
# If we got here then pack up all the instance details to send
|
||||
# back to ansible
|
||||
if command == 'snapshot':
|
||||
d = {
|
||||
'id' : resource.id,
|
||||
'create_time' : resource.snapshot_create_time,
|
||||
'status' : resource.status,
|
||||
'availability_zone' : resource.availability_zone,
|
||||
'instance_id' : resource.instance_id,
|
||||
'instance_created' : resource.instance_create_time,
|
||||
}
|
||||
try:
|
||||
d["snapshot_type"] = resource.snapshot_type
|
||||
d["iops"] = resource.iops
|
||||
except AttributeError, e:
|
||||
pass # needs boto >= 2.21.0
|
||||
|
||||
return module.exit_json(changed=changed, snapshot=d)
|
||||
|
||||
d = {
|
||||
'id' : resource.id,
|
||||
'create_time' : resource.create_time,
|
||||
'status' : resource.status,
|
||||
'availability_zone' : resource.availability_zone,
|
||||
'backup_retention' : resource.backup_retention_period,
|
||||
'backup_window' : resource.preferred_backup_window,
|
||||
'maintenance_window' : resource.preferred_maintenance_window,
|
||||
'multi_zone' : resource.multi_az,
|
||||
'instance_type' : resource.instance_class,
|
||||
'username' : resource.master_username,
|
||||
'iops' : resource.iops
|
||||
}
|
||||
|
||||
# Endpoint exists only if the instance is available
|
||||
if resource.status == 'available' and command != 'snapshot':
|
||||
d["endpoint"] = resource.endpoint[0]
|
||||
d["port"] = resource.endpoint[1]
|
||||
if resource.vpc_security_groups is not None:
|
||||
d["vpc_security_groups"] = ','.join(x.vpc_group for x in resource.vpc_security_groups)
|
||||
else:
|
||||
d["vpc_security_groups"] = None
|
||||
else:
|
||||
d["endpoint"] = None
|
||||
d["port"] = None
|
||||
d["vpc_security_groups"] = None
|
||||
|
||||
# ReadReplicaSourceDBInstanceIdentifier may or may not exist
|
||||
try:
|
||||
d["replication_source"] = resource.ReadReplicaSourceDBInstanceIdentifier
|
||||
except Exception, e:
|
||||
d["replication_source"] = None
|
||||
|
||||
module.exit_json(changed=changed, instance=d)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,313 +0,0 @@
|
||||
#!/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: rds_param_group
|
||||
version_added: "1.5"
|
||||
short_description: manage RDS parameter groups
|
||||
description:
|
||||
- Creates, modifies, and deletes RDS parameter groups. This module has a dependency on python-boto >= 2.5.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Specifies whether the group should be present or absent.
|
||||
required: true
|
||||
default: present
|
||||
aliases: []
|
||||
choices: [ 'present' , 'absent' ]
|
||||
name:
|
||||
description:
|
||||
- Database parameter group identifier.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
description:
|
||||
description:
|
||||
- Database parameter group description. Only set when a new group is added.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
engine:
|
||||
description:
|
||||
- The type of database for this group. Required for state=present.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'mysql5.1', 'mysql5.5', 'mysql5.6', 'oracle-ee-11.2', 'oracle-se-11.2', 'oracle-se1-11.2', 'postgres9.3', 'sqlserver-ee-10.5', 'sqlserver-ee-11.0', 'sqlserver-ex-10.5', 'sqlserver-ex-11.0', 'sqlserver-se-10.5', 'sqlserver-se-11.0', 'sqlserver-web-10.5', 'sqlserver-web-11.0']
|
||||
immediate:
|
||||
description:
|
||||
- Whether to apply the changes immediately, or after the next reboot of any associated instances.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
params:
|
||||
description:
|
||||
- Map of parameter names and values. Numeric values may be represented as K for kilo (1024), M for mega (1024^2), G for giga (1024^3), or T for tera (1024^4), and these values will be expanded into the appropriate number before being set in the parameter group.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'mysql5.1', 'mysql5.5', 'mysql5.6', 'oracle-ee-11.2', 'oracle-se-11.2', 'oracle-se1-11.2', 'postgres9.3', 'sqlserver-ee-10.5', 'sqlserver-ee-11.0', 'sqlserver-ex-10.5', 'sqlserver-ex-11.0', 'sqlserver-se-10.5', 'sqlserver-se-11.0', 'sqlserver-web-10.5', 'sqlserver-web-11.0']
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: true
|
||||
default: null
|
||||
aliases: [ 'aws_region', 'ec2_region' ]
|
||||
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' ]
|
||||
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' ]
|
||||
requirements: [ "boto" ]
|
||||
author: Scott Anderson
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add or change a parameter group, in this case setting auto_increment_increment to 42 * 1024
|
||||
- rds_param_group: >
|
||||
state=present
|
||||
name=norwegian_blue
|
||||
description=My Fancy Ex Parrot Group
|
||||
engine=mysql5.6
|
||||
params='{"auto_increment_increment": "42K"}'
|
||||
|
||||
# Remove a parameter group
|
||||
- rds_param_group: >
|
||||
state=absent
|
||||
name=norwegian_blue
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
VALID_ENGINES = [
|
||||
'mysql5.1',
|
||||
'mysql5.5',
|
||||
'mysql5.6',
|
||||
'oracle-ee-11.2',
|
||||
'oracle-se-11.2',
|
||||
'oracle-se1-11.2',
|
||||
'postgres9.3',
|
||||
'sqlserver-ee-10.5',
|
||||
'sqlserver-ee-11.0',
|
||||
'sqlserver-ex-10.5',
|
||||
'sqlserver-ex-11.0',
|
||||
'sqlserver-se-10.5',
|
||||
'sqlserver-se-11.0',
|
||||
'sqlserver-web-10.5',
|
||||
'sqlserver-web-11.0',
|
||||
]
|
||||
|
||||
try:
|
||||
import boto.rds
|
||||
from boto.exception import BotoServerError
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
# returns a tuple: (whether or not a parameter was changed, the remaining parameters that weren't found in this parameter group)
|
||||
|
||||
class NotModifiableError(StandardError):
|
||||
def __init__(self, error_message, *args):
|
||||
super(NotModifiableError, self).__init__(error_message, *args)
|
||||
self.error_message = error_message
|
||||
|
||||
def __repr__(self):
|
||||
return 'NotModifiableError: %s' % self.error_message
|
||||
|
||||
def __str__(self):
|
||||
return 'NotModifiableError: %s' % self.error_message
|
||||
|
||||
INT_MODIFIERS = {
|
||||
'K': 1024,
|
||||
'M': pow(1024, 2),
|
||||
'G': pow(1024, 3),
|
||||
'T': pow(1024, 4),
|
||||
}
|
||||
|
||||
TRUE_VALUES = ('on', 'true', 'yes', '1',)
|
||||
|
||||
def set_parameter(param, value, immediate):
|
||||
"""
|
||||
Allows setting parameters with 10M = 10* 1024 * 1024 and so on.
|
||||
"""
|
||||
converted_value = value
|
||||
|
||||
if param.type == 'string':
|
||||
converted_value = str(value)
|
||||
|
||||
elif param.type == 'integer':
|
||||
if isinstance(value, basestring):
|
||||
try:
|
||||
for modifier in INT_MODIFIERS.keys():
|
||||
if value.endswith(modifier):
|
||||
converted_value = int(value[:-1]) * INT_MODIFIERS[modifier]
|
||||
converted_value = int(converted_value)
|
||||
except ValueError:
|
||||
# may be based on a variable (ie. {foo*3/4}) so
|
||||
# just pass it on through to boto
|
||||
converted_value = str(value)
|
||||
elif type(value) == bool:
|
||||
converted_value = 1 if value else 0
|
||||
else:
|
||||
converted_value = int(value)
|
||||
|
||||
elif param.type == 'boolean':
|
||||
if isinstance(value, basestring):
|
||||
converted_value = value in TRUE_VALUES
|
||||
else:
|
||||
converted_value = bool(value)
|
||||
|
||||
param.value = converted_value
|
||||
param.apply(immediate)
|
||||
|
||||
def modify_group(group, params, immediate=False):
|
||||
""" Set all of the params in a group to the provided new params. Raises NotModifiableError if any of the
|
||||
params to be changed are read only.
|
||||
"""
|
||||
changed = {}
|
||||
|
||||
new_params = dict(params)
|
||||
|
||||
for key in new_params.keys():
|
||||
if group.has_key(key):
|
||||
param = group[key]
|
||||
new_value = new_params[key]
|
||||
|
||||
try:
|
||||
old_value = param.value
|
||||
except ValueError:
|
||||
# some versions of boto have problems with retrieving
|
||||
# integer values from params that may have their value
|
||||
# based on a variable (ie. {foo*3/4}), so grab it in a
|
||||
# way that bypasses the property functions
|
||||
old_value = param._value
|
||||
|
||||
if old_value != new_value:
|
||||
if not param.is_modifiable:
|
||||
raise NotModifiableError('Parameter %s is not modifiable.' % key)
|
||||
|
||||
changed[key] = {'old': param.value, 'new': new_value}
|
||||
|
||||
set_parameter(param, new_value, immediate)
|
||||
|
||||
del new_params[key]
|
||||
|
||||
return changed, new_params
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
state = dict(required=True, choices=['present', 'absent']),
|
||||
name = dict(required=True),
|
||||
engine = dict(required=False, choices=VALID_ENGINES),
|
||||
description = dict(required=False),
|
||||
params = dict(required=False, aliases=['parameters'], type='dict'),
|
||||
immediate = dict(required=False, type='bool'),
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
state = module.params.get('state')
|
||||
group_name = module.params.get('name').lower()
|
||||
group_engine = module.params.get('engine')
|
||||
group_description = module.params.get('description')
|
||||
group_params = module.params.get('params') or {}
|
||||
immediate = module.params.get('immediate') or False
|
||||
|
||||
if state == 'present':
|
||||
for required in ['name', 'description', 'engine', 'params']:
|
||||
if not module.params.get(required):
|
||||
module.fail_json(msg = str("Parameter %s required for state='present'" % required))
|
||||
else:
|
||||
for not_allowed in ['description', 'engine', 'params']:
|
||||
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.
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
if not region:
|
||||
module.fail_json(msg = str("region not specified and unable to determine region from EC2_REGION."))
|
||||
|
||||
try:
|
||||
conn = boto.rds.connect_to_region(region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
group_was_added = False
|
||||
|
||||
try:
|
||||
changed = False
|
||||
|
||||
try:
|
||||
all_groups = conn.get_all_dbparameter_groups(group_name, max_records=100)
|
||||
exists = len(all_groups) > 0
|
||||
except BotoServerError, e:
|
||||
if e.error_code != 'DBParameterGroupNotFound':
|
||||
module.fail_json(msg = e.error_message)
|
||||
exists = False
|
||||
|
||||
if state == 'absent':
|
||||
if exists:
|
||||
conn.delete_parameter_group(group_name)
|
||||
changed = True
|
||||
else:
|
||||
changed = {}
|
||||
if not exists:
|
||||
new_group = conn.create_parameter_group(group_name, engine=group_engine, description=group_description)
|
||||
group_was_added = True
|
||||
|
||||
# If a "Marker" is present, this group has more attributes remaining to check. Get the next batch, but only
|
||||
# if there are parameters left to set.
|
||||
marker = None
|
||||
while len(group_params):
|
||||
next_group = conn.get_all_dbparameters(group_name, marker=marker)
|
||||
|
||||
changed_params, group_params = modify_group(next_group, group_params, immediate)
|
||||
changed.update(changed_params)
|
||||
|
||||
if hasattr(next_group, 'Marker'):
|
||||
marker = next_group.Marker
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
except NotModifiableError, e:
|
||||
msg = e.error_message
|
||||
if group_was_added:
|
||||
msg = '%s The group "%s" was added first.' % (msg, group_name)
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,166 +0,0 @@
|
||||
#!/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: rds_subnet_group
|
||||
version_added: "1.5"
|
||||
short_description: manage RDS database subnet groups
|
||||
description:
|
||||
- Creates, modifies, and deletes RDS database 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
|
||||
aliases: []
|
||||
choices: [ 'present' , 'absent' ]
|
||||
name:
|
||||
description:
|
||||
- Database subnet group identifier.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
description:
|
||||
description:
|
||||
- Database subnet group description. Only set when a new group is added.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
subnets:
|
||||
description:
|
||||
- List of subnet IDs that make up the database subnet group.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
region:
|
||||
description:
|
||||
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||
required: true
|
||||
default: null
|
||||
aliases: [ 'aws_region', 'ec2_region' ]
|
||||
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' ]
|
||||
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' ]
|
||||
requirements: [ "boto" ]
|
||||
author: Scott Anderson
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add or change a subnet group
|
||||
- local_action:
|
||||
module: rds_subnet_group
|
||||
state: present
|
||||
name: norwegian-blue
|
||||
description: My Fancy Ex Parrot Subnet Group
|
||||
subnets:
|
||||
- subnet-aaaaaaaa
|
||||
- subnet-bbbbbbbb
|
||||
|
||||
# Remove a parameter group
|
||||
- rds_param_group: >
|
||||
state=absent
|
||||
name=norwegian-blue
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto.rds
|
||||
from boto.exception import BotoServerError
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
if not region:
|
||||
module.fail_json(msg = str("region not specified and unable to determine region from EC2_REGION."))
|
||||
|
||||
try:
|
||||
conn = boto.rds.connect_to_region(region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
try:
|
||||
changed = False
|
||||
exists = False
|
||||
|
||||
try:
|
||||
matching_groups = conn.get_all_db_subnet_groups(group_name, max_records=100)
|
||||
exists = len(matching_groups) > 0
|
||||
except BotoServerError, e:
|
||||
if e.error_code != 'DBSubnetGroupNotFoundFault':
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
if state == 'absent':
|
||||
if exists:
|
||||
conn.delete_db_subnet_group(group_name)
|
||||
changed = True
|
||||
else:
|
||||
if not exists:
|
||||
new_group = conn.create_db_subnet_group(group_name, desc=group_description, subnet_ids=group_subnets)
|
||||
|
||||
else:
|
||||
changed_group = conn.modify_db_subnet_group(group_name, description=group_description, subnet_ids=group_subnets)
|
||||
|
||||
except BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,281 +0,0 @@
|
||||
#!/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: route53
|
||||
version_added: "1.3"
|
||||
short_description: add or delete entries in Amazons Route53 DNS service
|
||||
description:
|
||||
- Creates and deletes DNS records in Amazons Route53 service
|
||||
options:
|
||||
command:
|
||||
description:
|
||||
- Specifies the action to take.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'get', 'create', 'delete' ]
|
||||
zone:
|
||||
description:
|
||||
- The DNS zone to modify
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
record:
|
||||
description:
|
||||
- The full DNS record to create or delete
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
ttl:
|
||||
description:
|
||||
- The TTL to give the new record
|
||||
required: false
|
||||
default: 3600 (one hour)
|
||||
aliases: []
|
||||
type:
|
||||
description:
|
||||
- The type of DNS record to create
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'NS' ]
|
||||
value:
|
||||
description:
|
||||
- The new value when creating a DNS record. Multiple comma-spaced values are allowed. When deleting a record all values for the record must be specified or Route53 will not delete it.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
aws_secret_key:
|
||||
description:
|
||||
- AWS secret key.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['ec2_secret_key', 'secret_key']
|
||||
aws_access_key:
|
||||
description:
|
||||
- AWS access key.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['ec2_access_key', 'access_key']
|
||||
overwrite:
|
||||
description:
|
||||
- Whether an existing record should be overwritten on create if values do not match
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
retry_interval:
|
||||
description:
|
||||
- In the case that route53 is still servicing a prior request, this module will wait and try again after this many seconds. If you have many domain names, the default of 500 seconds may be too long.
|
||||
required: false
|
||||
default: 500
|
||||
aliases: []
|
||||
requirements: [ "boto" ]
|
||||
author: Bruce Pennypacker
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add new.foo.com as an A record with 3 IPs
|
||||
- route53: >
|
||||
command=create
|
||||
zone=foo.com
|
||||
record=new.foo.com
|
||||
type=A
|
||||
ttl=7200
|
||||
value=1.1.1.1,2.2.2.2,3.3.3.3
|
||||
|
||||
# Retrieve the details for new.foo.com
|
||||
- route53: >
|
||||
command=get
|
||||
zone=foo.com
|
||||
record=new.foo.com
|
||||
type=A
|
||||
register: rec
|
||||
|
||||
# Delete new.foo.com A record using the results from the get command
|
||||
- route53: >
|
||||
command=delete
|
||||
zone=foo.com
|
||||
record={{ rec.set.record }}
|
||||
type={{ rec.set.type }}
|
||||
value={{ rec.set.value }}
|
||||
|
||||
# Add an AAAA record. Note that because there are colons in the value
|
||||
# that the entire parameter list must be quoted:
|
||||
- route53: >
|
||||
command=create
|
||||
zone=foo.com
|
||||
record=localhost.foo.com
|
||||
type=AAAA
|
||||
ttl=7200
|
||||
value="::1"
|
||||
|
||||
# Add a TXT record. Note that TXT and SPF records must be surrounded
|
||||
# by quotes when sent to Route 53:
|
||||
- route53: >
|
||||
command=create
|
||||
zone=foo.com
|
||||
record=localhost.foo.com
|
||||
type=TXT
|
||||
ttl=7200
|
||||
value="\"bar\""
|
||||
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import boto
|
||||
from boto import route53
|
||||
from boto.route53.record import ResourceRecordSets
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def commit(changes, retry_interval):
|
||||
"""Commit changes, but retry PriorRequestNotComplete errors."""
|
||||
retry = 10
|
||||
while True:
|
||||
try:
|
||||
retry -= 1
|
||||
return changes.commit()
|
||||
except boto.route53.exception.DNSServerError, e:
|
||||
code = e.body.split("<Code>")[1]
|
||||
code = code.split("</Code>")[0]
|
||||
if code != 'PriorRequestNotComplete' or retry < 0:
|
||||
raise e
|
||||
time.sleep(retry_interval)
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
command = dict(choices=['get', 'create', 'delete'], required=True),
|
||||
zone = dict(required=True),
|
||||
record = dict(required=True),
|
||||
ttl = dict(required=False, default=3600),
|
||||
type = dict(choices=['A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'NS'], required=True),
|
||||
value = dict(required=False),
|
||||
overwrite = dict(required=False, type='bool'),
|
||||
retry_interval = dict(required=False, default=500)
|
||||
)
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
command_in = module.params.get('command')
|
||||
zone_in = module.params.get('zone')
|
||||
ttl_in = module.params.get('ttl')
|
||||
record_in = module.params.get('record')
|
||||
type_in = module.params.get('type')
|
||||
value_in = module.params.get('value')
|
||||
retry_interval_in = module.params.get('retry_interval')
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
value_list = ()
|
||||
|
||||
if type(value_in) is str:
|
||||
if value_in:
|
||||
value_list = sorted(value_in.split(','))
|
||||
elif type(value_in) is list:
|
||||
value_list = sorted(value_in)
|
||||
|
||||
if zone_in[-1:] != '.':
|
||||
zone_in += "."
|
||||
|
||||
if record_in[-1:] != '.':
|
||||
record_in += "."
|
||||
|
||||
if command_in == 'create' or command_in == 'delete':
|
||||
if not value_in:
|
||||
module.fail_json(msg = "parameter 'value' required for create/delete")
|
||||
|
||||
# connect to the route53 endpoint
|
||||
try:
|
||||
conn = boto.route53.connection.Route53Connection(aws_access_key, aws_secret_key)
|
||||
except boto.exception.BotoServerError, e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
# Get all the existing hosted zones and save their ID's
|
||||
zones = {}
|
||||
results = conn.get_all_hosted_zones()
|
||||
for r53zone in results['ListHostedZonesResponse']['HostedZones']:
|
||||
zone_id = r53zone['Id'].replace('/hostedzone/', '')
|
||||
zones[r53zone['Name']] = zone_id
|
||||
|
||||
# Verify that the requested zone is already defined in Route53
|
||||
if not zone_in in zones:
|
||||
errmsg = "Zone %s does not exist in Route53" % zone_in
|
||||
module.fail_json(msg = errmsg)
|
||||
|
||||
record = {}
|
||||
|
||||
found_record = False
|
||||
sets = conn.get_all_rrsets(zones[zone_in])
|
||||
for rset in sets:
|
||||
# Due to a bug in either AWS or Boto, "special" characters are returned as octals, preventing round
|
||||
# tripping of things like * and @.
|
||||
decoded_name = rset.name.replace(r'\052', '*')
|
||||
decoded_name = decoded_name.replace(r'\100', '@')
|
||||
|
||||
if rset.type == type_in and decoded_name == record_in:
|
||||
found_record = True
|
||||
record['zone'] = zone_in
|
||||
record['type'] = rset.type
|
||||
record['record'] = decoded_name
|
||||
record['ttl'] = rset.ttl
|
||||
record['value'] = ','.join(sorted(rset.resource_records))
|
||||
record['values'] = sorted(rset.resource_records)
|
||||
if value_list == sorted(rset.resource_records) and int(record['ttl']) == ttl_in and command_in == 'create':
|
||||
module.exit_json(changed=False)
|
||||
|
||||
if command_in == 'get':
|
||||
module.exit_json(changed=False, set=record)
|
||||
|
||||
if command_in == 'delete' and not found_record:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
changes = ResourceRecordSets(conn, zones[zone_in])
|
||||
|
||||
if command_in == 'create' and found_record:
|
||||
if not module.params['overwrite']:
|
||||
module.fail_json(msg = "Record already exists with different value. Set 'overwrite' to replace it")
|
||||
else:
|
||||
change = changes.add_change("DELETE", record_in, type_in, record['ttl'])
|
||||
for v in record['values']:
|
||||
change.add_value(v)
|
||||
|
||||
if command_in == 'create' or command_in == 'delete':
|
||||
change = changes.add_change(command_in.upper(), record_in, type_in, ttl_in)
|
||||
for v in value_list:
|
||||
change.add_value(v)
|
||||
|
||||
try:
|
||||
result = commit(changes, retry_interval_in)
|
||||
except boto.route53.exception.DNSServerError, e:
|
||||
txt = e.body.split("<Message>")[1]
|
||||
txt = txt.split("</Message>")[0]
|
||||
module.fail_json(msg = txt)
|
||||
|
||||
module.exit_json(changed=True)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,514 +0,0 @@
|
||||
#!/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: s3
|
||||
short_description: S3 module putting a file into S3.
|
||||
description:
|
||||
- This module allows the user to dictate the presence of a given file in an S3 bucket. If or once the key (file) exists in the bucket, it returns a time-expired download URL. This module has a dependency on python-boto.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
bucket:
|
||||
description:
|
||||
- Bucket name.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
object:
|
||||
description:
|
||||
- Keyname of the object inside the bucket. Can be used to create "virtual directories", see examples.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.3"
|
||||
src:
|
||||
description:
|
||||
- The source file path when performing a PUT operation.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.3"
|
||||
dest:
|
||||
description:
|
||||
- The destination file path when downloading an object/key with a GET operation.
|
||||
required: false
|
||||
aliases: []
|
||||
version_added: "1.3"
|
||||
overwrite:
|
||||
description:
|
||||
- Force overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations.
|
||||
required: false
|
||||
default: true
|
||||
version_added: "1.2"
|
||||
mode:
|
||||
description:
|
||||
- Switches the module behaviour between put (upload), get (download), geturl (return download url (Ansible 1.3+), getstr (download object as string (1.3+)), create (bucket) and delete (bucket).
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
expiration:
|
||||
description:
|
||||
- Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a mode=put or mode=geturl operation.
|
||||
required: false
|
||||
default: 600
|
||||
aliases: []
|
||||
s3_url:
|
||||
description:
|
||||
- "S3 URL endpoint. If not specified then the S3_URL environment variable is used, if that variable is defined. Ansible tries to guess if fakes3 (https://github.com/jubos/fake-s3) or Eucalyptus Walrus (https://github.com/eucalyptus/eucalyptus/wiki/Walrus) is used and configure connection accordingly. Current heuristic is: everything with scheme fakes3:// is fakes3, everything else not ending with amazonaws.com is Walrus."
|
||||
default: null
|
||||
aliases: [ S3_URL ]
|
||||
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' ]
|
||||
metadata:
|
||||
description:
|
||||
- Metadata for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'.
|
||||
required: false
|
||||
default: null
|
||||
version_added: "1.6"
|
||||
region:
|
||||
description:
|
||||
- "AWS region to create the bucket in. If not set then the value of the EC2_REGION and AWS_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. Prior to ansible 1.8 this parameter could be specified but had no effect."
|
||||
required: false
|
||||
default: null
|
||||
version_added: "1.8"
|
||||
|
||||
requirements: [ "boto" ]
|
||||
author: Lester Wade, Ralph Tice
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Simple PUT operation
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put
|
||||
# Simple GET operation
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get
|
||||
# GET/download and overwrite local file (trust remote)
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get
|
||||
# GET/download and do not overwrite local file (trust remote)
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get force=false
|
||||
# PUT/upload and overwrite remote file (trust local)
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put
|
||||
# PUT/upload with metadata
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put metadata='Content-Encoding=gzip'
|
||||
# PUT/upload with multiple metadata
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put metadata='Content-Encoding=gzip,Cache-Control=no-cache'
|
||||
# PUT/upload and do not overwrite remote file (trust local)
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put force=false
|
||||
# Download an object as a string to use else where in your playbook
|
||||
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=getstr
|
||||
# Create an empty bucket
|
||||
- s3: bucket=mybucket mode=create
|
||||
# Create a bucket with key as directory
|
||||
- s3: bucket=mybucket object=/my/directory/path mode=create
|
||||
# Create an empty bucket in the EU region
|
||||
- s3: bucket=mybucket mode=create region=eu-west-1
|
||||
# Delete a bucket and all contents
|
||||
- s3: bucket=mybucket mode=delete
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import urlparse
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
import boto
|
||||
from boto.s3.connection import Location
|
||||
except ImportError:
|
||||
print "failed=True msg='boto required for this module'"
|
||||
sys.exit(1)
|
||||
|
||||
def key_check(module, s3, bucket, obj):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
key_check = bucket.get_key(obj)
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
if key_check:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def keysum(module, s3, bucket, obj):
|
||||
bucket = s3.lookup(bucket)
|
||||
key_check = bucket.get_key(obj)
|
||||
if not key_check:
|
||||
return None
|
||||
md5_remote = key_check.etag[1:-1]
|
||||
etag_multipart = '-' in md5_remote # Check for multipart, etag is not md5
|
||||
if etag_multipart is True:
|
||||
module.fail_json(msg="Files uploaded with multipart of s3 are not supported with checksum, unable to compute checksum.")
|
||||
return md5_remote
|
||||
|
||||
def bucket_check(module, s3, bucket):
|
||||
try:
|
||||
result = s3.lookup(bucket)
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
if result:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create_bucket(module, s3, bucket, location=Location.DEFAULT):
|
||||
try:
|
||||
bucket = s3.create_bucket(bucket, location=location)
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
if bucket:
|
||||
return True
|
||||
|
||||
def delete_bucket(module, s3, bucket):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
bucket_contents = bucket.list()
|
||||
bucket.delete_keys([key.name for key in bucket_contents])
|
||||
bucket.delete()
|
||||
return True
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def delete_key(module, s3, bucket, obj):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
bucket.delete_key(obj)
|
||||
module.exit_json(msg="Object deleted from bucket %s"%bucket, changed=True)
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def create_dirkey(module, s3, bucket, obj):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
key = bucket.new_key(obj)
|
||||
key.set_contents_from_string('')
|
||||
module.exit_json(msg="Virtual directory %s created in bucket %s" % (obj, bucket.name), changed=True)
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def upload_file_check(src):
|
||||
if os.path.exists(src):
|
||||
file_exists is True
|
||||
else:
|
||||
file_exists is False
|
||||
if os.path.isdir(src):
|
||||
module.fail_json(msg="Specifying a directory is not a valid source for upload.", failed=True)
|
||||
return file_exists
|
||||
|
||||
def path_check(path):
|
||||
if os.path.exists(path):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def upload_s3file(module, s3, bucket, obj, src, expiry, metadata):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
key = bucket.new_key(obj)
|
||||
if metadata:
|
||||
for meta_key in metadata.keys():
|
||||
key.set_metadata(meta_key, metadata[meta_key])
|
||||
|
||||
key.set_contents_from_filename(src)
|
||||
url = key.generate_url(expiry)
|
||||
module.exit_json(msg="PUT operation complete", url=url, changed=True)
|
||||
except s3.provider.storage_copy_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def download_s3file(module, s3, bucket, obj, dest):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
key = bucket.lookup(obj)
|
||||
key.get_contents_to_filename(dest)
|
||||
module.exit_json(msg="GET operation complete", changed=True)
|
||||
except s3.provider.storage_copy_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def download_s3str(module, s3, bucket, obj):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
key = bucket.lookup(obj)
|
||||
contents = key.get_contents_as_string()
|
||||
module.exit_json(msg="GET operation complete", contents=contents, changed=True)
|
||||
except s3.provider.storage_copy_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def get_download_url(module, s3, bucket, obj, expiry, changed=True):
|
||||
try:
|
||||
bucket = s3.lookup(bucket)
|
||||
key = bucket.lookup(obj)
|
||||
url = key.generate_url(expiry)
|
||||
module.exit_json(msg="Download url:", url=url, expiry=expiry, changed=changed)
|
||||
except s3.provider.storage_response_error, e:
|
||||
module.fail_json(msg= str(e))
|
||||
|
||||
def is_fakes3(s3_url):
|
||||
""" Return True if s3_url has scheme fakes3:// """
|
||||
if s3_url is not None:
|
||||
return urlparse.urlparse(s3_url).scheme == 'fakes3'
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_walrus(s3_url):
|
||||
""" Return True if it's Walrus endpoint, not S3
|
||||
|
||||
We assume anything other than *.amazonaws.com is Walrus"""
|
||||
if s3_url is not None:
|
||||
o = urlparse.urlparse(s3_url)
|
||||
return not o.hostname.endswith('amazonaws.com')
|
||||
else:
|
||||
return False
|
||||
|
||||
def main():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
bucket = dict(required=True),
|
||||
object = dict(),
|
||||
src = dict(),
|
||||
dest = dict(default=None),
|
||||
mode = dict(choices=['get', 'put', 'delete', 'create', 'geturl', 'getstr'], required=True),
|
||||
expiry = dict(default=600, aliases=['expiration']),
|
||||
s3_url = dict(aliases=['S3_URL']),
|
||||
overwrite = dict(aliases=['force'], default=True, type='bool'),
|
||||
metadata = dict(type='dict'),
|
||||
),
|
||||
)
|
||||
module = AnsibleModule(argument_spec=argument_spec)
|
||||
|
||||
bucket = module.params.get('bucket')
|
||||
obj = module.params.get('object')
|
||||
src = module.params.get('src')
|
||||
if module.params.get('dest'):
|
||||
dest = os.path.expanduser(module.params.get('dest'))
|
||||
mode = module.params.get('mode')
|
||||
expiry = int(module.params['expiry'])
|
||||
s3_url = module.params.get('s3_url')
|
||||
overwrite = module.params.get('overwrite')
|
||||
metadata = module.params.get('metadata')
|
||||
|
||||
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
|
||||
|
||||
if region in ('us-east-1', '', None):
|
||||
# S3ism for the US Standard region
|
||||
location = Location.DEFAULT
|
||||
else:
|
||||
# Boto uses symbolic names for locations but region strings will
|
||||
# actually work fine for everything except us-east-1 (US Standard)
|
||||
location = region
|
||||
|
||||
if module.params.get('object'):
|
||||
obj = os.path.expanduser(module.params['object'])
|
||||
|
||||
# allow eucarc environment variables to be used if ansible vars aren't set
|
||||
if not s3_url and 'S3_URL' in os.environ:
|
||||
s3_url = os.environ['S3_URL']
|
||||
|
||||
# Look at s3_url and tweak connection settings
|
||||
# if connecting to Walrus or fakes3
|
||||
if is_fakes3(s3_url):
|
||||
try:
|
||||
fakes3 = urlparse.urlparse(s3_url)
|
||||
from boto.s3.connection import OrdinaryCallingFormat
|
||||
s3 = boto.connect_s3(
|
||||
aws_access_key,
|
||||
aws_secret_key,
|
||||
is_secure=False,
|
||||
host=fakes3.hostname,
|
||||
port=fakes3.port,
|
||||
calling_format=OrdinaryCallingFormat())
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg = str(e))
|
||||
elif is_walrus(s3_url):
|
||||
try:
|
||||
walrus = urlparse.urlparse(s3_url).hostname
|
||||
s3 = boto.connect_walrus(walrus, aws_access_key, aws_secret_key)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg = str(e))
|
||||
else:
|
||||
try:
|
||||
s3 = boto.connect_s3(aws_access_key, aws_secret_key)
|
||||
except boto.exception.NoAuthHandlerFound, e:
|
||||
module.fail_json(msg = str(e))
|
||||
|
||||
# If our mode is a GET operation (download), go through the procedure as appropriate ...
|
||||
if mode == 'get':
|
||||
|
||||
# First, we check to see if the bucket exists, we get "bucket" returned.
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if bucketrtn is False:
|
||||
module.fail_json(msg="Target bucket cannot be found", failed=True)
|
||||
|
||||
# Next, we check to see if the key in the bucket exists. If it exists, it also returns key_matches md5sum check.
|
||||
keyrtn = key_check(module, s3, bucket, obj)
|
||||
if keyrtn is False:
|
||||
module.fail_json(msg="Target key cannot be found", failed=True)
|
||||
|
||||
# If the destination path doesn't exist, no need to md5um etag check, so just download.
|
||||
pathrtn = path_check(dest)
|
||||
if pathrtn is False:
|
||||
download_s3file(module, s3, bucket, obj, dest)
|
||||
|
||||
# Compare the remote MD5 sum of the object with the local dest md5sum, if it already exists.
|
||||
if pathrtn is True:
|
||||
md5_remote = keysum(module, s3, bucket, obj)
|
||||
md5_local = hashlib.md5(open(dest, 'rb').read()).hexdigest()
|
||||
if md5_local == md5_remote:
|
||||
sum_matches = True
|
||||
if overwrite is True:
|
||||
download_s3file(module, s3, bucket, obj, dest)
|
||||
else:
|
||||
module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)
|
||||
else:
|
||||
sum_matches = False
|
||||
if overwrite is True:
|
||||
download_s3file(module, s3, bucket, obj, dest)
|
||||
else:
|
||||
module.fail_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.", failed=True)
|
||||
|
||||
# Firstly, if key_matches is TRUE and overwrite is not enabled, we EXIT with a helpful message.
|
||||
if sum_matches is True and overwrite is False:
|
||||
module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)
|
||||
|
||||
# At this point explicitly define the overwrite condition.
|
||||
if sum_matches is True and pathrtn is True and overwrite is True:
|
||||
download_s3file(module, s3, bucket, obj, dest)
|
||||
|
||||
# If sum does not match but the destination exists, we
|
||||
|
||||
# if our mode is a PUT operation (upload), go through the procedure as appropriate ...
|
||||
if mode == 'put':
|
||||
|
||||
# Use this snippet to debug through conditionals:
|
||||
# module.exit_json(msg="Bucket return %s"%bucketrtn)
|
||||
# sys.exit(0)
|
||||
|
||||
# Lets check the src path.
|
||||
pathrtn = path_check(src)
|
||||
if pathrtn is False:
|
||||
module.fail_json(msg="Local object for PUT does not exist", failed=True)
|
||||
|
||||
# Lets check to see if bucket exists to get ground truth.
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if bucketrtn is True:
|
||||
keyrtn = key_check(module, s3, bucket, obj)
|
||||
|
||||
# Lets check key state. Does it exist and if it does, compute the etag md5sum.
|
||||
if bucketrtn is True and keyrtn is True:
|
||||
md5_remote = keysum(module, s3, bucket, obj)
|
||||
md5_local = hashlib.md5(open(src, 'rb').read()).hexdigest()
|
||||
if md5_local == md5_remote:
|
||||
sum_matches = True
|
||||
if overwrite is True:
|
||||
upload_s3file(module, s3, bucket, obj, src, expiry, metadata)
|
||||
else:
|
||||
get_download_url(module, s3, bucket, obj, expiry, changed=False)
|
||||
else:
|
||||
sum_matches = False
|
||||
if overwrite is True:
|
||||
upload_s3file(module, s3, bucket, obj, src, expiry, metadata)
|
||||
else:
|
||||
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force upload.", failed=True)
|
||||
|
||||
# If neither exist (based on bucket existence), we can create both.
|
||||
if bucketrtn is False and pathrtn is True:
|
||||
create_bucket(module, s3, bucket, location)
|
||||
upload_s3file(module, s3, bucket, obj, src, expiry, metadata)
|
||||
|
||||
# If bucket exists but key doesn't, just upload.
|
||||
if bucketrtn is True and pathrtn is True and keyrtn is False:
|
||||
upload_s3file(module, s3, bucket, obj, src, expiry, metadata)
|
||||
|
||||
# Support for deleting an object if we have both params.
|
||||
if mode == 'delete':
|
||||
if bucket:
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if bucketrtn is True:
|
||||
deletertn = delete_bucket(module, s3, bucket)
|
||||
if deletertn is True:
|
||||
module.exit_json(msg="Bucket %s and all keys have been deleted."%bucket, changed=True)
|
||||
else:
|
||||
module.fail_json(msg="Bucket does not exist.", changed=False)
|
||||
else:
|
||||
module.fail_json(msg="Bucket parameter is required.", failed=True)
|
||||
|
||||
# Need to research how to create directories without "populating" a key, so this should just do bucket creation for now.
|
||||
# WE SHOULD ENABLE SOME WAY OF CREATING AN EMPTY KEY TO CREATE "DIRECTORY" STRUCTURE, AWS CONSOLE DOES THIS.
|
||||
if mode == 'create':
|
||||
if bucket and not obj:
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if bucketrtn is True:
|
||||
module.exit_json(msg="Bucket already exists.", changed=False)
|
||||
else:
|
||||
module.exit_json(msg="Bucket created successfully", changed=create_bucket(module, s3, bucket, location))
|
||||
if bucket and obj:
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if obj.endswith('/'):
|
||||
dirobj = obj
|
||||
else:
|
||||
dirobj = obj + "/"
|
||||
if bucketrtn is True:
|
||||
keyrtn = key_check(module, s3, bucket, dirobj)
|
||||
if keyrtn is True:
|
||||
module.exit_json(msg="Bucket %s and key %s already exists."% (bucket, obj), changed=False)
|
||||
else:
|
||||
create_dirkey(module, s3, bucket, dirobj)
|
||||
if bucketrtn is False:
|
||||
created = create_bucket(module, s3, bucket, location)
|
||||
create_dirkey(module, s3, bucket, dirobj)
|
||||
|
||||
# Support for grabbing the time-expired URL for an object in S3/Walrus.
|
||||
if mode == 'geturl':
|
||||
if bucket and obj:
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if bucketrtn is False:
|
||||
module.fail_json(msg="Bucket %s does not exist."%bucket, failed=True)
|
||||
else:
|
||||
keyrtn = key_check(module, s3, bucket, obj)
|
||||
if keyrtn is True:
|
||||
get_download_url(module, s3, bucket, obj, expiry)
|
||||
else:
|
||||
module.fail_json(msg="Key %s does not exist."%obj, failed=True)
|
||||
else:
|
||||
module.fail_json(msg="Bucket and Object parameters must be set", failed=True)
|
||||
|
||||
if mode == 'getstr':
|
||||
if bucket and obj:
|
||||
bucketrtn = bucket_check(module, s3, bucket)
|
||||
if bucketrtn is False:
|
||||
module.fail_json(msg="Bucket %s does not exist."%bucket, failed=True)
|
||||
else:
|
||||
keyrtn = key_check(module, s3, bucket, obj)
|
||||
if keyrtn is True:
|
||||
download_s3str(module, s3, bucket, obj)
|
||||
else:
|
||||
module.fail_json(msg="Key %s does not exist."%obj, failed=True)
|
||||
|
||||
module.exit_json(failed=False)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.ec2 import *
|
||||
|
||||
main()
|
@ -1,493 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Virt management features
|
||||
|
||||
Copyright 2007, 2012 Red Hat, Inc
|
||||
Michael DeHaan <michael.dehaan@gmail.com>
|
||||
Seth Vidal <skvidal@fedoraproject.org>
|
||||
|
||||
This software may be freely redistributed under the terms of the GNU
|
||||
general public license.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: virt
|
||||
short_description: Manages virtual machines supported by libvirt
|
||||
description:
|
||||
- Manages virtual machines supported by I(libvirt).
|
||||
version_added: "0.2"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of the guest VM being managed. Note that VM must be previously
|
||||
defined with xml.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Note that there may be some lag for state requests like C(shutdown)
|
||||
since these refer only to VM states. After starting a guest, it may not
|
||||
be immediately accessible.
|
||||
required: false
|
||||
choices: [ "running", "shutdown", "destroyed", "paused" ]
|
||||
default: "no"
|
||||
command:
|
||||
description:
|
||||
- in addition to state management, various non-idempotent commands are available. See examples
|
||||
required: false
|
||||
choices: ["create","status", "start", "stop", "pause", "unpause",
|
||||
"shutdown", "undefine", "destroy", "get_xml", "autostart",
|
||||
"freemem", "list_vms", "info", "nodeinfo", "virttype", "define"]
|
||||
uri:
|
||||
description:
|
||||
- libvirt connection uri
|
||||
required: false
|
||||
defaults: qemu:///system
|
||||
xml:
|
||||
description:
|
||||
- XML document used with the define command
|
||||
required: false
|
||||
default: null
|
||||
requirements: [ "libvirt" ]
|
||||
author: Michael DeHaan, Seth Vidal
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# a playbook task line:
|
||||
- virt: name=alpha state=running
|
||||
|
||||
# /usr/bin/ansible invocations
|
||||
ansible host -m virt -a "name=alpha command=status"
|
||||
ansible host -m virt -a "name=alpha command=get_xml"
|
||||
ansible host -m virt -a "name=alpha command=create uri=lxc:///"
|
||||
|
||||
# a playbook example of defining and launching an LXC guest
|
||||
tasks:
|
||||
- name: define vm
|
||||
virt: name=foo
|
||||
command=define
|
||||
xml="{{ lookup('template', 'container-template.xml.j2') }}"
|
||||
uri=lxc:///
|
||||
- name: start vm
|
||||
virt: name=foo state=running uri=lxc:///
|
||||
'''
|
||||
|
||||
VIRT_FAILED = 1
|
||||
VIRT_SUCCESS = 0
|
||||
VIRT_UNAVAILABLE=2
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import libvirt
|
||||
except ImportError:
|
||||
print "failed=True msg='libvirt python module unavailable'"
|
||||
sys.exit(1)
|
||||
|
||||
ALL_COMMANDS = []
|
||||
VM_COMMANDS = ['create','status', 'start', 'stop', 'pause', 'unpause',
|
||||
'shutdown', 'undefine', 'destroy', 'get_xml', 'autostart', 'define']
|
||||
HOST_COMMANDS = ['freemem', 'list_vms', 'info', 'nodeinfo', 'virttype']
|
||||
ALL_COMMANDS.extend(VM_COMMANDS)
|
||||
ALL_COMMANDS.extend(HOST_COMMANDS)
|
||||
|
||||
VIRT_STATE_NAME_MAP = {
|
||||
0 : "running",
|
||||
1 : "running",
|
||||
2 : "running",
|
||||
3 : "paused",
|
||||
4 : "shutdown",
|
||||
5 : "shutdown",
|
||||
6 : "crashed"
|
||||
}
|
||||
|
||||
class VMNotFound(Exception):
|
||||
pass
|
||||
|
||||
class LibvirtConnection(object):
|
||||
|
||||
def __init__(self, uri, module):
|
||||
|
||||
self.module = module
|
||||
|
||||
cmd = "uname -r"
|
||||
rc, stdout, stderr = self.module.run_command(cmd)
|
||||
|
||||
if "xen" in stdout:
|
||||
conn = libvirt.open(None)
|
||||
else:
|
||||
conn = libvirt.open(uri)
|
||||
|
||||
if not conn:
|
||||
raise Exception("hypervisor connection failure")
|
||||
|
||||
self.conn = conn
|
||||
|
||||
def find_vm(self, vmid):
|
||||
"""
|
||||
Extra bonus feature: vmid = -1 returns a list of everything
|
||||
"""
|
||||
conn = self.conn
|
||||
|
||||
vms = []
|
||||
|
||||
# this block of code borrowed from virt-manager:
|
||||
# get working domain's name
|
||||
ids = conn.listDomainsID()
|
||||
for id in ids:
|
||||
vm = conn.lookupByID(id)
|
||||
vms.append(vm)
|
||||
# get defined domain
|
||||
names = conn.listDefinedDomains()
|
||||
for name in names:
|
||||
vm = conn.lookupByName(name)
|
||||
vms.append(vm)
|
||||
|
||||
if vmid == -1:
|
||||
return vms
|
||||
|
||||
for vm in vms:
|
||||
if vm.name() == vmid:
|
||||
return vm
|
||||
|
||||
raise VMNotFound("virtual machine %s not found" % vmid)
|
||||
|
||||
def shutdown(self, vmid):
|
||||
return self.find_vm(vmid).shutdown()
|
||||
|
||||
def pause(self, vmid):
|
||||
return self.suspend(self.conn,vmid)
|
||||
|
||||
def unpause(self, vmid):
|
||||
return self.resume(self.conn,vmid)
|
||||
|
||||
def suspend(self, vmid):
|
||||
return self.find_vm(vmid).suspend()
|
||||
|
||||
def resume(self, vmid):
|
||||
return self.find_vm(vmid).resume()
|
||||
|
||||
def create(self, vmid):
|
||||
return self.find_vm(vmid).create()
|
||||
|
||||
def destroy(self, vmid):
|
||||
return self.find_vm(vmid).destroy()
|
||||
|
||||
def undefine(self, vmid):
|
||||
return self.find_vm(vmid).undefine()
|
||||
|
||||
def get_status2(self, vm):
|
||||
state = vm.info()[0]
|
||||
return VIRT_STATE_NAME_MAP.get(state,"unknown")
|
||||
|
||||
def get_status(self, vmid):
|
||||
state = self.find_vm(vmid).info()[0]
|
||||
return VIRT_STATE_NAME_MAP.get(state,"unknown")
|
||||
|
||||
def nodeinfo(self):
|
||||
return self.conn.getInfo()
|
||||
|
||||
def get_type(self):
|
||||
return self.conn.getType()
|
||||
|
||||
def get_xml(self, vmid):
|
||||
vm = self.conn.lookupByName(vmid)
|
||||
return vm.XMLDesc(0)
|
||||
|
||||
def get_maxVcpus(self, vmid):
|
||||
vm = self.conn.lookupByName(vmid)
|
||||
return vm.maxVcpus()
|
||||
|
||||
def get_maxMemory(self, vmid):
|
||||
vm = self.conn.lookupByName(vmid)
|
||||
return vm.maxMemory()
|
||||
|
||||
def getFreeMemory(self):
|
||||
return self.conn.getFreeMemory()
|
||||
|
||||
def get_autostart(self, vmid):
|
||||
vm = self.conn.lookupByName(vmid)
|
||||
return vm.autostart()
|
||||
|
||||
def set_autostart(self, vmid, val):
|
||||
vm = self.conn.lookupByName(vmid)
|
||||
return vm.setAutostart(val)
|
||||
|
||||
def define_from_xml(self, xml):
|
||||
return self.conn.defineXML(xml)
|
||||
|
||||
|
||||
class Virt(object):
|
||||
|
||||
def __init__(self, uri, module):
|
||||
self.module = module
|
||||
self.uri = uri
|
||||
|
||||
def __get_conn(self):
|
||||
self.conn = LibvirtConnection(self.uri, self.module)
|
||||
return self.conn
|
||||
|
||||
def get_vm(self, vmid):
|
||||
self.__get_conn()
|
||||
return self.conn.find_vm(vmid)
|
||||
|
||||
def state(self):
|
||||
vms = self.list_vms()
|
||||
state = []
|
||||
for vm in vms:
|
||||
state_blurb = self.conn.get_status(vm)
|
||||
state.append("%s %s" % (vm,state_blurb))
|
||||
return state
|
||||
|
||||
def info(self):
|
||||
vms = self.list_vms()
|
||||
info = dict()
|
||||
for vm in vms:
|
||||
data = self.conn.find_vm(vm).info()
|
||||
# libvirt returns maxMem, memory, and cpuTime as long()'s, which
|
||||
# xmlrpclib tries to convert to regular int's during serialization.
|
||||
# This throws exceptions, so convert them to strings here and
|
||||
# assume the other end of the xmlrpc connection can figure things
|
||||
# out or doesn't care.
|
||||
info[vm] = {
|
||||
"state" : VIRT_STATE_NAME_MAP.get(data[0],"unknown"),
|
||||
"maxMem" : str(data[1]),
|
||||
"memory" : str(data[2]),
|
||||
"nrVirtCpu" : data[3],
|
||||
"cpuTime" : str(data[4]),
|
||||
}
|
||||
info[vm]["autostart"] = self.conn.get_autostart(vm)
|
||||
|
||||
return info
|
||||
|
||||
def nodeinfo(self):
|
||||
self.__get_conn()
|
||||
info = dict()
|
||||
data = self.conn.nodeinfo()
|
||||
info = {
|
||||
"cpumodel" : str(data[0]),
|
||||
"phymemory" : str(data[1]),
|
||||
"cpus" : str(data[2]),
|
||||
"cpumhz" : str(data[3]),
|
||||
"numanodes" : str(data[4]),
|
||||
"sockets" : str(data[5]),
|
||||
"cpucores" : str(data[6]),
|
||||
"cputhreads" : str(data[7])
|
||||
}
|
||||
return info
|
||||
|
||||
def list_vms(self, state=None):
|
||||
self.conn = self.__get_conn()
|
||||
vms = self.conn.find_vm(-1)
|
||||
results = []
|
||||
for x in vms:
|
||||
try:
|
||||
if state:
|
||||
vmstate = self.conn.get_status2(x)
|
||||
if vmstate == state:
|
||||
results.append(x.name())
|
||||
else:
|
||||
results.append(x.name())
|
||||
except:
|
||||
pass
|
||||
return results
|
||||
|
||||
def virttype(self):
|
||||
return self.__get_conn().get_type()
|
||||
|
||||
def autostart(self, vmid):
|
||||
self.conn = self.__get_conn()
|
||||
return self.conn.set_autostart(vmid, True)
|
||||
|
||||
def freemem(self):
|
||||
self.conn = self.__get_conn()
|
||||
return self.conn.getFreeMemory()
|
||||
|
||||
def shutdown(self, vmid):
|
||||
""" Make the machine with the given vmid stop running. Whatever that takes. """
|
||||
self.__get_conn()
|
||||
self.conn.shutdown(vmid)
|
||||
return 0
|
||||
|
||||
|
||||
def pause(self, vmid):
|
||||
""" Pause the machine with the given vmid. """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.suspend(vmid)
|
||||
|
||||
def unpause(self, vmid):
|
||||
""" Unpause the machine with the given vmid. """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.resume(vmid)
|
||||
|
||||
def create(self, vmid):
|
||||
""" Start the machine via the given vmid """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.create(vmid)
|
||||
|
||||
def start(self, vmid):
|
||||
""" Start the machine via the given id/name """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.create(vmid)
|
||||
|
||||
def destroy(self, vmid):
|
||||
""" Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """
|
||||
self.__get_conn()
|
||||
return self.conn.destroy(vmid)
|
||||
|
||||
def undefine(self, vmid):
|
||||
""" Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.undefine(vmid)
|
||||
|
||||
def status(self, vmid):
|
||||
"""
|
||||
Return a state suitable for server consumption. Aka, codes.py values, not XM output.
|
||||
"""
|
||||
self.__get_conn()
|
||||
return self.conn.get_status(vmid)
|
||||
|
||||
def get_xml(self, vmid):
|
||||
"""
|
||||
Receive a Vm id as input
|
||||
Return an xml describing vm config returned by a libvirt call
|
||||
"""
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.get_xml(vmid)
|
||||
|
||||
def get_maxVcpus(self, vmid):
|
||||
"""
|
||||
Gets the max number of VCPUs on a guest
|
||||
"""
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.get_maxVcpus(vmid)
|
||||
|
||||
def get_max_memory(self, vmid):
|
||||
"""
|
||||
Gets the max memory on a guest
|
||||
"""
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.get_MaxMemory(vmid)
|
||||
|
||||
def define(self, xml):
|
||||
"""
|
||||
Define a guest with the given xml
|
||||
"""
|
||||
self.__get_conn()
|
||||
return self.conn.define_from_xml(xml)
|
||||
|
||||
def core(module):
|
||||
|
||||
state = module.params.get('state', None)
|
||||
guest = module.params.get('name', None)
|
||||
command = module.params.get('command', None)
|
||||
uri = module.params.get('uri', None)
|
||||
xml = module.params.get('xml', None)
|
||||
|
||||
v = Virt(uri, module)
|
||||
res = {}
|
||||
|
||||
if state and command=='list_vms':
|
||||
res = v.list_vms(state=state)
|
||||
if type(res) != dict:
|
||||
res = { command: res }
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
if state:
|
||||
if not guest:
|
||||
module.fail_json(msg = "state change requires a guest specified")
|
||||
|
||||
res['changed'] = False
|
||||
if state == 'running':
|
||||
if v.status(guest) is 'paused':
|
||||
res['changed'] = True
|
||||
res['msg'] = v.unpause(guest)
|
||||
elif v.status(guest) is not 'running':
|
||||
res['changed'] = True
|
||||
res['msg'] = v.start(guest)
|
||||
elif state == 'shutdown':
|
||||
if v.status(guest) is not 'shutdown':
|
||||
res['changed'] = True
|
||||
res['msg'] = v.shutdown(guest)
|
||||
elif state == 'destroyed':
|
||||
if v.status(guest) is not 'shutdown':
|
||||
res['changed'] = True
|
||||
res['msg'] = v.destroy(guest)
|
||||
elif state == 'paused':
|
||||
if v.status(guest) is 'running':
|
||||
res['changed'] = True
|
||||
res['msg'] = v.pause(guest)
|
||||
else:
|
||||
module.fail_json(msg="unexpected state")
|
||||
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
if command:
|
||||
if command in VM_COMMANDS:
|
||||
if not guest:
|
||||
module.fail_json(msg = "%s requires 1 argument: guest" % command)
|
||||
if command == 'define':
|
||||
if not xml:
|
||||
module.fail_json(msg = "define requires xml argument")
|
||||
try:
|
||||
v.get_vm(guest)
|
||||
except VMNotFound:
|
||||
v.define(xml)
|
||||
res = {'changed': True, 'created': guest}
|
||||
return VIRT_SUCCESS, res
|
||||
res = getattr(v, command)(guest)
|
||||
if type(res) != dict:
|
||||
res = { command: res }
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
elif hasattr(v, command):
|
||||
res = getattr(v, command)()
|
||||
if type(res) != dict:
|
||||
res = { command: res }
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
else:
|
||||
module.fail_json(msg="Command %s not recognized" % basecmd)
|
||||
|
||||
module.fail_json(msg="expected state or command parameter to be specified")
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(argument_spec=dict(
|
||||
name = dict(aliases=['guest']),
|
||||
state = dict(choices=['running', 'shutdown', 'destroyed', 'paused']),
|
||||
command = dict(choices=ALL_COMMANDS),
|
||||
uri = dict(default='qemu:///system'),
|
||||
xml = dict(),
|
||||
))
|
||||
|
||||
rc = VIRT_SUCCESS
|
||||
try:
|
||||
rc, result = core(module)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if rc != 0: # something went wrong emit the msg
|
||||
module.fail_json(rc=rc, msg=result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
File diff suppressed because it is too large
Load Diff
@ -1,275 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import sys
|
||||
import datetime
|
||||
import traceback
|
||||
import re
|
||||
import shlex
|
||||
import os
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: command
|
||||
version_added: historical
|
||||
short_description: Executes a command on a remote node
|
||||
description:
|
||||
- The M(command) module takes the command name followed by a list of space-delimited arguments.
|
||||
- The given command will be executed on all selected nodes. It will not be
|
||||
processed through the shell, so variables like C($HOME) and operations
|
||||
like C("<"), C(">"), C("|"), and C("&") will not work (use the M(shell)
|
||||
module if you need these features).
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- the command module takes a free form command to run. There is no parameter actually named 'free form'.
|
||||
See the examples!
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
creates:
|
||||
description:
|
||||
- a filename, when it already exists, this step will B(not) be run.
|
||||
required: no
|
||||
default: null
|
||||
removes:
|
||||
description:
|
||||
- a filename, when it does not exist, this step will B(not) be run.
|
||||
version_added: "0.8"
|
||||
required: no
|
||||
default: null
|
||||
chdir:
|
||||
description:
|
||||
- cd into this directory before running the command
|
||||
version_added: "0.6"
|
||||
required: false
|
||||
default: null
|
||||
executable:
|
||||
description:
|
||||
- change the shell used to execute the command. Should be an absolute path to the executable.
|
||||
required: false
|
||||
default: null
|
||||
version_added: "0.9"
|
||||
warn:
|
||||
version_added: "1.8"
|
||||
default: yes
|
||||
description:
|
||||
- if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false.
|
||||
required: false
|
||||
default: True
|
||||
notes:
|
||||
- If you want to run a command through the shell (say you are using C(<),
|
||||
C(>), C(|), etc), you actually want the M(shell) module instead. The
|
||||
M(command) module is much more secure as it's not affected by the user's
|
||||
environment.
|
||||
- " C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not exist, use this."
|
||||
author: Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example from Ansible Playbooks.
|
||||
- command: /sbin/shutdown -t now
|
||||
|
||||
# Run the command if the specified file does not exist.
|
||||
- command: /usr/bin/make_database.sh arg1 arg2 creates=/path/to/database
|
||||
|
||||
# You can also use the 'args' form to provide the options. This command
|
||||
# will change the working directory to somedir/ and will only run when
|
||||
# /path/to/database doesn't exist.
|
||||
- command: /usr/bin/make_database.sh arg1 arg2
|
||||
args:
|
||||
chdir: somedir/
|
||||
creates: /path/to/database
|
||||
'''
|
||||
|
||||
# This is a pretty complex regex, which functions as follows:
|
||||
#
|
||||
# 1. (^|\s)
|
||||
# ^ look for a space or the beginning of the line
|
||||
# 2. (creates|removes|chdir|executable|NO_LOG)=
|
||||
# ^ look for a valid param, followed by an '='
|
||||
# 3. (?P<quote>[\'"])?
|
||||
# ^ look for an optional quote character, which can either be
|
||||
# a single or double quote character, and store it for later
|
||||
# 4. (.*?)
|
||||
# ^ match everything in a non-greedy manner until...
|
||||
# 5. (?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)
|
||||
# ^ a non-escaped space or a non-escaped quote of the same kind
|
||||
# that was matched in the first 'quote' is found, or the end of
|
||||
# the line is reached
|
||||
|
||||
PARAM_REGEX = re.compile(r'(^|\s)(creates|removes|chdir|executable|NO_LOG|warn)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
|
||||
|
||||
|
||||
def check_command(commandline):
|
||||
arguments = { 'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group',
|
||||
'ln': 'state=link', 'mkdir': 'state=directory',
|
||||
'rmdir': 'state=absent', 'rm': 'state=absent', 'touch': 'state=touch' }
|
||||
commands = { 'git': 'git', 'hg': 'hg', 'curl': 'get_url', 'wget': 'get_url',
|
||||
'svn': 'subversion', 'service': 'service',
|
||||
'mount': 'mount', 'rpm': 'yum', 'yum': 'yum', 'apt-get': 'apt-get',
|
||||
'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'template or lineinfile',
|
||||
'rsync': 'synchronize' }
|
||||
warnings = list()
|
||||
command = os.path.basename(commandline.split()[0])
|
||||
if command in arguments:
|
||||
warnings.append("Consider using file module with %s rather than running %s" % (arguments[command], command))
|
||||
if command in commands:
|
||||
warnings.append("Consider using %s module rather than running %s" % (commands[command], command))
|
||||
return warnings
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# the command module is the one ansible module that does not take key=value args
|
||||
# hence don't copy this one if you are looking to build others!
|
||||
module = CommandModule(argument_spec=dict())
|
||||
|
||||
shell = module.params['shell']
|
||||
chdir = module.params['chdir']
|
||||
executable = module.params['executable']
|
||||
args = module.params['args']
|
||||
creates = module.params['creates']
|
||||
removes = module.params['removes']
|
||||
warn = module.params.get('warn', True)
|
||||
|
||||
if args.strip() == '':
|
||||
module.fail_json(rc=256, msg="no command given")
|
||||
|
||||
if chdir:
|
||||
os.chdir(chdir)
|
||||
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
v = os.path.expanduser(creates)
|
||||
if os.path.exists(v):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s exists" % v,
|
||||
changed=False,
|
||||
stderr=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if removes:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename does not exist. This allows idempotence
|
||||
# of command executions.
|
||||
v = os.path.expanduser(removes)
|
||||
if not os.path.exists(v):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s does not exist" % v,
|
||||
changed=False,
|
||||
stderr=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
warnings = list()
|
||||
if warn:
|
||||
warnings = check_command(args)
|
||||
|
||||
if not shell:
|
||||
args = shlex.split(args)
|
||||
startd = datetime.datetime.now()
|
||||
|
||||
rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell)
|
||||
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
|
||||
if out is None:
|
||||
out = ''
|
||||
if err is None:
|
||||
err = ''
|
||||
|
||||
module.exit_json(
|
||||
cmd = args,
|
||||
stdout = out.rstrip("\r\n"),
|
||||
stderr = err.rstrip("\r\n"),
|
||||
rc = rc,
|
||||
start = str(startd),
|
||||
end = str(endd),
|
||||
delta = str(delta),
|
||||
changed = True,
|
||||
warnings = warnings
|
||||
)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.splitter import *
|
||||
|
||||
# only the command module should ever need to do this
|
||||
# everything else should be simple key=value
|
||||
|
||||
class CommandModule(AnsibleModule):
|
||||
|
||||
def _handle_aliases(self):
|
||||
return {}
|
||||
|
||||
def _check_invalid_arguments(self):
|
||||
pass
|
||||
|
||||
def _load_params(self):
|
||||
''' read the input and return a dictionary and the arguments string '''
|
||||
args = MODULE_ARGS
|
||||
params = {}
|
||||
params['chdir'] = None
|
||||
params['creates'] = None
|
||||
params['removes'] = None
|
||||
params['shell'] = False
|
||||
params['executable'] = None
|
||||
params['warn'] = True
|
||||
if "#USE_SHELL" in args:
|
||||
args = args.replace("#USE_SHELL", "")
|
||||
params['shell'] = True
|
||||
|
||||
items = split_args(args)
|
||||
|
||||
for x in items:
|
||||
quoted = x.startswith('"') and x.endswith('"') or x.startswith("'") and x.endswith("'")
|
||||
if '=' in x and not quoted:
|
||||
# check to see if this is a special parameter for the command
|
||||
k, v = x.split('=', 1)
|
||||
v = unquote(v)
|
||||
# because we're not breaking out quotes in the shlex split
|
||||
# above, the value of the k=v pair may still be quoted. If
|
||||
# so, remove them.
|
||||
if len(v) > 1 and (v.startswith('"') and v.endswith('"') or v.startswith("'") and v.endswith("'")):
|
||||
v = v[1:-1]
|
||||
if k in ('creates', 'removes', 'chdir', 'executable', 'NO_LOG'):
|
||||
if k == "chdir":
|
||||
v = os.path.abspath(os.path.expanduser(v))
|
||||
if not (os.path.exists(v) and os.path.isdir(v)):
|
||||
self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v)
|
||||
elif k == "executable":
|
||||
v = os.path.abspath(os.path.expanduser(v))
|
||||
if not (os.path.exists(v)):
|
||||
self.fail_json(rc=258, msg="cannot use executable '%s': file does not exist" % v)
|
||||
params[k] = v
|
||||
# Remove any of the above k=v params from the args string
|
||||
args = PARAM_REGEX.sub('', args)
|
||||
params['args'] = args.strip()
|
||||
|
||||
return (params, params['args'])
|
||||
|
||||
main()
|
@ -1,43 +0,0 @@
|
||||
# this is a virtual module that is entirely implemented server side
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: raw
|
||||
version_added: historical
|
||||
short_description: Executes a low-down and dirty SSH command
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- the raw module takes a free form command to run
|
||||
required: true
|
||||
executable:
|
||||
description:
|
||||
- change the shell used to execute the command. Should be an absolute path to the executable.
|
||||
required: false
|
||||
version_added: "1.0"
|
||||
description:
|
||||
- Executes a low-down and dirty SSH command, not going through the module
|
||||
subsystem. This is useful and should only be done in two cases. The
|
||||
first case is installing C(python-simplejson) on older (Python 2.4 and
|
||||
before) hosts that need it as a dependency to run modules, since nearly
|
||||
all core modules require it. Another is speaking to any devices such as
|
||||
routers that do not have any Python installed. In any other case, using
|
||||
the M(shell) or M(command) module is much more appropriate. Arguments
|
||||
given to M(raw) are run directly through the configured remote shell.
|
||||
Standard output, error output and return code are returned when
|
||||
available. There is no change handler support for this module.
|
||||
- This module does not require python on the remote system, much like
|
||||
the M(script) module.
|
||||
notes:
|
||||
- If you want to execute a command securely and predictably, it may be
|
||||
better to use the M(command) module instead. Best practices when writing
|
||||
playbooks will follow the trend of using M(command) unless M(shell) is
|
||||
explicitly required. When running ad-hoc commands, use your best
|
||||
judgement.
|
||||
author: Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Bootstrap a legacy python 2.4 host
|
||||
- raw: yum -y install python-simplejson
|
||||
'''
|
@ -1,47 +0,0 @@
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: script
|
||||
version_added: "0.9"
|
||||
short_description: Runs a local script on a remote node after transferring it
|
||||
description:
|
||||
- "The M(script) module takes the script name followed by a list of
|
||||
space-delimited arguments. "
|
||||
- "The local script at path will be transferred to the remote node and then executed. "
|
||||
- "The given script will be processed through the shell environment on the remote node. "
|
||||
- "This module does not require python on the remote system, much like
|
||||
the M(raw) module. "
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- path to the local script file followed by optional arguments.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
creates:
|
||||
description:
|
||||
- a filename, when it already exists, this step will B(not) be run.
|
||||
required: no
|
||||
default: null
|
||||
version_added: "1.5"
|
||||
removes:
|
||||
description:
|
||||
- a filename, when it does not exist, this step will B(not) be run.
|
||||
required: no
|
||||
default: null
|
||||
version_added: "1.5"
|
||||
notes:
|
||||
- It is usually preferable to write Ansible modules than pushing scripts. Convert your script to an Ansible module for bonus points!
|
||||
author: Michael DeHaan
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example from Ansible Playbooks
|
||||
- script: /some/local/script.sh --some-arguments 1234
|
||||
|
||||
# Run a script that creates a file, but only if the file is not yet created
|
||||
- script: /some/local/create_file.sh --some-arguments 1234 creates=/the/created/file.txt
|
||||
|
||||
# Run a script that removes a file, but only if the file is not yet removed
|
||||
- script: /some/local/remove_file.sh --some-arguments 1234 removes=/the/removed/file.txt
|
||||
'''
|
@ -1,78 +0,0 @@
|
||||
# There is actually no actual shell module source, when you use 'shell' in ansible,
|
||||
# it runs the 'command' module with special arguments and it behaves differently.
|
||||
# See the command source and the comment "#USE_SHELL".
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: shell
|
||||
short_description: Execute commands in nodes.
|
||||
description:
|
||||
- The M(shell) module takes the command name followed by a list of space-delimited arguments.
|
||||
It is almost exactly like the M(command) module but runs
|
||||
the command through a shell (C(/bin/sh)) on the remote node.
|
||||
version_added: "0.2"
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- The shell module takes a free form command to run, as a string. There's not an actual
|
||||
option named "free form". See the examples!
|
||||
required: true
|
||||
default: null
|
||||
creates:
|
||||
description:
|
||||
- a filename, when it already exists, this step will B(not) be run.
|
||||
required: no
|
||||
default: null
|
||||
removes:
|
||||
description:
|
||||
- a filename, when it does not exist, this step will B(not) be run.
|
||||
version_added: "0.8"
|
||||
required: no
|
||||
default: null
|
||||
chdir:
|
||||
description:
|
||||
- cd into this directory before running the command
|
||||
required: false
|
||||
default: null
|
||||
version_added: "0.6"
|
||||
executable:
|
||||
description:
|
||||
- change the shell used to execute the command. Should be an absolute path to the executable.
|
||||
required: false
|
||||
default: null
|
||||
version_added: "0.9"
|
||||
warn:
|
||||
description:
|
||||
- if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false.
|
||||
required: false
|
||||
default: True
|
||||
version_added: "1.8"
|
||||
notes:
|
||||
- If you want to execute a command securely and predictably, it may be
|
||||
better to use the M(command) module instead. Best practices when writing
|
||||
playbooks will follow the trend of using M(command) unless M(shell) is
|
||||
explicitly required. When running ad-hoc commands, use your best
|
||||
judgement.
|
||||
- To sanitize any variables passed to the shell module, you should use
|
||||
"{{ var | quote }}" instead of just "{{ var }}" to make sure they don't include evil things like semicolons.
|
||||
|
||||
requirements: [ ]
|
||||
author: Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Execute the command in remote shell; stdout goes to the specified
|
||||
# file on the remote.
|
||||
- shell: somescript.sh >> somelog.txt
|
||||
|
||||
# Change the working directory to somedir/ before executing the command.
|
||||
- shell: somescript.sh >> somelog.txt chdir=somedir/
|
||||
|
||||
# You can also use the 'args' form to provide the options. This command
|
||||
# will change the working directory to somedir/ and will only run when
|
||||
# somedir/somelog.txt doesn't exist.
|
||||
- shell: somescript.sh >> somelog.txt
|
||||
args:
|
||||
chdir: somedir/
|
||||
creates: somelog.txt
|
||||
'''
|
@ -1,242 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Elliott Foster <elliott@fourkitchens.com>
|
||||
# Sponsored by Four Kitchens http://fourkitchens.com.
|
||||
# (c) 2014, Epic Games, Inc.
|
||||
#
|
||||
# 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: mongodb_user
|
||||
short_description: Adds or removes a user from a MongoDB database.
|
||||
description:
|
||||
- Adds or removes a user from a MongoDB database.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
login_user:
|
||||
description:
|
||||
- The username used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_password:
|
||||
description:
|
||||
- The password used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_host:
|
||||
description:
|
||||
- The host running the database
|
||||
required: false
|
||||
default: localhost
|
||||
login_port:
|
||||
description:
|
||||
- The port to connect to
|
||||
required: false
|
||||
default: 27017
|
||||
replica_set:
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- Replica set to connect to (automatically connects to primary for writes)
|
||||
required: false
|
||||
default: null
|
||||
database:
|
||||
description:
|
||||
- The name of the database to add/remove the user from
|
||||
required: true
|
||||
user:
|
||||
description:
|
||||
- The name of the user to add or remove
|
||||
required: true
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- The password to use for the user
|
||||
required: false
|
||||
default: null
|
||||
roles:
|
||||
version_added: "1.3"
|
||||
description:
|
||||
- "The database user roles valid values are one or more of the following: read, 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase', 'dbAdminAnyDatabase'"
|
||||
- This param requires mongodb 2.4+ and pymongo 2.5+
|
||||
required: false
|
||||
default: "readWrite"
|
||||
state:
|
||||
state:
|
||||
description:
|
||||
- The database user state
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
notes:
|
||||
- Requires the pymongo Python package on the remote host, version 2.4.2+. This
|
||||
can be installed using pip or the OS package manager. @see http://api.mongodb.org/python/current/installation.html
|
||||
requirements: [ "pymongo" ]
|
||||
author: Elliott Foster
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create 'burgers' database user with name 'bob' and password '12345'.
|
||||
- mongodb_user: database=burgers name=bob password=12345 state=present
|
||||
|
||||
# Delete 'burgers' database user with name 'bob'.
|
||||
- mongodb_user: database=burgers name=bob state=absent
|
||||
|
||||
# Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style)
|
||||
- mongodb_user: database=burgers name=ben password=12345 roles='read' state=present
|
||||
- mongodb_user: database=burgers name=jim password=12345 roles='readWrite,dbAdmin,userAdmin' state=present
|
||||
- mongodb_user: database=burgers name=joe password=12345 roles='readWriteAnyDatabase' state=present
|
||||
|
||||
# add a user to database in a replica set, the primary server is automatically discovered and written to
|
||||
- mongodb_user: database=burgers name=bob replica_set=blecher password=12345 roles='readWriteAnyDatabase' state=present
|
||||
'''
|
||||
|
||||
import ConfigParser
|
||||
from distutils.version import LooseVersion
|
||||
try:
|
||||
from pymongo.errors import ConnectionFailure
|
||||
from pymongo.errors import OperationFailure
|
||||
from pymongo import version as PyMongoVersion
|
||||
from pymongo import MongoClient
|
||||
except ImportError:
|
||||
try: # for older PyMongo 2.2
|
||||
from pymongo import Connection as MongoClient
|
||||
except ImportError:
|
||||
pymongo_found = False
|
||||
else:
|
||||
pymongo_found = True
|
||||
else:
|
||||
pymongo_found = True
|
||||
|
||||
# =========================================
|
||||
# MongoDB module specific support methods.
|
||||
#
|
||||
|
||||
def user_add(module, client, db_name, user, password, roles):
|
||||
db = client[db_name]
|
||||
if roles is None:
|
||||
db.add_user(user, password, False)
|
||||
else:
|
||||
try:
|
||||
db.add_user(user, password, None, roles=roles)
|
||||
except OperationFailure, e:
|
||||
err_msg = str(e)
|
||||
if LooseVersion(PyMongoVersion) <= LooseVersion('2.5'):
|
||||
err_msg = err_msg + ' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)'
|
||||
module.fail_json(msg=err_msg)
|
||||
|
||||
def user_remove(client, db_name, user):
|
||||
db = client[db_name]
|
||||
db.remove_user(user)
|
||||
|
||||
def load_mongocnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mongocnf = os.path.expanduser('~/.mongodb.cnf')
|
||||
|
||||
try:
|
||||
config.readfp(open(mongocnf))
|
||||
creds = dict(
|
||||
user=config.get('client', 'user'),
|
||||
password=config.get('client', 'pass')
|
||||
)
|
||||
except (ConfigParser.NoOptionError, IOError):
|
||||
return False
|
||||
|
||||
return creds
|
||||
|
||||
# =========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default='localhost'),
|
||||
login_port=dict(default='27017'),
|
||||
replica_set=dict(default=None),
|
||||
database=dict(required=True, aliases=['db']),
|
||||
user=dict(required=True, aliases=['name']),
|
||||
password=dict(aliases=['pass']),
|
||||
roles=dict(default=None, type='list'),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
)
|
||||
)
|
||||
|
||||
if not pymongo_found:
|
||||
module.fail_json(msg='the python pymongo module is required')
|
||||
|
||||
login_user = module.params['login_user']
|
||||
login_password = module.params['login_password']
|
||||
login_host = module.params['login_host']
|
||||
login_port = module.params['login_port']
|
||||
replica_set = module.params['replica_set']
|
||||
db_name = module.params['database']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
roles = module.params['roles']
|
||||
state = module.params['state']
|
||||
|
||||
try:
|
||||
if replica_set:
|
||||
client = MongoClient(login_host, int(login_port), replicaset=replica_set)
|
||||
else:
|
||||
client = MongoClient(login_host, int(login_port))
|
||||
|
||||
# try to authenticate as a target user to check if it already exists
|
||||
try:
|
||||
client[db_name].authenticate(user, password)
|
||||
if state == 'present':
|
||||
module.exit_json(changed=False, user=user)
|
||||
except OperationFailure:
|
||||
if state == 'absent':
|
||||
module.exit_json(changed=False, user=user)
|
||||
|
||||
if login_user is None and login_password is None:
|
||||
mongocnf_creds = load_mongocnf()
|
||||
if mongocnf_creds is not False:
|
||||
login_user = mongocnf_creds['user']
|
||||
login_password = mongocnf_creds['password']
|
||||
elif login_password is None and login_user is not None:
|
||||
module.fail_json(msg='when supplying login arguments, both login_user and login_password must be provided')
|
||||
|
||||
if login_user is not None and login_password is not None:
|
||||
client.admin.authenticate(login_user, login_password)
|
||||
|
||||
except ConnectionFailure, e:
|
||||
module.fail_json(msg='unable to connect to database: %s' % str(e))
|
||||
|
||||
if state == 'present':
|
||||
if password is None:
|
||||
module.fail_json(msg='password parameter required when adding a user')
|
||||
|
||||
try:
|
||||
user_add(module, client, db_name, user, password, roles)
|
||||
except OperationFailure, e:
|
||||
module.fail_json(msg='Unable to add or update user: %s' % str(e))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
user_remove(client, db_name, user)
|
||||
except OperationFailure, e:
|
||||
module.fail_json(msg='Unable to remove user: %s' % str(e))
|
||||
|
||||
module.exit_json(changed=True, user=user)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,363 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
|
||||
# Sponsored by Four Kitchens http://fourkitchens.com.
|
||||
#
|
||||
# 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: mysql_db
|
||||
short_description: Add or remove MySQL databases from a remote host.
|
||||
description:
|
||||
- Add or remove MySQL databases from a remote host.
|
||||
version_added: "0.6"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of the database to add or remove
|
||||
required: true
|
||||
default: null
|
||||
aliases: [ db ]
|
||||
login_user:
|
||||
description:
|
||||
- The username used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_password:
|
||||
description:
|
||||
- The password used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_host:
|
||||
description:
|
||||
- Host running the database
|
||||
required: false
|
||||
default: localhost
|
||||
login_port:
|
||||
description:
|
||||
- Port of the MySQL server. Requires login_host be defined as other then localhost if login_port is used
|
||||
required: false
|
||||
default: 3306
|
||||
login_unix_socket:
|
||||
description:
|
||||
- The path to a Unix domain socket for local connections
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- The database state
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent", "dump", "import" ]
|
||||
collation:
|
||||
description:
|
||||
- Collation mode
|
||||
required: false
|
||||
default: null
|
||||
encoding:
|
||||
description:
|
||||
- Encoding mode
|
||||
required: false
|
||||
default: null
|
||||
target:
|
||||
description:
|
||||
- Location, on the remote host, of the dump file to read from or write to. Uncompressed SQL
|
||||
files (C(.sql)) as well as bzip2 (C(.bz2)) and gzip (C(.gz)) compressed files are supported.
|
||||
required: false
|
||||
notes:
|
||||
- Requires the MySQLdb Python package on the remote host. For Ubuntu, this
|
||||
is as easy as apt-get install python-mysqldb. (See M(apt).)
|
||||
- Both I(login_password) and I(login_user) are required when you are
|
||||
passing credentials. If none are present, the module will attempt to read
|
||||
the credentials from C(~/.my.cnf), and finally fall back to using the MySQL
|
||||
default login of C(root) with no password.
|
||||
requirements: [ ConfigParser ]
|
||||
author: Mark Theunissen
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a new database with name 'bobdata'
|
||||
- mysql_db: name=bobdata state=present
|
||||
|
||||
# Copy database dump file to remote host and restore it to database 'my_db'
|
||||
- copy: src=dump.sql.bz2 dest=/tmp
|
||||
- mysql_db: name=my_db state=import target=/tmp/dump.sql.bz2
|
||||
'''
|
||||
|
||||
import ConfigParser
|
||||
import os
|
||||
import pipes
|
||||
try:
|
||||
import MySQLdb
|
||||
except ImportError:
|
||||
mysqldb_found = False
|
||||
else:
|
||||
mysqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# MySQL module specific support methods.
|
||||
#
|
||||
|
||||
def db_exists(cursor, db):
|
||||
res = cursor.execute("SHOW DATABASES LIKE %s", (db.replace("_","\_"),))
|
||||
return bool(res)
|
||||
|
||||
def db_delete(cursor, db):
|
||||
query = "DROP DATABASE `%s`" % db
|
||||
cursor.execute(query)
|
||||
return True
|
||||
|
||||
def db_dump(module, host, user, password, db_name, target, port, socket=None):
|
||||
cmd = module.get_bin_path('mysqldump', True)
|
||||
cmd += " --quick --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password))
|
||||
if socket is not None:
|
||||
cmd += " --socket=%s" % pipes.quote(socket)
|
||||
else:
|
||||
cmd += " --host=%s --port=%s" % (pipes.quote(host), pipes.quote(port))
|
||||
cmd += " %s" % pipes.quote(db_name)
|
||||
if os.path.splitext(target)[-1] == '.gz':
|
||||
cmd = cmd + ' | gzip > ' + pipes.quote(target)
|
||||
elif os.path.splitext(target)[-1] == '.bz2':
|
||||
cmd = cmd + ' | bzip2 > ' + pipes.quote(target)
|
||||
else:
|
||||
cmd += " > %s" % pipes.quote(target)
|
||||
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
|
||||
return rc, stdout, stderr
|
||||
|
||||
def db_import(module, host, user, password, db_name, target, port, socket=None):
|
||||
if not os.path.exists(target):
|
||||
return module.fail_json(msg="target %s does not exist on the host" % target)
|
||||
|
||||
cmd = module.get_bin_path('mysql', True)
|
||||
cmd += " --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password))
|
||||
if socket is not None:
|
||||
cmd += " --socket=%s" % pipes.quote(socket)
|
||||
else:
|
||||
cmd += " --host=%s --port=%s" % (pipes.quote(host), pipes.quote(port))
|
||||
cmd += " -D %s" % pipes.quote(db_name)
|
||||
if os.path.splitext(target)[-1] == '.gz':
|
||||
gunzip_path = module.get_bin_path('gunzip')
|
||||
if gunzip_path:
|
||||
rc, stdout, stderr = module.run_command('%s %s' % (gunzip_path, target))
|
||||
if rc != 0:
|
||||
return rc, stdout, stderr
|
||||
cmd += " < %s" % pipes.quote(os.path.splitext(target)[0])
|
||||
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
|
||||
if rc != 0:
|
||||
return rc, stdout, stderr
|
||||
gzip_path = module.get_bin_path('gzip')
|
||||
if gzip_path:
|
||||
rc, stdout, stderr = module.run_command('%s %s' % (gzip_path, os.path.splitext(target)[0]))
|
||||
else:
|
||||
module.fail_json(msg="gzip command not found")
|
||||
else:
|
||||
module.fail_json(msg="gunzip command not found")
|
||||
elif os.path.splitext(target)[-1] == '.bz2':
|
||||
bunzip2_path = module.get_bin_path('bunzip2')
|
||||
if bunzip2_path:
|
||||
rc, stdout, stderr = module.run_command('%s %s' % (bunzip2_path, target))
|
||||
if rc != 0:
|
||||
return rc, stdout, stderr
|
||||
cmd += " < %s" % pipes.quote(os.path.splitext(target)[0])
|
||||
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
|
||||
if rc != 0:
|
||||
return rc, stdout, stderr
|
||||
bzip2_path = module.get_bin_path('bzip2')
|
||||
if bzip2_path:
|
||||
rc, stdout, stderr = module.run_command('%s %s' % (bzip2_path, os.path.splitext(target)[0]))
|
||||
else:
|
||||
module.fail_json(msg="bzip2 command not found")
|
||||
else:
|
||||
module.fail_json(msg="bunzip2 command not found")
|
||||
else:
|
||||
cmd += " < %s" % pipes.quote(target)
|
||||
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
|
||||
return rc, stdout, stderr
|
||||
|
||||
def db_create(cursor, db, encoding, collation):
|
||||
if encoding:
|
||||
encoding = " CHARACTER SET %s" % encoding
|
||||
if collation:
|
||||
collation = " COLLATE %s" % collation
|
||||
query = "CREATE DATABASE `%s`%s%s" % (db, encoding, collation)
|
||||
res = cursor.execute(query)
|
||||
return True
|
||||
|
||||
def strip_quotes(s):
|
||||
""" Remove surrounding single or double quotes
|
||||
|
||||
>>> print strip_quotes('hello')
|
||||
hello
|
||||
>>> print strip_quotes('"hello"')
|
||||
hello
|
||||
>>> print strip_quotes("'hello'")
|
||||
hello
|
||||
>>> print strip_quotes("'hello")
|
||||
'hello
|
||||
|
||||
"""
|
||||
single_quote = "'"
|
||||
double_quote = '"'
|
||||
|
||||
if s.startswith(single_quote) and s.endswith(single_quote):
|
||||
s = s.strip(single_quote)
|
||||
elif s.startswith(double_quote) and s.endswith(double_quote):
|
||||
s = s.strip(double_quote)
|
||||
return s
|
||||
|
||||
|
||||
def config_get(config, section, option):
|
||||
""" Calls ConfigParser.get and strips quotes
|
||||
|
||||
See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html
|
||||
"""
|
||||
return strip_quotes(config.get(section, option))
|
||||
|
||||
|
||||
def load_mycnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mycnf = os.path.expanduser('~/.my.cnf')
|
||||
if not os.path.exists(mycnf):
|
||||
return False
|
||||
try:
|
||||
config.readfp(open(mycnf))
|
||||
except (IOError):
|
||||
return False
|
||||
# We support two forms of passwords in .my.cnf, both pass= and password=,
|
||||
# as these are both supported by MySQL.
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'password')
|
||||
except (ConfigParser.NoOptionError):
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'pass')
|
||||
except (ConfigParser.NoOptionError):
|
||||
return False
|
||||
try:
|
||||
creds = dict(user=config_get(config, 'client', 'user'),passwd=passwd)
|
||||
except (ConfigParser.NoOptionError):
|
||||
return False
|
||||
return creds
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default="localhost"),
|
||||
login_port=dict(default="3306"),
|
||||
login_unix_socket=dict(default=None),
|
||||
name=dict(required=True, aliases=['db']),
|
||||
encoding=dict(default=""),
|
||||
collation=dict(default=""),
|
||||
target=dict(default=None),
|
||||
state=dict(default="present", choices=["absent", "present","dump", "import"]),
|
||||
)
|
||||
)
|
||||
|
||||
if not mysqldb_found:
|
||||
module.fail_json(msg="the python mysqldb module is required")
|
||||
|
||||
db = module.params["name"]
|
||||
encoding = module.params["encoding"]
|
||||
collation = module.params["collation"]
|
||||
state = module.params["state"]
|
||||
target = module.params["target"]
|
||||
|
||||
# make sure the target path is expanded for ~ and $HOME
|
||||
if target is not None:
|
||||
target = os.path.expandvars(os.path.expanduser(target))
|
||||
|
||||
# Either the caller passes both a username and password with which to connect to
|
||||
# mysql, or they pass neither and allow this module to read the credentials from
|
||||
# ~/.my.cnf.
|
||||
login_password = module.params["login_password"]
|
||||
login_user = module.params["login_user"]
|
||||
if login_user is None and login_password is None:
|
||||
mycnf_creds = load_mycnf()
|
||||
if mycnf_creds is False:
|
||||
login_user = "root"
|
||||
login_password = ""
|
||||
else:
|
||||
login_user = mycnf_creds["user"]
|
||||
login_password = mycnf_creds["passwd"]
|
||||
elif login_password is None or login_user is None:
|
||||
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
|
||||
login_host = module.params["login_host"]
|
||||
|
||||
if state in ['dump','import']:
|
||||
if target is None:
|
||||
module.fail_json(msg="with state=%s target is required" % (state))
|
||||
connect_to_db = db
|
||||
else:
|
||||
connect_to_db = 'mysql'
|
||||
try:
|
||||
if module.params["login_unix_socket"]:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db=connect_to_db)
|
||||
elif module.params["login_port"] != "3306" and module.params["login_host"] == "localhost":
|
||||
module.fail_json(msg="login_host is required when login_port is defined, login_host cannot be localhost when login_port is defined")
|
||||
else:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], port=int(module.params["login_port"]), user=login_user, passwd=login_password, db=connect_to_db)
|
||||
cursor = db_connection.cursor()
|
||||
except Exception, e:
|
||||
if "Unknown database" in str(e):
|
||||
errno, errstr = e.args
|
||||
module.fail_json(msg="ERROR: %s %s" % (errno, errstr))
|
||||
else:
|
||||
module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials")
|
||||
|
||||
changed = False
|
||||
if db_exists(cursor, db):
|
||||
if state == "absent":
|
||||
try:
|
||||
changed = db_delete(cursor, db)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="error deleting database: " + str(e))
|
||||
elif state == "dump":
|
||||
rc, stdout, stderr = db_dump(module, login_host, login_user,
|
||||
login_password, db, target,
|
||||
port=module.params['login_port'],
|
||||
socket=module.params['login_unix_socket'])
|
||||
if rc != 0:
|
||||
module.fail_json(msg="%s" % stderr)
|
||||
else:
|
||||
module.exit_json(changed=True, db=db, msg=stdout)
|
||||
elif state == "import":
|
||||
rc, stdout, stderr = db_import(module, login_host, login_user,
|
||||
login_password, db, target,
|
||||
port=module.params['login_port'],
|
||||
socket=module.params['login_unix_socket'])
|
||||
if rc != 0:
|
||||
module.fail_json(msg="%s" % stderr)
|
||||
else:
|
||||
module.exit_json(changed=True, db=db, msg=stdout)
|
||||
else:
|
||||
if state == "present":
|
||||
try:
|
||||
changed = db_create(cursor, db, encoding, collation)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="error creating database: " + str(e))
|
||||
|
||||
module.exit_json(changed=changed, db=db)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,369 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
Ansible module to manage mysql replication
|
||||
(c) 2013, Balazs Pocze <banyek@gawker.com>
|
||||
Certain parts are taken from Mark Theunissen's mysqldb module
|
||||
|
||||
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: mysql_replication
|
||||
|
||||
short_description: Manage MySQL replication
|
||||
description:
|
||||
- Manages MySQL server replication, slave, master status get and change master host.
|
||||
version_added: "1.3"
|
||||
options:
|
||||
mode:
|
||||
description:
|
||||
- module operating mode. Could be getslave (SHOW SLAVE STATUS), getmaster (SHOW MASTER STATUS), changemaster (CHANGE MASTER TO), startslave (START SLAVE), stopslave (STOP SLAVE)
|
||||
required: False
|
||||
choices:
|
||||
- getslave
|
||||
- getmaster
|
||||
- changemaster
|
||||
- stopslave
|
||||
- startslave
|
||||
default: getslave
|
||||
login_user:
|
||||
description:
|
||||
- username to connect mysql host, if defined login_password also needed.
|
||||
required: False
|
||||
login_password:
|
||||
description:
|
||||
- password to connect mysql host, if defined login_user also needed.
|
||||
required: False
|
||||
login_host:
|
||||
description:
|
||||
- mysql host to connect
|
||||
required: False
|
||||
login_unix_socket:
|
||||
description:
|
||||
- unix socket to connect mysql server
|
||||
master_host:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_user:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_password:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_port:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_connect_retry:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_log_file:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_log_pos:
|
||||
description:
|
||||
- same as mysql variable
|
||||
relay_log_file:
|
||||
description:
|
||||
- same as mysql variable
|
||||
relay_log_pos:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_ssl:
|
||||
description:
|
||||
- same as mysql variable
|
||||
possible values: 0,1
|
||||
master_ssl_ca:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_ssl_capath:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_ssl_cert:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_ssl_key:
|
||||
description:
|
||||
- same as mysql variable
|
||||
master_ssl_cipher:
|
||||
description:
|
||||
- same as mysql variable
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Stop mysql slave thread
|
||||
- mysql_replication: mode=stopslave
|
||||
|
||||
# Get master binlog file name and binlog position
|
||||
- mysql_replication: mode=getmaster
|
||||
|
||||
# Change master to master server 192.168.1.1 and use binary log 'mysql-bin.000009' with position 4578
|
||||
- mysql_replication: mode=changemaster master_host=192.168.1.1 master_log_file=mysql-bin.000009 master_log_pos=4578
|
||||
'''
|
||||
|
||||
import ConfigParser
|
||||
import os
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import MySQLdb
|
||||
except ImportError:
|
||||
mysqldb_found = False
|
||||
else:
|
||||
mysqldb_found = True
|
||||
|
||||
|
||||
def get_master_status(cursor):
|
||||
cursor.execute("SHOW MASTER STATUS")
|
||||
masterstatus = cursor.fetchone()
|
||||
return masterstatus
|
||||
|
||||
|
||||
def get_slave_status(cursor):
|
||||
cursor.execute("SHOW SLAVE STATUS")
|
||||
slavestatus = cursor.fetchone()
|
||||
return slavestatus
|
||||
|
||||
|
||||
def stop_slave(cursor):
|
||||
try:
|
||||
cursor.execute("STOP SLAVE")
|
||||
stopped = True
|
||||
except:
|
||||
stopped = False
|
||||
return stopped
|
||||
|
||||
|
||||
def start_slave(cursor):
|
||||
try:
|
||||
cursor.execute("START SLAVE")
|
||||
started = True
|
||||
except:
|
||||
started = False
|
||||
return started
|
||||
|
||||
|
||||
def changemaster(cursor, chm):
|
||||
SQLPARAM = ",".join(chm)
|
||||
cursor.execute("CHANGE MASTER TO " + SQLPARAM)
|
||||
|
||||
|
||||
def strip_quotes(s):
|
||||
""" Remove surrounding single or double quotes
|
||||
|
||||
>>> print strip_quotes('hello')
|
||||
hello
|
||||
>>> print strip_quotes('"hello"')
|
||||
hello
|
||||
>>> print strip_quotes("'hello'")
|
||||
hello
|
||||
>>> print strip_quotes("'hello")
|
||||
'hello
|
||||
|
||||
"""
|
||||
single_quote = "'"
|
||||
double_quote = '"'
|
||||
|
||||
if s.startswith(single_quote) and s.endswith(single_quote):
|
||||
s = s.strip(single_quote)
|
||||
elif s.startswith(double_quote) and s.endswith(double_quote):
|
||||
s = s.strip(double_quote)
|
||||
return s
|
||||
|
||||
|
||||
def config_get(config, section, option):
|
||||
""" Calls ConfigParser.get and strips quotes
|
||||
|
||||
See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html
|
||||
"""
|
||||
return strip_quotes(config.get(section, option))
|
||||
|
||||
|
||||
def load_mycnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mycnf = os.path.expanduser('~/.my.cnf')
|
||||
if not os.path.exists(mycnf):
|
||||
return False
|
||||
try:
|
||||
config.readfp(open(mycnf))
|
||||
except (IOError):
|
||||
return False
|
||||
# We support two forms of passwords in .my.cnf, both pass= and password=,
|
||||
# as these are both supported by MySQL.
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'password')
|
||||
except (ConfigParser.NoOptionError):
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'pass')
|
||||
except (ConfigParser.NoOptionError):
|
||||
return False
|
||||
|
||||
# If .my.cnf doesn't specify a user, default to user login name
|
||||
try:
|
||||
user = config_get(config, 'client', 'user')
|
||||
except (ConfigParser.NoOptionError):
|
||||
user = getpass.getuser()
|
||||
creds = dict(user=user, passwd=passwd)
|
||||
return creds
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default="localhost"),
|
||||
login_unix_socket=dict(default=None),
|
||||
mode=dict(default="getslave", choices=["getmaster", "getslave", "changemaster", "stopslave", "startslave"]),
|
||||
master_host=dict(default=None),
|
||||
master_user=dict(default=None),
|
||||
master_password=dict(default=None),
|
||||
master_port=dict(default=None),
|
||||
master_connect_retry=dict(default=None),
|
||||
master_log_file=dict(default=None),
|
||||
master_log_pos=dict(default=None),
|
||||
relay_log_file=dict(default=None),
|
||||
relay_log_pos=dict(default=None),
|
||||
master_ssl=dict(default=False, type='bool'),
|
||||
master_ssl_ca=dict(default=None),
|
||||
master_ssl_capath=dict(default=None),
|
||||
master_ssl_cert=dict(default=None),
|
||||
master_ssl_key=dict(default=None),
|
||||
master_ssl_cipher=dict(default=None),
|
||||
)
|
||||
)
|
||||
user = module.params["login_user"]
|
||||
password = module.params["login_password"]
|
||||
host = module.params["login_host"]
|
||||
mode = module.params["mode"]
|
||||
master_host = module.params["master_host"]
|
||||
master_user = module.params["master_user"]
|
||||
master_password = module.params["master_password"]
|
||||
master_port = module.params["master_port"]
|
||||
master_connect_retry = module.params["master_connect_retry"]
|
||||
master_log_file = module.params["master_log_file"]
|
||||
master_log_pos = module.params["master_log_pos"]
|
||||
relay_log_file = module.params["relay_log_file"]
|
||||
relay_log_pos = module.params["relay_log_pos"]
|
||||
master_ssl = module.params["master_ssl"]
|
||||
master_ssl_ca = module.params["master_ssl_ca"]
|
||||
master_ssl_capath = module.params["master_ssl_capath"]
|
||||
master_ssl_cert = module.params["master_ssl_cert"]
|
||||
master_ssl_key = module.params["master_ssl_key"]
|
||||
master_ssl_cipher = module.params["master_ssl_cipher"]
|
||||
|
||||
if not mysqldb_found:
|
||||
module.fail_json(msg="the python mysqldb module is required")
|
||||
else:
|
||||
warnings.filterwarnings('error', category=MySQLdb.Warning)
|
||||
|
||||
# Either the caller passes both a username and password with which to connect to
|
||||
# mysql, or they pass neither and allow this module to read the credentials from
|
||||
# ~/.my.cnf.
|
||||
login_password = module.params["login_password"]
|
||||
login_user = module.params["login_user"]
|
||||
if login_user is None and login_password is None:
|
||||
mycnf_creds = load_mycnf()
|
||||
if mycnf_creds is False:
|
||||
login_user = "root"
|
||||
login_password = ""
|
||||
else:
|
||||
login_user = mycnf_creds["user"]
|
||||
login_password = mycnf_creds["passwd"]
|
||||
elif login_password is None or login_user is None:
|
||||
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
|
||||
|
||||
try:
|
||||
if module.params["login_unix_socket"]:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql")
|
||||
else:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
|
||||
try:
|
||||
cursor = db_connection.cursor(cursorclass=MySQLdb.cursors.DictCursor)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Trouble getting DictCursor from db_connection: %s" % e)
|
||||
|
||||
if mode in "getmaster":
|
||||
masterstatus = get_master_status(cursor)
|
||||
try:
|
||||
module.exit_json( **masterstatus )
|
||||
except TypeError:
|
||||
module.fail_json(msg="Server is not configured as mysql master")
|
||||
|
||||
elif mode in "getslave":
|
||||
slavestatus = get_slave_status(cursor)
|
||||
try:
|
||||
module.exit_json( **slavestatus )
|
||||
except TypeError:
|
||||
module.fail_json(msg="Server is not configured as mysql slave")
|
||||
|
||||
elif mode in "changemaster":
|
||||
print "Change master"
|
||||
chm=[]
|
||||
if master_host:
|
||||
chm.append("MASTER_HOST='" + master_host + "'")
|
||||
if master_user:
|
||||
chm.append("MASTER_USER='" + master_user + "'")
|
||||
if master_password:
|
||||
chm.append("MASTER_PASSWORD='" + master_password + "'")
|
||||
if master_port:
|
||||
chm.append("MASTER_PORT=" + master_port)
|
||||
if master_connect_retry:
|
||||
chm.append("MASTER_CONNECT_RETRY='" + master_connect_retry + "'")
|
||||
if master_log_file:
|
||||
chm.append("MASTER_LOG_FILE='" + master_log_file + "'")
|
||||
if master_log_pos:
|
||||
chm.append("MASTER_LOG_POS=" + master_log_pos)
|
||||
if relay_log_file:
|
||||
chm.append("RELAY_LOG_FILE='" + relay_log_file + "'")
|
||||
if relay_log_pos:
|
||||
chm.append("RELAY_LOG_POS=" + relay_log_pos)
|
||||
if master_ssl:
|
||||
chm.append("MASTER_SSL=1")
|
||||
if master_ssl_ca:
|
||||
chm.append("MASTER_SSL_CA='" + master_ssl_ca + "'")
|
||||
if master_ssl_capath:
|
||||
chm.append("MASTER_SSL_CAPATH='" + master_ssl_capath + "'")
|
||||
if master_ssl_cert:
|
||||
chm.append("MASTER_SSL_CERT='" + master_ssl_cert + "'")
|
||||
if master_ssl_key:
|
||||
chm.append("MASTER_SSL_KEY='" + master_ssl_key + "'")
|
||||
if master_ssl_cipher:
|
||||
chm.append("MASTER_SSL_CIPHER='" + master_ssl_cipher + "'")
|
||||
changemaster(cursor,chm)
|
||||
module.exit_json(changed=True)
|
||||
elif mode in "startslave":
|
||||
started = start_slave(cursor)
|
||||
if started is True:
|
||||
module.exit_json(msg="Slave started ", changed=True)
|
||||
else:
|
||||
module.exit_json(msg="Slave already started (Or cannot be started)", changed=False)
|
||||
elif mode in "stopslave":
|
||||
stopped = stop_slave(cursor)
|
||||
if stopped is True:
|
||||
module.exit_json(msg="Slave stopped", changed=True)
|
||||
else:
|
||||
module.exit_json(msg="Slave already stopped", changed=False)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
||||
warnings.simplefilter("ignore")
|
@ -1,476 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
|
||||
# Sponsored by Four Kitchens http://fourkitchens.com.
|
||||
#
|
||||
# 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: mysql_user
|
||||
short_description: Adds or removes a user from a MySQL database.
|
||||
description:
|
||||
- Adds or removes a user from a MySQL database.
|
||||
version_added: "0.6"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of the user (role) to add or remove
|
||||
required: true
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- set the user's password
|
||||
required: false
|
||||
default: null
|
||||
host:
|
||||
description:
|
||||
- the 'host' part of the MySQL username
|
||||
required: false
|
||||
default: localhost
|
||||
login_user:
|
||||
description:
|
||||
- The username used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_password:
|
||||
description:
|
||||
- The password used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_host:
|
||||
description:
|
||||
- Host running the database
|
||||
required: false
|
||||
default: localhost
|
||||
login_port:
|
||||
description:
|
||||
- Port of the MySQL server
|
||||
required: false
|
||||
default: 3306
|
||||
version_added: '1.4'
|
||||
login_unix_socket:
|
||||
description:
|
||||
- The path to a Unix domain socket for local connections
|
||||
required: false
|
||||
default: null
|
||||
priv:
|
||||
description:
|
||||
- "MySQL privileges string in the format: C(db.table:priv1,priv2)"
|
||||
required: false
|
||||
default: null
|
||||
append_privs:
|
||||
description:
|
||||
- Append the privileges defined by priv to the existing ones for this
|
||||
user instead of overwriting existing ones.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
version_added: "1.4"
|
||||
state:
|
||||
description:
|
||||
- Whether the user should exist. When C(absent), removes
|
||||
the user.
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
check_implicit_admin:
|
||||
description:
|
||||
- Check if mysql allows login as root/nopassword before trying supplied credentials.
|
||||
required: false
|
||||
default: false
|
||||
version_added: "1.3"
|
||||
notes:
|
||||
- Requires the MySQLdb Python package on the remote host. For Ubuntu, this
|
||||
is as easy as apt-get install python-mysqldb.
|
||||
- Both C(login_password) and C(login_username) are required when you are
|
||||
passing credentials. If none are present, the module will attempt to read
|
||||
the credentials from C(~/.my.cnf), and finally fall back to using the MySQL
|
||||
default login of 'root' with no password.
|
||||
- "MySQL server installs with default login_user of 'root' and no password. To secure this user
|
||||
as part of an idempotent playbook, you must create at least two tasks: the first must change the root user's password,
|
||||
without providing any login_user/login_password details. The second must drop a ~/.my.cnf file containing
|
||||
the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from
|
||||
the file."
|
||||
|
||||
requirements: [ "ConfigParser", "MySQLdb" ]
|
||||
author: Mark Theunissen
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
# Create database user with name 'bob' and password '12345' with all database privileges
|
||||
- mysql_user: name=bob password=12345 priv=*.*:ALL state=present
|
||||
|
||||
# Creates database user 'bob' and password '12345' with all database privileges and 'WITH GRANT OPTION'
|
||||
- mysql_user: name=bob password=12345 priv=*.*:ALL,GRANT state=present
|
||||
|
||||
# Ensure no user named 'sally' exists, also passing in the auth credentials.
|
||||
- mysql_user: login_user=root login_password=123456 name=sally state=absent
|
||||
|
||||
# Specify grants composed of more than one word
|
||||
- mysql_user: name=replication password=12345 priv=*.*:"REPLICATION CLIENT" state=present
|
||||
|
||||
# Revoke all privileges for user 'bob' and password '12345'
|
||||
- mysql_user: name=bob password=12345 priv=*.*:USAGE state=present
|
||||
|
||||
# Example privileges string format
|
||||
mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL
|
||||
|
||||
# Example using login_unix_socket to connect to server
|
||||
- mysql_user: name=root password=abc123 login_unix_socket=/var/run/mysqld/mysqld.sock
|
||||
|
||||
# Example .my.cnf file for setting the root password
|
||||
# Note: don't use quotes around the password, because the mysql_user module
|
||||
# will include them in the password but the mysql client will not
|
||||
|
||||
[client]
|
||||
user=root
|
||||
password=n<_665{vS43y
|
||||
"""
|
||||
|
||||
import ConfigParser
|
||||
import getpass
|
||||
import tempfile
|
||||
try:
|
||||
import MySQLdb
|
||||
except ImportError:
|
||||
mysqldb_found = False
|
||||
else:
|
||||
mysqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# MySQL module specific support methods.
|
||||
#
|
||||
|
||||
def user_exists(cursor, user, host):
|
||||
cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host))
|
||||
count = cursor.fetchone()
|
||||
return count[0] > 0
|
||||
|
||||
def user_add(cursor, user, host, password, new_priv):
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password))
|
||||
if new_priv is not None:
|
||||
for db_table, priv in new_priv.iteritems():
|
||||
privileges_grant(cursor, user,host,db_table,priv)
|
||||
return True
|
||||
|
||||
def user_mod(cursor, user, host, password, new_priv, append_privs):
|
||||
changed = False
|
||||
grant_option = False
|
||||
|
||||
# Handle passwords.
|
||||
if password is not None:
|
||||
cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host))
|
||||
current_pass_hash = cursor.fetchone()
|
||||
cursor.execute("SELECT PASSWORD(%s)", (password,))
|
||||
new_pass_hash = cursor.fetchone()
|
||||
if current_pass_hash[0] != new_pass_hash[0]:
|
||||
cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user,host,password))
|
||||
changed = True
|
||||
|
||||
# Handle privileges.
|
||||
if new_priv is not None:
|
||||
curr_priv = privileges_get(cursor, user,host)
|
||||
|
||||
# If the user has privileges on a db.table that doesn't appear at all in
|
||||
# the new specification, then revoke all privileges on it.
|
||||
for db_table, priv in curr_priv.iteritems():
|
||||
# If the user has the GRANT OPTION on a db.table, revoke it first.
|
||||
if "GRANT" in priv:
|
||||
grant_option = True
|
||||
if db_table not in new_priv:
|
||||
if user != "root" and "PROXY" not in priv and not append_privs:
|
||||
privileges_revoke(cursor, user,host,db_table,grant_option)
|
||||
changed = True
|
||||
|
||||
# If the user doesn't currently have any privileges on a db.table, then
|
||||
# we can perform a straight grant operation.
|
||||
for db_table, priv in new_priv.iteritems():
|
||||
if db_table not in curr_priv:
|
||||
privileges_grant(cursor, user,host,db_table,priv)
|
||||
changed = True
|
||||
|
||||
# If the db.table specification exists in both the user's current privileges
|
||||
# and in the new privileges, then we need to see if there's a difference.
|
||||
db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys())
|
||||
for db_table in db_table_intersect:
|
||||
priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table])
|
||||
if (len(priv_diff) > 0):
|
||||
if not append_privs:
|
||||
privileges_revoke(cursor, user,host,db_table,grant_option)
|
||||
privileges_grant(cursor, user,host,db_table,new_priv[db_table])
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
def user_delete(cursor, user, host):
|
||||
cursor.execute("DROP USER %s@%s", (user,host))
|
||||
return True
|
||||
|
||||
def privileges_get(cursor, user,host):
|
||||
""" MySQL doesn't have a better method of getting privileges aside from the
|
||||
SHOW GRANTS query syntax, which requires us to then parse the returned string.
|
||||
Here's an example of the string that is returned from MySQL:
|
||||
|
||||
GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass';
|
||||
|
||||
This function makes the query and returns a dictionary containing the results.
|
||||
The dictionary format is the same as that returned by privileges_unpack() below.
|
||||
"""
|
||||
output = {}
|
||||
cursor.execute("SHOW GRANTS FOR %s@%s", (user,host))
|
||||
grants = cursor.fetchall()
|
||||
|
||||
def pick(x):
|
||||
if x == 'ALL PRIVILEGES':
|
||||
return 'ALL'
|
||||
else:
|
||||
return x
|
||||
|
||||
for grant in grants:
|
||||
res = re.match("GRANT (.+) ON (.+) TO '.+'@'.+'( IDENTIFIED BY PASSWORD '.+')? ?(.*)", grant[0])
|
||||
if res is None:
|
||||
module.fail_json(msg="unable to parse the MySQL grant string")
|
||||
privileges = res.group(1).split(", ")
|
||||
privileges = [ pick(x) for x in privileges]
|
||||
if "WITH GRANT OPTION" in res.group(4):
|
||||
privileges.append('GRANT')
|
||||
db = res.group(2)
|
||||
output[db] = privileges
|
||||
return output
|
||||
|
||||
def privileges_unpack(priv):
|
||||
""" Take a privileges string, typically passed as a parameter, and unserialize
|
||||
it into a dictionary, the same format as privileges_get() above. We have this
|
||||
custom format to avoid using YAML/JSON strings inside YAML playbooks. Example
|
||||
of a privileges string:
|
||||
|
||||
mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL
|
||||
|
||||
The privilege USAGE stands for no privileges, so we add that in on *.* if it's
|
||||
not specified in the string, as MySQL will always provide this by default.
|
||||
"""
|
||||
output = {}
|
||||
for item in priv.split('/'):
|
||||
pieces = item.split(':')
|
||||
if '.' in pieces[0]:
|
||||
pieces[0] = pieces[0].split('.')
|
||||
for idx, piece in enumerate(pieces):
|
||||
if pieces[0][idx] != "*":
|
||||
pieces[0][idx] = "`" + pieces[0][idx] + "`"
|
||||
pieces[0] = '.'.join(pieces[0])
|
||||
|
||||
output[pieces[0]] = pieces[1].upper().split(',')
|
||||
|
||||
if '*.*' not in output:
|
||||
output['*.*'] = ['USAGE']
|
||||
|
||||
return output
|
||||
|
||||
def privileges_revoke(cursor, user,host,db_table,grant_option):
|
||||
if grant_option:
|
||||
query = "REVOKE GRANT OPTION ON %s FROM '%s'@'%s'" % (db_table,user,host)
|
||||
cursor.execute(query)
|
||||
query = "REVOKE ALL PRIVILEGES ON %s FROM '%s'@'%s'" % (db_table,user,host)
|
||||
cursor.execute(query)
|
||||
|
||||
def privileges_grant(cursor, user,host,db_table,priv):
|
||||
|
||||
priv_string = ",".join(filter(lambda x: x != 'GRANT', priv))
|
||||
query = "GRANT %s ON %s TO '%s'@'%s'" % (priv_string,db_table,user,host)
|
||||
if 'GRANT' in priv:
|
||||
query = query + " WITH GRANT OPTION"
|
||||
cursor.execute(query)
|
||||
|
||||
|
||||
def strip_quotes(s):
|
||||
""" Remove surrounding single or double quotes
|
||||
|
||||
>>> print strip_quotes('hello')
|
||||
hello
|
||||
>>> print strip_quotes('"hello"')
|
||||
hello
|
||||
>>> print strip_quotes("'hello'")
|
||||
hello
|
||||
>>> print strip_quotes("'hello")
|
||||
'hello
|
||||
|
||||
"""
|
||||
single_quote = "'"
|
||||
double_quote = '"'
|
||||
|
||||
if s.startswith(single_quote) and s.endswith(single_quote):
|
||||
s = s.strip(single_quote)
|
||||
elif s.startswith(double_quote) and s.endswith(double_quote):
|
||||
s = s.strip(double_quote)
|
||||
return s
|
||||
|
||||
|
||||
def config_get(config, section, option):
|
||||
""" Calls ConfigParser.get and strips quotes
|
||||
|
||||
See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html
|
||||
"""
|
||||
return strip_quotes(config.get(section, option))
|
||||
|
||||
|
||||
def _safe_cnf_load(config, path):
|
||||
|
||||
data = {'user':'', 'password':''}
|
||||
|
||||
# read in user/pass
|
||||
f = open(path, 'r')
|
||||
for line in f.readlines():
|
||||
line = line.strip()
|
||||
if line.startswith('user='):
|
||||
data['user'] = line.split('=', 1)[1].strip()
|
||||
if line.startswith('password=') or line.startswith('pass='):
|
||||
data['password'] = line.split('=', 1)[1].strip()
|
||||
f.close()
|
||||
|
||||
# write out a new cnf file with only user/pass
|
||||
fh, newpath = tempfile.mkstemp(prefix=path + '.')
|
||||
f = open(newpath, 'wb')
|
||||
f.write('[client]\n')
|
||||
f.write('user=%s\n' % data['user'])
|
||||
f.write('password=%s\n' % data['password'])
|
||||
f.close()
|
||||
|
||||
config.readfp(open(newpath))
|
||||
os.remove(newpath)
|
||||
return config
|
||||
|
||||
def load_mycnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mycnf = os.path.expanduser('~/.my.cnf')
|
||||
if not os.path.exists(mycnf):
|
||||
return False
|
||||
try:
|
||||
config.readfp(open(mycnf))
|
||||
except (IOError):
|
||||
return False
|
||||
except:
|
||||
config = _safe_cnf_load(config, mycnf)
|
||||
|
||||
# We support two forms of passwords in .my.cnf, both pass= and password=,
|
||||
# as these are both supported by MySQL.
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'password')
|
||||
except (ConfigParser.NoOptionError):
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'pass')
|
||||
except (ConfigParser.NoOptionError):
|
||||
return False
|
||||
|
||||
# If .my.cnf doesn't specify a user, default to user login name
|
||||
try:
|
||||
user = config_get(config, 'client', 'user')
|
||||
except (ConfigParser.NoOptionError):
|
||||
user = getpass.getuser()
|
||||
creds = dict(user=user,passwd=passwd)
|
||||
return creds
|
||||
|
||||
def connect(module, login_user, login_password):
|
||||
if module.params["login_unix_socket"]:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql")
|
||||
else:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], port=int(module.params["login_port"]), user=login_user, passwd=login_password, db="mysql")
|
||||
return db_connection.cursor()
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default="localhost"),
|
||||
login_port=dict(default="3306"),
|
||||
login_unix_socket=dict(default=None),
|
||||
user=dict(required=True, aliases=['name']),
|
||||
password=dict(default=None),
|
||||
host=dict(default="localhost"),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
priv=dict(default=None),
|
||||
append_privs=dict(type="bool", default="no"),
|
||||
check_implicit_admin=dict(default=False),
|
||||
)
|
||||
)
|
||||
user = module.params["user"]
|
||||
password = module.params["password"]
|
||||
host = module.params["host"]
|
||||
state = module.params["state"]
|
||||
priv = module.params["priv"]
|
||||
check_implicit_admin = module.params['check_implicit_admin']
|
||||
append_privs = module.boolean(module.params["append_privs"])
|
||||
|
||||
if not mysqldb_found:
|
||||
module.fail_json(msg="the python mysqldb module is required")
|
||||
|
||||
if priv is not None:
|
||||
try:
|
||||
priv = privileges_unpack(priv)
|
||||
except:
|
||||
module.fail_json(msg="invalid privileges string")
|
||||
|
||||
# Either the caller passes both a username and password with which to connect to
|
||||
# mysql, or they pass neither and allow this module to read the credentials from
|
||||
# ~/.my.cnf.
|
||||
login_password = module.params["login_password"]
|
||||
login_user = module.params["login_user"]
|
||||
if login_user is None and login_password is None:
|
||||
mycnf_creds = load_mycnf()
|
||||
if mycnf_creds is False:
|
||||
login_user = "root"
|
||||
login_password = ""
|
||||
else:
|
||||
login_user = mycnf_creds["user"]
|
||||
login_password = mycnf_creds["passwd"]
|
||||
elif login_password is None or login_user is None:
|
||||
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
|
||||
|
||||
cursor = None
|
||||
try:
|
||||
if check_implicit_admin:
|
||||
try:
|
||||
cursor = connect(module, 'root', '')
|
||||
except:
|
||||
pass
|
||||
|
||||
if not cursor:
|
||||
cursor = connect(module, login_user, login_password)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
|
||||
|
||||
if state == "present":
|
||||
if user_exists(cursor, user, host):
|
||||
changed = user_mod(cursor, user, host, password, priv, append_privs)
|
||||
else:
|
||||
if password is None:
|
||||
module.fail_json(msg="password parameter required when adding a user")
|
||||
changed = user_add(cursor, user, host, password, priv)
|
||||
elif state == "absent":
|
||||
if user_exists(cursor, user, host):
|
||||
changed = user_delete(cursor, user, host)
|
||||
else:
|
||||
changed = False
|
||||
module.exit_json(changed=changed, user=user)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,253 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
Ansible module to manage mysql variables
|
||||
(c) 2013, Balazs Pocze <banyek@gawker.com>
|
||||
Certain parts are taken from Mark Theunissen's mysqldb module
|
||||
|
||||
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: mysql_variables
|
||||
|
||||
short_description: Manage MySQL global variables
|
||||
description:
|
||||
- Query / Set MySQL variables
|
||||
version_added: 1.3
|
||||
options:
|
||||
variable:
|
||||
description:
|
||||
- Variable name to operate
|
||||
required: True
|
||||
value:
|
||||
description:
|
||||
- If set, then sets variable value to this
|
||||
required: False
|
||||
login_user:
|
||||
description:
|
||||
- username to connect mysql host, if defined login_password also needed.
|
||||
required: False
|
||||
login_password:
|
||||
description:
|
||||
- password to connect mysql host, if defined login_user also needed.
|
||||
required: False
|
||||
login_host:
|
||||
description:
|
||||
- mysql host to connect
|
||||
required: False
|
||||
login_unix_socket:
|
||||
description:
|
||||
- unix socket to connect mysql server
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
# Check for sync_binlog setting
|
||||
- mysql_variables: variable=sync_binlog
|
||||
|
||||
# Set read_only variable to 1
|
||||
- mysql_variables: variable=read_only value=1
|
||||
'''
|
||||
|
||||
|
||||
import ConfigParser
|
||||
import os
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import MySQLdb
|
||||
except ImportError:
|
||||
mysqldb_found = False
|
||||
else:
|
||||
mysqldb_found = True
|
||||
|
||||
|
||||
def typedvalue(value):
|
||||
"""
|
||||
Convert value to number whenever possible, return same value
|
||||
otherwise.
|
||||
|
||||
>>> typedvalue('3')
|
||||
3
|
||||
>>> typedvalue('3.0')
|
||||
3.0
|
||||
>>> typedvalue('foobar')
|
||||
'foobar'
|
||||
|
||||
"""
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def getvariable(cursor, mysqlvar):
|
||||
cursor.execute("SHOW VARIABLES LIKE '" + mysqlvar + "'")
|
||||
mysqlvar_val = cursor.fetchall()
|
||||
return mysqlvar_val
|
||||
|
||||
|
||||
def setvariable(cursor, mysqlvar, value):
|
||||
""" Set a global mysql variable to a given value
|
||||
|
||||
The DB driver will handle quoting of the given value based on its
|
||||
type, thus numeric strings like '3.0' or '8' are illegal, they
|
||||
should be passed as numeric literals.
|
||||
|
||||
"""
|
||||
try:
|
||||
cursor.execute("SET GLOBAL " + mysqlvar + " = %s", (value,))
|
||||
cursor.fetchall()
|
||||
result = True
|
||||
except Exception, e:
|
||||
result = str(e)
|
||||
return result
|
||||
|
||||
|
||||
def strip_quotes(s):
|
||||
""" Remove surrounding single or double quotes
|
||||
|
||||
>>> print strip_quotes('hello')
|
||||
hello
|
||||
>>> print strip_quotes('"hello"')
|
||||
hello
|
||||
>>> print strip_quotes("'hello'")
|
||||
hello
|
||||
>>> print strip_quotes("'hello")
|
||||
'hello
|
||||
|
||||
"""
|
||||
single_quote = "'"
|
||||
double_quote = '"'
|
||||
|
||||
if s.startswith(single_quote) and s.endswith(single_quote):
|
||||
s = s.strip(single_quote)
|
||||
elif s.startswith(double_quote) and s.endswith(double_quote):
|
||||
s = s.strip(double_quote)
|
||||
return s
|
||||
|
||||
|
||||
def config_get(config, section, option):
|
||||
""" Calls ConfigParser.get and strips quotes
|
||||
|
||||
See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html
|
||||
"""
|
||||
return strip_quotes(config.get(section, option))
|
||||
|
||||
|
||||
def load_mycnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mycnf = os.path.expanduser('~/.my.cnf')
|
||||
if not os.path.exists(mycnf):
|
||||
return False
|
||||
try:
|
||||
config.readfp(open(mycnf))
|
||||
except (IOError):
|
||||
return False
|
||||
# We support two forms of passwords in .my.cnf, both pass= and password=,
|
||||
# as these are both supported by MySQL.
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'password')
|
||||
except (ConfigParser.NoOptionError):
|
||||
try:
|
||||
passwd = config_get(config, 'client', 'pass')
|
||||
except (ConfigParser.NoOptionError):
|
||||
return False
|
||||
|
||||
# If .my.cnf doesn't specify a user, default to user login name
|
||||
try:
|
||||
user = config_get(config, 'client', 'user')
|
||||
except (ConfigParser.NoOptionError):
|
||||
user = getpass.getuser()
|
||||
creds = dict(user=user, passwd=passwd)
|
||||
return creds
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default="localhost"),
|
||||
login_unix_socket=dict(default=None),
|
||||
variable=dict(default=None),
|
||||
value=dict(default=None)
|
||||
|
||||
)
|
||||
)
|
||||
user = module.params["login_user"]
|
||||
password = module.params["login_password"]
|
||||
host = module.params["login_host"]
|
||||
mysqlvar = module.params["variable"]
|
||||
value = module.params["value"]
|
||||
if not mysqldb_found:
|
||||
module.fail_json(msg="the python mysqldb module is required")
|
||||
else:
|
||||
warnings.filterwarnings('error', category=MySQLdb.Warning)
|
||||
|
||||
# Either the caller passes both a username and password with which to connect to
|
||||
# mysql, or they pass neither and allow this module to read the credentials from
|
||||
# ~/.my.cnf.
|
||||
login_password = module.params["login_password"]
|
||||
login_user = module.params["login_user"]
|
||||
if login_user is None and login_password is None:
|
||||
mycnf_creds = load_mycnf()
|
||||
if mycnf_creds is False:
|
||||
login_user = "root"
|
||||
login_password = ""
|
||||
else:
|
||||
login_user = mycnf_creds["user"]
|
||||
login_password = mycnf_creds["passwd"]
|
||||
elif login_password is None or login_user is None:
|
||||
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
|
||||
try:
|
||||
if module.params["login_unix_socket"]:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql")
|
||||
else:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
|
||||
cursor = db_connection.cursor()
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
|
||||
if mysqlvar is None:
|
||||
module.fail_json(msg="Cannot run without variable to operate with")
|
||||
mysqlvar_val = getvariable(cursor, mysqlvar)
|
||||
if value is None:
|
||||
module.exit_json(msg=mysqlvar_val)
|
||||
else:
|
||||
if len(mysqlvar_val) < 1:
|
||||
module.fail_json(msg="Variable not available", changed=False)
|
||||
# Type values before using them
|
||||
value_wanted = typedvalue(value)
|
||||
value_actual = typedvalue(mysqlvar_val[0][1])
|
||||
if value_wanted == value_actual:
|
||||
module.exit_json(msg="Variable already set to requested value", changed=False)
|
||||
result = setvariable(cursor, mysqlvar, value_wanted)
|
||||
if result is True:
|
||||
module.exit_json(msg="Variable change succeeded prev_value=%s" % value_actual, changed=True)
|
||||
else:
|
||||
module.fail_json(msg=result, changed=False)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,301 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: postgresql_db
|
||||
short_description: Add or remove PostgreSQL databases from a remote host.
|
||||
description:
|
||||
- Add or remove PostgreSQL databases from a remote host.
|
||||
version_added: "0.6"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of the database to add or remove
|
||||
required: true
|
||||
default: null
|
||||
login_user:
|
||||
description:
|
||||
- The username used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_password:
|
||||
description:
|
||||
- The password used to authenticate with
|
||||
required: false
|
||||
default: null
|
||||
login_host:
|
||||
description:
|
||||
- Host running the database
|
||||
required: false
|
||||
default: localhost
|
||||
owner:
|
||||
description:
|
||||
- Name of the role to set as owner of the database
|
||||
required: false
|
||||
default: null
|
||||
port:
|
||||
description:
|
||||
- Database port to connect to.
|
||||
required: false
|
||||
default: 5432
|
||||
template:
|
||||
description:
|
||||
- Template used to create the database
|
||||
required: false
|
||||
default: null
|
||||
encoding:
|
||||
description:
|
||||
- Encoding of the database
|
||||
required: false
|
||||
default: null
|
||||
encoding:
|
||||
description:
|
||||
- Encoding of the database
|
||||
required: false
|
||||
default: null
|
||||
lc_collate:
|
||||
description:
|
||||
- Collation order (LC_COLLATE) to use in the database. Must match collation order of template database unless C(template0) is used as template.
|
||||
required: false
|
||||
default: null
|
||||
lc_ctype:
|
||||
description:
|
||||
- Character classification (LC_CTYPE) to use in the database (e.g. lower, upper, ...) Must match LC_CTYPE of template database unless C(template0) is used as template.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- The database state
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
notes:
|
||||
- The default authentication assumes that you are either logging in as or sudo'ing to the C(postgres) account on the host.
|
||||
- This module uses I(psycopg2), a Python PostgreSQL database adapter. You must ensure that psycopg2 is installed on
|
||||
the host before using this module. If the remote host is the PostgreSQL server (which is the default case), then PostgreSQL must also be installed on the remote host. For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), and C(python-psycopg2) packages on the remote host before using this module.
|
||||
requirements: [ psycopg2 ]
|
||||
author: Lorin Hochstein
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a new database with name "acme"
|
||||
- postgresql_db: name=acme
|
||||
|
||||
# Create a new database with name "acme" and specific encoding and locale
|
||||
# settings. If a template different from "template0" is specified, encoding
|
||||
# and locale settings must match those of the template.
|
||||
- postgresql_db: name=acme
|
||||
encoding='UTF-8'
|
||||
lc_collate='de_DE.UTF-8'
|
||||
lc_ctype='de_DE.UTF-8'
|
||||
template='template0'
|
||||
'''
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
except ImportError:
|
||||
postgresqldb_found = False
|
||||
else:
|
||||
postgresqldb_found = True
|
||||
|
||||
class NotSupportedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# ===========================================
|
||||
# PostgreSQL module specific support methods.
|
||||
#
|
||||
|
||||
def set_owner(cursor, db, owner):
|
||||
query = "ALTER DATABASE \"%s\" OWNER TO \"%s\"" % (db, owner)
|
||||
cursor.execute(query)
|
||||
return True
|
||||
|
||||
def get_encoding_id(cursor, encoding):
|
||||
query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;"
|
||||
cursor.execute(query, {'encoding': encoding})
|
||||
return cursor.fetchone()['encoding_id']
|
||||
|
||||
def get_db_info(cursor, db):
|
||||
query = """
|
||||
SELECT rolname AS owner,
|
||||
pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id,
|
||||
datcollate AS lc_collate, datctype AS lc_ctype
|
||||
FROM pg_database JOIN pg_roles ON pg_roles.oid = pg_database.datdba
|
||||
WHERE datname = %(db)s
|
||||
"""
|
||||
cursor.execute(query, {'db':db})
|
||||
return cursor.fetchone()
|
||||
|
||||
def db_exists(cursor, db):
|
||||
query = "SELECT * FROM pg_database WHERE datname=%(db)s"
|
||||
cursor.execute(query, {'db': db})
|
||||
return cursor.rowcount == 1
|
||||
|
||||
def db_delete(cursor, db):
|
||||
if db_exists(cursor, db):
|
||||
query = "DROP DATABASE \"%s\"" % db
|
||||
cursor.execute(query)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
|
||||
if not db_exists(cursor, db):
|
||||
if owner:
|
||||
owner = " OWNER \"%s\"" % owner
|
||||
if template:
|
||||
template = " TEMPLATE \"%s\"" % template
|
||||
if encoding:
|
||||
encoding = " ENCODING '%s'" % encoding
|
||||
if lc_collate:
|
||||
lc_collate = " LC_COLLATE '%s'" % lc_collate
|
||||
if lc_ctype:
|
||||
lc_ctype = " LC_CTYPE '%s'" % lc_ctype
|
||||
query = 'CREATE DATABASE "%s"%s%s%s%s%s' % (db, owner,
|
||||
template, encoding,
|
||||
lc_collate, lc_ctype)
|
||||
cursor.execute(query)
|
||||
return True
|
||||
else:
|
||||
db_info = get_db_info(cursor, db)
|
||||
if (encoding and
|
||||
get_encoding_id(cursor, encoding) != db_info['encoding_id']):
|
||||
raise NotSupportedError(
|
||||
'Changing database encoding is not supported. '
|
||||
'Current encoding: %s' % db_info['encoding']
|
||||
)
|
||||
elif lc_collate and lc_collate != db_info['lc_collate']:
|
||||
raise NotSupportedError(
|
||||
'Changing LC_COLLATE is not supported. '
|
||||
'Current LC_COLLATE: %s' % db_info['lc_collate']
|
||||
)
|
||||
elif lc_ctype and lc_ctype != db_info['lc_ctype']:
|
||||
raise NotSupportedError(
|
||||
'Changing LC_CTYPE is not supported.'
|
||||
'Current LC_CTYPE: %s' % db_info['lc_ctype']
|
||||
)
|
||||
elif owner and owner != db_info['owner']:
|
||||
return set_owner(cursor, db, owner)
|
||||
else:
|
||||
return False
|
||||
|
||||
def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype):
|
||||
if not db_exists(cursor, db):
|
||||
return False
|
||||
else:
|
||||
db_info = get_db_info(cursor, db)
|
||||
if (encoding and
|
||||
get_encoding_id(cursor, encoding) != db_info['encoding_id']):
|
||||
return False
|
||||
elif lc_collate and lc_collate != db_info['lc_collate']:
|
||||
return False
|
||||
elif lc_ctype and lc_ctype != db_info['lc_ctype']:
|
||||
return False
|
||||
elif owner and owner != db_info['owner']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(default="postgres"),
|
||||
login_password=dict(default=""),
|
||||
login_host=dict(default=""),
|
||||
port=dict(default="5432"),
|
||||
db=dict(required=True, aliases=['name']),
|
||||
owner=dict(default=""),
|
||||
template=dict(default=""),
|
||||
encoding=dict(default=""),
|
||||
lc_collate=dict(default=""),
|
||||
lc_ctype=dict(default=""),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
if not postgresqldb_found:
|
||||
module.fail_json(msg="the python psycopg2 module is required")
|
||||
|
||||
db = module.params["db"]
|
||||
port = module.params["port"]
|
||||
owner = module.params["owner"]
|
||||
template = module.params["template"]
|
||||
encoding = module.params["encoding"]
|
||||
lc_collate = module.params["lc_collate"]
|
||||
lc_ctype = module.params["lc_ctype"]
|
||||
state = module.params["state"]
|
||||
changed = False
|
||||
|
||||
# To use defaults values, keyword arguments must be absent, so
|
||||
# check which values are empty and don't include in the **kw
|
||||
# dictionary
|
||||
params_map = {
|
||||
"login_host":"host",
|
||||
"login_user":"user",
|
||||
"login_password":"password",
|
||||
"port":"port"
|
||||
}
|
||||
kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems()
|
||||
if k in params_map and v != '' )
|
||||
try:
|
||||
db_connection = psycopg2.connect(database="template1", **kw)
|
||||
# Enable autocommit so we can create databases
|
||||
if psycopg2.__version__ >= '2.4.2':
|
||||
db_connection.autocommit = True
|
||||
else:
|
||||
db_connection.set_isolation_level(psycopg2
|
||||
.extensions
|
||||
.ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
cursor = db_connection.cursor(
|
||||
cursor_factory=psycopg2.extras.DictCursor)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
try:
|
||||
if module.check_mode:
|
||||
if state == "absent":
|
||||
changed = not db_exists(cursor, db)
|
||||
elif state == "present":
|
||||
changed = not db_matches(cursor, db, owner, template, encoding,
|
||||
lc_collate, lc_ctype)
|
||||
module.exit_json(changed=changed,db=db)
|
||||
|
||||
if state == "absent":
|
||||
changed = db_delete(cursor, db)
|
||||
|
||||
elif state == "present":
|
||||
changed = db_create(cursor, db, owner, template, encoding,
|
||||
lc_collate, lc_ctype)
|
||||
except NotSupportedError, e:
|
||||
module.fail_json(msg=str(e))
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Database query failed: %s" % e)
|
||||
|
||||
module.exit_json(changed=changed, db=db)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,613 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: postgresql_privs
|
||||
version_added: "1.2"
|
||||
short_description: Grant or revoke privileges on PostgreSQL database objects.
|
||||
description:
|
||||
- Grant or revoke privileges on PostgreSQL database objects.
|
||||
- This module is basically a wrapper around most of the functionality of
|
||||
PostgreSQL's GRANT and REVOKE statements with detection of changes
|
||||
(GRANT/REVOKE I(privs) ON I(type) I(objs) TO/FROM I(roles))
|
||||
options:
|
||||
database:
|
||||
description:
|
||||
- Name of database to connect to.
|
||||
- 'Alias: I(db)'
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- If C(present), the specified privileges are granted, if C(absent) they
|
||||
are revoked.
|
||||
required: no
|
||||
default: present
|
||||
choices: [present, absent]
|
||||
privs:
|
||||
description:
|
||||
- Comma separated list of privileges to grant/revoke.
|
||||
- 'Alias: I(priv)'
|
||||
required: no
|
||||
type:
|
||||
description:
|
||||
- Type of database object to set privileges on.
|
||||
required: no
|
||||
default: table
|
||||
choices: [table, sequence, function, database,
|
||||
schema, language, tablespace, group]
|
||||
objs:
|
||||
description:
|
||||
- Comma separated list of database objects to set privileges on.
|
||||
- If I(type) is C(table) or C(sequence), the special value
|
||||
C(ALL_IN_SCHEMA) can be provided instead to specify all database
|
||||
objects of type I(type) in the schema specified via I(schema). (This
|
||||
also works with PostgreSQL < 9.0.)
|
||||
- If I(type) is C(database), this parameter can be omitted, in which case
|
||||
privileges are set for the database specified via I(database).
|
||||
- 'If I(type) is I(function), colons (":") in object names will be
|
||||
replaced with commas (needed to specify function signatures, see
|
||||
examples)'
|
||||
- 'Alias: I(obj)'
|
||||
required: no
|
||||
schema:
|
||||
description:
|
||||
- Schema that contains the database objects specified via I(objs).
|
||||
- May only be provided if I(type) is C(table), C(sequence) or
|
||||
C(function). Defaults to C(public) in these cases.
|
||||
required: no
|
||||
roles:
|
||||
description:
|
||||
- Comma separated list of role (user/group) names to set permissions for.
|
||||
- The special value C(PUBLIC) can be provided instead to set permissions
|
||||
for the implicitly defined PUBLIC group.
|
||||
- 'Alias: I(role)'
|
||||
required: yes
|
||||
grant_option:
|
||||
description:
|
||||
- Whether C(role) may grant/revoke the specified privileges/group
|
||||
memberships to others.
|
||||
- Set to C(no) to revoke GRANT OPTION, leave unspecified to
|
||||
make no changes.
|
||||
- I(grant_option) only has an effect if I(state) is C(present).
|
||||
- 'Alias: I(admin_option)'
|
||||
required: no
|
||||
choices: ['yes', 'no']
|
||||
host:
|
||||
description:
|
||||
- Database host address. If unspecified, connect via Unix socket.
|
||||
- 'Alias: I(login_host)'
|
||||
default: null
|
||||
required: no
|
||||
port:
|
||||
description:
|
||||
- Database port to connect to.
|
||||
required: no
|
||||
default: 5432
|
||||
login:
|
||||
description:
|
||||
- The username to authenticate with.
|
||||
- 'Alias: I(login_user)'
|
||||
default: postgres
|
||||
password:
|
||||
description:
|
||||
- The password to authenticate with.
|
||||
- 'Alias: I(login_password))'
|
||||
default: null
|
||||
required: no
|
||||
notes:
|
||||
- Default authentication assumes that postgresql_privs is run by the
|
||||
C(postgres) user on the remote host. (Ansible's C(user) or C(sudo-user)).
|
||||
- This module requires Python package I(psycopg2) to be installed on the
|
||||
remote host. In the default case of the remote host also being the
|
||||
PostgreSQL server, PostgreSQL has to be installed there as well, obviously.
|
||||
For Debian/Ubuntu-based systems, install packages I(postgresql) and
|
||||
I(python-psycopg2).
|
||||
- Parameters that accept comma separated lists (I(privs), I(objs), I(roles))
|
||||
have singular alias names (I(priv), I(obj), I(role)).
|
||||
- To revoke only C(GRANT OPTION) for a specific object, set I(state) to
|
||||
C(present) and I(grant_option) to C(no) (see examples).
|
||||
- Note that when revoking privileges from a role R, this role may still have
|
||||
access via privileges granted to any role R is a member of including
|
||||
C(PUBLIC).
|
||||
- Note that when revoking privileges from a role R, you do so as the user
|
||||
specified via I(login). If R has been granted the same privileges by
|
||||
another user also, R can still access database objects via these privileges.
|
||||
- When revoking privileges, C(RESTRICT) is assumed (see PostgreSQL docs).
|
||||
requirements: [psycopg2]
|
||||
author: Bernhard Weitzhofer
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# On database "library":
|
||||
# GRANT SELECT, INSERT, UPDATE ON TABLE public.books, public.authors
|
||||
# TO librarian, reader WITH GRANT OPTION
|
||||
- postgresql_privs: >
|
||||
database=library
|
||||
state=present
|
||||
privs=SELECT,INSERT,UPDATE
|
||||
type=table
|
||||
objs=books,authors
|
||||
schema=public
|
||||
roles=librarian,reader
|
||||
grant_option=yes
|
||||
|
||||
# Same as above leveraging default values:
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
privs=SELECT,INSERT,UPDATE
|
||||
objs=books,authors
|
||||
roles=librarian,reader
|
||||
grant_option=yes
|
||||
|
||||
# REVOKE GRANT OPTION FOR INSERT ON TABLE books FROM reader
|
||||
# Note that role "reader" will be *granted* INSERT privilege itself if this
|
||||
# isn't already the case (since state=present).
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
state=present
|
||||
priv=INSERT
|
||||
obj=books
|
||||
role=reader
|
||||
grant_option=no
|
||||
|
||||
# REVOKE INSERT, UPDATE ON ALL TABLES IN SCHEMA public FROM reader
|
||||
# "public" is the default schema. This also works for PostgreSQL 8.x.
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
state=absent
|
||||
privs=INSERT,UPDATE
|
||||
objs=ALL_IN_SCHEMA
|
||||
role=reader
|
||||
|
||||
# GRANT ALL PRIVILEGES ON SCHEMA public, math TO librarian
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
privs=ALL
|
||||
type=schema
|
||||
objs=public,math
|
||||
role=librarian
|
||||
|
||||
# GRANT ALL PRIVILEGES ON FUNCTION math.add(int, int) TO librarian, reader
|
||||
# Note the separation of arguments with colons.
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
privs=ALL
|
||||
type=function
|
||||
obj=add(int:int)
|
||||
schema=math
|
||||
roles=librarian,reader
|
||||
|
||||
# GRANT librarian, reader TO alice, bob WITH ADMIN OPTION
|
||||
# Note that group role memberships apply cluster-wide and therefore are not
|
||||
# restricted to database "library" here.
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
type=group
|
||||
objs=librarian,reader
|
||||
roles=alice,bob
|
||||
admin_option=yes
|
||||
|
||||
# GRANT ALL PRIVILEGES ON DATABASE library TO librarian
|
||||
# Note that here "db=postgres" specifies the database to connect to, not the
|
||||
# database to grant privileges on (which is specified via the "objs" param)
|
||||
- postgresql_privs: >
|
||||
db=postgres
|
||||
privs=ALL
|
||||
type=database
|
||||
obj=library
|
||||
role=librarian
|
||||
|
||||
# GRANT ALL PRIVILEGES ON DATABASE library TO librarian
|
||||
# If objs is omitted for type "database", it defaults to the database
|
||||
# to which the connection is established
|
||||
- postgresql_privs: >
|
||||
db=library
|
||||
privs=ALL
|
||||
type=database
|
||||
role=librarian
|
||||
"""
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
import psycopg2.extensions
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# We don't have functools.partial in Python < 2.5
|
||||
def partial(f, *args, **kwargs):
|
||||
"""Partial function application"""
|
||||
def g(*g_args, **g_kwargs):
|
||||
new_kwargs = kwargs.copy()
|
||||
new_kwargs.update(g_kwargs)
|
||||
return f(*(args + g_args), **g_kwargs)
|
||||
g.f = f
|
||||
g.args = args
|
||||
g.kwargs = kwargs
|
||||
return g
|
||||
|
||||
|
||||
class Connection(object):
|
||||
"""Wrapper around a psycopg2 connection with some convenience methods"""
|
||||
|
||||
def __init__(self, params):
|
||||
self.database = params.database
|
||||
# To use defaults values, keyword arguments must be absent, so
|
||||
# check which values are empty and don't include in the **kw
|
||||
# dictionary
|
||||
params_map = {
|
||||
"host":"host",
|
||||
"login":"user",
|
||||
"password":"password",
|
||||
"port":"port",
|
||||
"database": "database",
|
||||
}
|
||||
kw = dict( (params_map[k], getattr(params, k)) for k in params_map
|
||||
if getattr(params, k) != '' )
|
||||
self.connection = psycopg2.connect(**kw)
|
||||
self.cursor = self.connection.cursor()
|
||||
|
||||
|
||||
def commit(self):
|
||||
self.connection.commit()
|
||||
|
||||
|
||||
def rollback(self):
|
||||
self.connection.rollback()
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
"""Connection encoding in Python-compatible form"""
|
||||
return psycopg2.extensions.encodings[self.connection.encoding]
|
||||
|
||||
|
||||
### Methods for querying database objects
|
||||
|
||||
# PostgreSQL < 9.0 doesn't support "ALL TABLES IN SCHEMA schema"-like
|
||||
# phrases in GRANT or REVOKE statements, therefore alternative methods are
|
||||
# provided here.
|
||||
|
||||
def schema_exists(self, schema):
|
||||
query = """SELECT count(*)
|
||||
FROM pg_catalog.pg_namespace WHERE nspname = %s"""
|
||||
self.cursor.execute(query, (schema,))
|
||||
return self.cursor.fetchone()[0] > 0
|
||||
|
||||
|
||||
def get_all_tables_in_schema(self, schema):
|
||||
if not self.schema_exists(schema):
|
||||
raise Error('Schema "%s" does not exist.' % schema)
|
||||
query = """SELECT relname
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE nspname = %s AND relkind = 'r'"""
|
||||
self.cursor.execute(query, (schema,))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_all_sequences_in_schema(self, schema):
|
||||
if not self.schema_exists(schema):
|
||||
raise Error('Schema "%s" does not exist.' % schema)
|
||||
query = """SELECT relname
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE nspname = %s AND relkind = 'S'"""
|
||||
self.cursor.execute(query, (schema,))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
|
||||
### Methods for getting access control lists and group membership info
|
||||
|
||||
# To determine whether anything has changed after granting/revoking
|
||||
# privileges, we compare the access control lists of the specified database
|
||||
# objects before and afterwards. Python's list/string comparison should
|
||||
# suffice for change detection, we should not actually have to parse ACLs.
|
||||
# The same should apply to group membership information.
|
||||
|
||||
def get_table_acls(self, schema, tables):
|
||||
query = """SELECT relacl
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE nspname = %s AND relkind = 'r' AND relname = ANY (%s)
|
||||
ORDER BY relname"""
|
||||
self.cursor.execute(query, (schema, tables))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_sequence_acls(self, schema, sequences):
|
||||
query = """SELECT relacl
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE nspname = %s AND relkind = 'S' AND relname = ANY (%s)
|
||||
ORDER BY relname"""
|
||||
self.cursor.execute(query, (schema, sequences))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_function_acls(self, schema, function_signatures):
|
||||
funcnames = [f.split('(', 1)[0] for f in function_signatures]
|
||||
query = """SELECT proacl
|
||||
FROM pg_catalog.pg_proc p
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE nspname = %s AND proname = ANY (%s)
|
||||
ORDER BY proname, proargtypes"""
|
||||
self.cursor.execute(query, (schema, funcnames))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_schema_acls(self, schemas):
|
||||
query = """SELECT nspacl FROM pg_catalog.pg_namespace
|
||||
WHERE nspname = ANY (%s) ORDER BY nspname"""
|
||||
self.cursor.execute(query, (schemas,))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_language_acls(self, languages):
|
||||
query = """SELECT lanacl FROM pg_catalog.pg_language
|
||||
WHERE lanname = ANY (%s) ORDER BY lanname"""
|
||||
self.cursor.execute(query, (languages,))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_tablespace_acls(self, tablespaces):
|
||||
query = """SELECT spcacl FROM pg_catalog.pg_tablespace
|
||||
WHERE spcname = ANY (%s) ORDER BY spcname"""
|
||||
self.cursor.execute(query, (tablespaces,))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_database_acls(self, databases):
|
||||
query = """SELECT datacl FROM pg_catalog.pg_database
|
||||
WHERE datname = ANY (%s) ORDER BY datname"""
|
||||
self.cursor.execute(query, (databases,))
|
||||
return [t[0] for t in self.cursor.fetchall()]
|
||||
|
||||
|
||||
def get_group_memberships(self, groups):
|
||||
query = """SELECT roleid, grantor, member, admin_option
|
||||
FROM pg_catalog.pg_auth_members am
|
||||
JOIN pg_catalog.pg_roles r ON r.oid = am.roleid
|
||||
WHERE r.rolname = ANY(%s)
|
||||
ORDER BY roleid, grantor, member"""
|
||||
self.cursor.execute(query, (groups,))
|
||||
return self.cursor.fetchall()
|
||||
|
||||
|
||||
### Manipulating privileges
|
||||
|
||||
def manipulate_privs(self, obj_type, privs, objs, roles,
|
||||
state, grant_option, schema_qualifier=None):
|
||||
"""Manipulate database object privileges.
|
||||
|
||||
:param obj_type: Type of database object to grant/revoke
|
||||
privileges for.
|
||||
:param privs: Either a list of privileges to grant/revoke
|
||||
or None if type is "group".
|
||||
:param objs: List of database objects to grant/revoke
|
||||
privileges for.
|
||||
:param roles: Either a list of role names or "PUBLIC"
|
||||
for the implicitly defined "PUBLIC" group
|
||||
:param state: "present" to grant privileges, "absent" to revoke.
|
||||
:param grant_option: Only for state "present": If True, set
|
||||
grant/admin option. If False, revoke it.
|
||||
If None, don't change grant option.
|
||||
:param schema_qualifier: Some object types ("TABLE", "SEQUENCE",
|
||||
"FUNCTION") must be qualified by schema.
|
||||
Ignored for other Types.
|
||||
"""
|
||||
# get_status: function to get current status
|
||||
if obj_type == 'table':
|
||||
get_status = partial(self.get_table_acls, schema_qualifier)
|
||||
elif obj_type == 'sequence':
|
||||
get_status = partial(self.get_sequence_acls, schema_qualifier)
|
||||
elif obj_type == 'function':
|
||||
get_status = partial(self.get_function_acls, schema_qualifier)
|
||||
elif obj_type == 'schema':
|
||||
get_status = self.get_schema_acls
|
||||
elif obj_type == 'language':
|
||||
get_status = self.get_language_acls
|
||||
elif obj_type == 'tablespace':
|
||||
get_status = self.get_tablespace_acls
|
||||
elif obj_type == 'database':
|
||||
get_status = self.get_database_acls
|
||||
elif obj_type == 'group':
|
||||
get_status = self.get_group_memberships
|
||||
else:
|
||||
raise Error('Unsupported database object type "%s".' % obj_type)
|
||||
|
||||
# Return False (nothing has changed) if there are no objs to work on.
|
||||
if not objs:
|
||||
return False
|
||||
|
||||
# obj_ids: quoted db object identifiers (sometimes schema-qualified)
|
||||
if obj_type == 'function':
|
||||
obj_ids = []
|
||||
for obj in objs:
|
||||
try:
|
||||
f, args = obj.split('(', 1)
|
||||
except:
|
||||
raise Error('Illegal function signature: "%s".' % obj)
|
||||
obj_ids.append('"%s"."%s"(%s' % (schema_qualifier, f, args))
|
||||
elif obj_type in ['table', 'sequence']:
|
||||
obj_ids = ['"%s"."%s"' % (schema_qualifier, o) for o in objs]
|
||||
else:
|
||||
obj_ids = ['"%s"' % o for o in objs]
|
||||
|
||||
# set_what: SQL-fragment specifying what to set for the target roless:
|
||||
# Either group membership or privileges on objects of a certain type.
|
||||
if obj_type == 'group':
|
||||
set_what = ','.join(obj_ids)
|
||||
else:
|
||||
set_what = '%s ON %s %s' % (','.join(privs), obj_type,
|
||||
','.join(obj_ids))
|
||||
|
||||
# for_whom: SQL-fragment specifying for whom to set the above
|
||||
if roles == 'PUBLIC':
|
||||
for_whom = 'PUBLIC'
|
||||
else:
|
||||
for_whom = ','.join(['"%s"' % r for r in roles])
|
||||
|
||||
status_before = get_status(objs)
|
||||
if state == 'present':
|
||||
if grant_option:
|
||||
if obj_type == 'group':
|
||||
query = 'GRANT %s TO %s WITH ADMIN OPTION'
|
||||
else:
|
||||
query = 'GRANT %s TO %s WITH GRANT OPTION'
|
||||
else:
|
||||
query = 'GRANT %s TO %s'
|
||||
self.cursor.execute(query % (set_what, for_whom))
|
||||
|
||||
# Only revoke GRANT/ADMIN OPTION if grant_option actually is False.
|
||||
if grant_option == False:
|
||||
if obj_type == 'group':
|
||||
query = 'REVOKE ADMIN OPTION FOR %s FROM %s'
|
||||
else:
|
||||
query = 'REVOKE GRANT OPTION FOR %s FROM %s'
|
||||
self.cursor.execute(query % (set_what, for_whom))
|
||||
else:
|
||||
query = 'REVOKE %s FROM %s'
|
||||
self.cursor.execute(query % (set_what, for_whom))
|
||||
status_after = get_status(objs)
|
||||
return status_before != status_after
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
database=dict(required=True, aliases=['db']),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
privs=dict(required=False, aliases=['priv']),
|
||||
type=dict(default='table',
|
||||
choices=['table',
|
||||
'sequence',
|
||||
'function',
|
||||
'database',
|
||||
'schema',
|
||||
'language',
|
||||
'tablespace',
|
||||
'group']),
|
||||
objs=dict(required=False, aliases=['obj']),
|
||||
schema=dict(required=False),
|
||||
roles=dict(required=True, aliases=['role']),
|
||||
grant_option=dict(required=False, type='bool',
|
||||
aliases=['admin_option']),
|
||||
host=dict(default='', aliases=['login_host']),
|
||||
port=dict(type='int', default=5432),
|
||||
login=dict(default='postgres', aliases=['login_user']),
|
||||
password=dict(default='', aliases=['login_password'])
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
# Create type object as namespace for module params
|
||||
p = type('Params', (), module.params)
|
||||
|
||||
# param "schema": default, allowed depends on param "type"
|
||||
if p.type in ['table', 'sequence', 'function']:
|
||||
p.schema = p.schema or 'public'
|
||||
elif p.schema:
|
||||
module.fail_json(msg='Argument "schema" is not allowed '
|
||||
'for type "%s".' % p.type)
|
||||
|
||||
# param "objs": default, required depends on param "type"
|
||||
if p.type == 'database':
|
||||
p.objs = p.objs or p.database
|
||||
elif not p.objs:
|
||||
module.fail_json(msg='Argument "objs" is required '
|
||||
'for type "%s".' % p.type)
|
||||
|
||||
# param "privs": allowed, required depends on param "type"
|
||||
if p.type == 'group':
|
||||
if p.privs:
|
||||
module.fail_json(msg='Argument "privs" is not allowed '
|
||||
'for type "group".')
|
||||
elif not p.privs:
|
||||
module.fail_json(msg='Argument "privs" is required '
|
||||
'for type "%s".' % p.type)
|
||||
|
||||
# Connect to Database
|
||||
if not psycopg2:
|
||||
module.fail_json(msg='Python module "psycopg2" must be installed.')
|
||||
try:
|
||||
conn = Connection(p)
|
||||
except psycopg2.Error, e:
|
||||
module.fail_json(msg='Could not connect to database: %s' % e)
|
||||
|
||||
try:
|
||||
# privs
|
||||
if p.privs:
|
||||
privs = p.privs.split(',')
|
||||
else:
|
||||
privs = None
|
||||
|
||||
# objs:
|
||||
if p.type == 'table' and p.objs == 'ALL_IN_SCHEMA':
|
||||
objs = conn.get_all_tables_in_schema(p.schema)
|
||||
elif p.type == 'sequence' and p.objs == 'ALL_IN_SCHEMA':
|
||||
objs = conn.get_all_sequences_in_schema(p.schema)
|
||||
else:
|
||||
objs = p.objs.split(',')
|
||||
|
||||
# function signatures are encoded using ':' to separate args
|
||||
if p.type == 'function':
|
||||
objs = [obj.replace(':', ',') for obj in objs]
|
||||
|
||||
# roles
|
||||
if p.roles == 'PUBLIC':
|
||||
roles = 'PUBLIC'
|
||||
else:
|
||||
roles = p.roles.split(',')
|
||||
|
||||
changed = conn.manipulate_privs(
|
||||
obj_type = p.type,
|
||||
privs = privs,
|
||||
objs = objs,
|
||||
roles = roles,
|
||||
state = p.state,
|
||||
grant_option = p.grant_option,
|
||||
schema_qualifier=p.schema
|
||||
)
|
||||
|
||||
except Error, e:
|
||||
conn.rollback()
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
except psycopg2.Error, e:
|
||||
conn.rollback()
|
||||
# psycopg2 errors come in connection encoding, reencode
|
||||
msg = e.message.decode(conn.encoding).encode(sys.getdefaultencoding(),
|
||||
'replace')
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
if module.check_mode:
|
||||
conn.rollback()
|
||||
else:
|
||||
conn.commit()
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,526 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: postgresql_user
|
||||
short_description: Adds or removes a users (roles) from a PostgreSQL database.
|
||||
description:
|
||||
- Add or remove PostgreSQL users (roles) from a remote host and, optionally,
|
||||
grant the users access to an existing database or tables.
|
||||
- The fundamental function of the module is to create, or delete, roles from
|
||||
a PostgreSQL cluster. Privilege assignment, or removal, is an optional
|
||||
step, which works on one database at a time. This allows for the module to
|
||||
be called several times in the same module to modify the permissions on
|
||||
different databases, or to grant permissions to already existing users.
|
||||
- A user cannot be removed until all the privileges have been stripped from
|
||||
the user. In such situation, if the module tries to remove the user it
|
||||
will fail. To avoid this from happening the fail_on_user option signals
|
||||
the module to try to remove the user, but if not possible keep going; the
|
||||
module will report if changes happened and separately if the user was
|
||||
removed or not.
|
||||
version_added: "0.6"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of the user (role) to add or remove
|
||||
required: true
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- set the user's password, before 1.4 this was required.
|
||||
- "When passing an encrypted password, the encrypted parameter must also be true, and it must be generated with the format C('str[\\"md5\\"] + md5[ password + username ]'), resulting in a total of 35 characters. An easy way to do this is: C(echo \\"md5`echo -n \\"verysecretpasswordJOE\\" | md5`\\")."
|
||||
required: false
|
||||
default: null
|
||||
db:
|
||||
description:
|
||||
- name of database where permissions will be granted
|
||||
required: false
|
||||
default: null
|
||||
fail_on_user:
|
||||
description:
|
||||
- if C(yes), fail when user can't be removed. Otherwise just log and continue
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: [ "yes", "no" ]
|
||||
port:
|
||||
description:
|
||||
- Database port to connect to.
|
||||
required: false
|
||||
default: 5432
|
||||
login_user:
|
||||
description:
|
||||
- User (role) used to authenticate with PostgreSQL
|
||||
required: false
|
||||
default: postgres
|
||||
login_password:
|
||||
description:
|
||||
- Password used to authenticate with PostgreSQL
|
||||
required: false
|
||||
default: null
|
||||
login_host:
|
||||
description:
|
||||
- Host running PostgreSQL.
|
||||
required: false
|
||||
default: localhost
|
||||
priv:
|
||||
description:
|
||||
- "PostgreSQL privileges string in the format: C(table:priv1,priv2)"
|
||||
required: false
|
||||
default: null
|
||||
role_attr_flags:
|
||||
description:
|
||||
- "PostgreSQL role attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER"
|
||||
required: false
|
||||
default: null
|
||||
choices: [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEUSER", "[NO]CREATEDB",
|
||||
"[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION" ]
|
||||
state:
|
||||
description:
|
||||
- The user (role) state
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
encrypted:
|
||||
description:
|
||||
- denotes if the password is already encrypted. boolean.
|
||||
required: false
|
||||
default: false
|
||||
version_added: '1.4'
|
||||
expires:
|
||||
description:
|
||||
- sets the user's password expiration.
|
||||
required: false
|
||||
default: null
|
||||
version_added: '1.4'
|
||||
notes:
|
||||
- The default authentication assumes that you are either logging in as or
|
||||
sudo'ing to the postgres account on the host.
|
||||
- This module uses psycopg2, a Python PostgreSQL database adapter. You must
|
||||
ensure that psycopg2 is installed on the host before using this module. If
|
||||
the remote host is the PostgreSQL server (which is the default case), then
|
||||
PostgreSQL must also be installed on the remote host. For Ubuntu-based
|
||||
systems, install the postgresql, libpq-dev, and python-psycopg2 packages
|
||||
on the remote host before using this module.
|
||||
- If you specify PUBLIC as the user, then the privilege changes will apply
|
||||
to all users. You may not specify password or role_attr_flags when the
|
||||
PUBLIC user is specified.
|
||||
requirements: [ psycopg2 ]
|
||||
author: Lorin Hochstein
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create django user and grant access to database and products table
|
||||
- postgresql_user: db=acme name=django password=ceec4eif7ya priv=CONNECT/products:ALL
|
||||
|
||||
# Create rails user, grant privilege to create other databases and demote rails from super user status
|
||||
- postgresql_user: name=rails password=secret role_attr_flags=CREATEDB,NOSUPERUSER
|
||||
|
||||
# Remove test user privileges from acme
|
||||
- postgresql_user: db=acme name=test priv=ALL/products:ALL state=absent fail_on_user=no
|
||||
|
||||
# Remove test user from test database and the cluster
|
||||
- postgresql_user: db=test name=test priv=ALL state=absent
|
||||
|
||||
# Example privileges string format
|
||||
INSERT,UPDATE/table:SELECT/anothertable:ALL
|
||||
|
||||
# Remove an existing user's password
|
||||
- postgresql_user: db=test user=test password=NULL
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
postgresqldb_found = False
|
||||
else:
|
||||
postgresqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# PostgreSQL module specific support methods.
|
||||
#
|
||||
|
||||
|
||||
def user_exists(cursor, user):
|
||||
# The PUBLIC user is a special case that is always there
|
||||
if user == 'PUBLIC':
|
||||
return True
|
||||
query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s"
|
||||
cursor.execute(query, {'user': user})
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def user_add(cursor, user, password, role_attr_flags, encrypted, expires):
|
||||
"""Create a new database user (role)."""
|
||||
query_password_data = dict()
|
||||
query = 'CREATE USER "%(user)s"' % { "user": user}
|
||||
if password is not None:
|
||||
query = query + " WITH %(crypt)s" % { "crypt": encrypted }
|
||||
query = query + " PASSWORD %(password)s"
|
||||
query_password_data.update(password=password)
|
||||
if expires is not None:
|
||||
query = query + " VALID UNTIL '%(expires)s'" % { "expires": expires }
|
||||
query = query + " " + role_attr_flags
|
||||
cursor.execute(query, query_password_data)
|
||||
return True
|
||||
|
||||
def user_alter(cursor, module, user, password, role_attr_flags, encrypted, expires):
|
||||
"""Change user password and/or attributes. Return True if changed, False otherwise."""
|
||||
changed = False
|
||||
|
||||
if user == 'PUBLIC':
|
||||
if password is not None:
|
||||
module.fail_json(msg="cannot change the password for PUBLIC user")
|
||||
elif role_attr_flags != '':
|
||||
module.fail_json(msg="cannot change the role_attr_flags for PUBLIC user")
|
||||
else:
|
||||
return False
|
||||
|
||||
# Handle passwords.
|
||||
if password is not None or role_attr_flags is not None:
|
||||
# Select password and all flag-like columns in order to verify changes.
|
||||
query_password_data = dict()
|
||||
select = "SELECT * FROM pg_authid where rolname=%(user)s"
|
||||
cursor.execute(select, {"user": user})
|
||||
# Grab current role attributes.
|
||||
current_role_attrs = cursor.fetchone()
|
||||
|
||||
alter = 'ALTER USER "%(user)s"' % {"user": user}
|
||||
if password is not None:
|
||||
query_password_data.update(password=password)
|
||||
alter = alter + " WITH %(crypt)s" % {"crypt": encrypted}
|
||||
alter = alter + " PASSWORD %(password)s"
|
||||
alter = alter + " %(flags)s" % {'flags': role_attr_flags}
|
||||
elif role_attr_flags:
|
||||
alter = alter + ' WITH ' + role_attr_flags
|
||||
if expires is not None:
|
||||
alter = alter + " VALID UNTIL '%(expires)s'" % { "exipres": expires }
|
||||
|
||||
try:
|
||||
cursor.execute(alter, query_password_data)
|
||||
except psycopg2.InternalError, e:
|
||||
if e.pgcode == '25006':
|
||||
# Handle errors due to read-only transactions indicated by pgcode 25006
|
||||
# ERROR: cannot execute ALTER ROLE in a read-only transaction
|
||||
changed = False
|
||||
module.fail_json(msg=e.pgerror)
|
||||
return changed
|
||||
else:
|
||||
raise psycopg2.InternalError, e
|
||||
|
||||
# Grab new role attributes.
|
||||
cursor.execute(select, {"user": user})
|
||||
new_role_attrs = cursor.fetchone()
|
||||
|
||||
# Detect any differences between current_ and new_role_attrs.
|
||||
for i in range(len(current_role_attrs)):
|
||||
if current_role_attrs[i] != new_role_attrs[i]:
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
def user_delete(cursor, user):
|
||||
"""Try to remove a user. Returns True if successful otherwise False"""
|
||||
cursor.execute("SAVEPOINT ansible_pgsql_user_delete")
|
||||
try:
|
||||
cursor.execute("DROP USER \"%s\"" % user)
|
||||
except:
|
||||
cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete")
|
||||
cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
|
||||
return False
|
||||
|
||||
cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
|
||||
return True
|
||||
|
||||
def has_table_privilege(cursor, user, table, priv):
|
||||
query = 'SELECT has_table_privilege(%s, %s, %s)'
|
||||
cursor.execute(query, (user, table, priv))
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def get_table_privileges(cursor, user, table):
|
||||
if '.' in table:
|
||||
schema, table = table.split('.', 1)
|
||||
else:
|
||||
schema = 'public'
|
||||
query = '''SELECT privilege_type FROM information_schema.role_table_grants
|
||||
WHERE grantee=%s AND table_name=%s AND table_schema=%s'''
|
||||
cursor.execute(query, (user, table, schema))
|
||||
return set([x[0] for x in cursor.fetchall()])
|
||||
|
||||
|
||||
def quote_pg_identifier(identifier):
|
||||
"""
|
||||
quote postgresql identifiers involving zero or more namespaces
|
||||
"""
|
||||
|
||||
if '"' in identifier:
|
||||
# the user has supplied their own quoting. we have to hope they're
|
||||
# doing it right. Maybe they have an unfortunately named table
|
||||
# containing a period in the name, such as: "public"."users.2013"
|
||||
return identifier
|
||||
|
||||
tokens = identifier.strip().split(".")
|
||||
quoted_tokens = []
|
||||
for token in tokens:
|
||||
quoted_tokens.append('"%s"' % (token, ))
|
||||
return ".".join(quoted_tokens)
|
||||
|
||||
def grant_table_privilege(cursor, user, table, priv):
|
||||
prev_priv = get_table_privileges(cursor, user, table)
|
||||
query = 'GRANT %s ON TABLE %s TO %s' % (
|
||||
priv, quote_pg_identifier(table), quote_pg_identifier(user), )
|
||||
cursor.execute(query)
|
||||
curr_priv = get_table_privileges(cursor, user, table)
|
||||
return len(curr_priv) > len(prev_priv)
|
||||
|
||||
def revoke_table_privilege(cursor, user, table, priv):
|
||||
prev_priv = get_table_privileges(cursor, user, table)
|
||||
query = 'REVOKE %s ON TABLE %s FROM %s' % (
|
||||
priv, quote_pg_identifier(table), quote_pg_identifier(user), )
|
||||
cursor.execute(query)
|
||||
curr_priv = get_table_privileges(cursor, user, table)
|
||||
return len(curr_priv) < len(prev_priv)
|
||||
|
||||
|
||||
def get_database_privileges(cursor, user, db):
|
||||
priv_map = {
|
||||
'C':'CREATE',
|
||||
'T':'TEMPORARY',
|
||||
'c':'CONNECT',
|
||||
}
|
||||
query = 'SELECT datacl FROM pg_database WHERE datname = %s'
|
||||
cursor.execute(query, (db,))
|
||||
datacl = cursor.fetchone()[0]
|
||||
if datacl is None:
|
||||
return []
|
||||
r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl)
|
||||
if r is None:
|
||||
return []
|
||||
o = []
|
||||
for v in r.group(1):
|
||||
o.append(priv_map[v])
|
||||
return o
|
||||
|
||||
def has_database_privilege(cursor, user, db, priv):
|
||||
query = 'SELECT has_database_privilege(%s, %s, %s)'
|
||||
cursor.execute(query, (user, db, priv))
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def grant_database_privilege(cursor, user, db, priv):
|
||||
prev_priv = get_database_privileges(cursor, user, db)
|
||||
if user == "PUBLIC":
|
||||
query = 'GRANT %s ON DATABASE \"%s\" TO PUBLIC' % (priv, db)
|
||||
else:
|
||||
query = 'GRANT %s ON DATABASE \"%s\" TO \"%s\"' % (priv, db, user)
|
||||
cursor.execute(query)
|
||||
curr_priv = get_database_privileges(cursor, user, db)
|
||||
return len(curr_priv) > len(prev_priv)
|
||||
|
||||
def revoke_database_privilege(cursor, user, db, priv):
|
||||
prev_priv = get_database_privileges(cursor, user, db)
|
||||
if user == "PUBLIC":
|
||||
query = 'REVOKE %s ON DATABASE \"%s\" FROM PUBLIC' % (priv, db)
|
||||
else:
|
||||
query = 'REVOKE %s ON DATABASE \"%s\" FROM \"%s\"' % (priv, db, user)
|
||||
cursor.execute(query)
|
||||
curr_priv = get_database_privileges(cursor, user, db)
|
||||
return len(curr_priv) < len(prev_priv)
|
||||
|
||||
def revoke_privileges(cursor, user, privs):
|
||||
if privs is None:
|
||||
return False
|
||||
|
||||
changed = False
|
||||
for type_ in privs:
|
||||
revoke_func = {
|
||||
'table':revoke_table_privilege,
|
||||
'database':revoke_database_privilege
|
||||
}[type_]
|
||||
for name, privileges in privs[type_].iteritems():
|
||||
for privilege in privileges:
|
||||
changed = revoke_func(cursor, user, name, privilege)\
|
||||
or changed
|
||||
|
||||
return changed
|
||||
|
||||
def grant_privileges(cursor, user, privs):
|
||||
if privs is None:
|
||||
return False
|
||||
|
||||
changed = False
|
||||
for type_ in privs:
|
||||
grant_func = {
|
||||
'table':grant_table_privilege,
|
||||
'database':grant_database_privilege
|
||||
}[type_]
|
||||
for name, privileges in privs[type_].iteritems():
|
||||
for privilege in privileges:
|
||||
changed = grant_func(cursor, user, name, privilege)\
|
||||
or changed
|
||||
|
||||
return changed
|
||||
|
||||
def parse_role_attrs(role_attr_flags):
|
||||
"""
|
||||
Parse role attributes string for user creation.
|
||||
Format:
|
||||
|
||||
attributes[,attributes,...]
|
||||
|
||||
Where:
|
||||
|
||||
attributes := CREATEDB,CREATEROLE,NOSUPERUSER,...
|
||||
"""
|
||||
if ',' not in role_attr_flags:
|
||||
return role_attr_flags
|
||||
flag_set = role_attr_flags.split(",")
|
||||
o_flags = " ".join(flag_set)
|
||||
return o_flags
|
||||
|
||||
def parse_privs(privs, db):
|
||||
"""
|
||||
Parse privilege string to determine permissions for database db.
|
||||
Format:
|
||||
|
||||
privileges[/privileges/...]
|
||||
|
||||
Where:
|
||||
|
||||
privileges := DATABASE_PRIVILEGES[,DATABASE_PRIVILEGES,...] |
|
||||
TABLE_NAME:TABLE_PRIVILEGES[,TABLE_PRIVILEGES,...]
|
||||
"""
|
||||
if privs is None:
|
||||
return privs
|
||||
|
||||
o_privs = {
|
||||
'database':{},
|
||||
'table':{}
|
||||
}
|
||||
for token in privs.split('/'):
|
||||
if ':' not in token:
|
||||
type_ = 'database'
|
||||
name = db
|
||||
priv_set = set(x.strip() for x in token.split(','))
|
||||
else:
|
||||
type_ = 'table'
|
||||
name, privileges = token.split(':', 1)
|
||||
priv_set = set(x.strip() for x in privileges.split(','))
|
||||
|
||||
o_privs[type_][name] = priv_set
|
||||
|
||||
return o_privs
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(default="postgres"),
|
||||
login_password=dict(default=""),
|
||||
login_host=dict(default=""),
|
||||
user=dict(required=True, aliases=['name']),
|
||||
password=dict(default=None),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
priv=dict(default=None),
|
||||
db=dict(default=''),
|
||||
port=dict(default='5432'),
|
||||
fail_on_user=dict(type='bool', default='yes'),
|
||||
role_attr_flags=dict(default=''),
|
||||
encrypted=dict(type='bool', default='no'),
|
||||
expires=dict(default=None)
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
user = module.params["user"]
|
||||
password = module.params["password"]
|
||||
state = module.params["state"]
|
||||
fail_on_user = module.params["fail_on_user"]
|
||||
db = module.params["db"]
|
||||
if db == '' and module.params["priv"] is not None:
|
||||
module.fail_json(msg="privileges require a database to be specified")
|
||||
privs = parse_privs(module.params["priv"], db)
|
||||
port = module.params["port"]
|
||||
role_attr_flags = parse_role_attrs(module.params["role_attr_flags"])
|
||||
if module.params["encrypted"]:
|
||||
encrypted = "ENCRYPTED"
|
||||
else:
|
||||
encrypted = "UNENCRYPTED"
|
||||
expires = module.params["expires"]
|
||||
|
||||
if not postgresqldb_found:
|
||||
module.fail_json(msg="the python psycopg2 module is required")
|
||||
|
||||
# To use defaults values, keyword arguments must be absent, so
|
||||
# check which values are empty and don't include in the **kw
|
||||
# dictionary
|
||||
params_map = {
|
||||
"login_host":"host",
|
||||
"login_user":"user",
|
||||
"login_password":"password",
|
||||
"port":"port",
|
||||
"db":"database"
|
||||
}
|
||||
kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems()
|
||||
if k in params_map and v != "" )
|
||||
try:
|
||||
db_connection = psycopg2.connect(**kw)
|
||||
cursor = db_connection.cursor()
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
kw = dict(user=user)
|
||||
changed = False
|
||||
user_removed = False
|
||||
|
||||
if state == "present":
|
||||
if user_exists(cursor, user):
|
||||
changed = user_alter(cursor, module, user, password, role_attr_flags, encrypted, expires)
|
||||
else:
|
||||
changed = user_add(cursor, user, password, role_attr_flags, encrypted, expires)
|
||||
changed = grant_privileges(cursor, user, privs) or changed
|
||||
else:
|
||||
if user_exists(cursor, user):
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
kw['user_removed'] = True
|
||||
else:
|
||||
changed = revoke_privileges(cursor, user, privs)
|
||||
user_removed = user_delete(cursor, user)
|
||||
changed = changed or user_removed
|
||||
if fail_on_user and not user_removed:
|
||||
msg = "unable to remove user"
|
||||
module.fail_json(msg=msg)
|
||||
kw['user_removed'] = user_removed
|
||||
|
||||
if changed:
|
||||
if module.check_mode:
|
||||
db_connection.rollback()
|
||||
else:
|
||||
db_connection.commit()
|
||||
|
||||
kw['changed'] = changed
|
||||
module.exit_json(**kw)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,329 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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: redis
|
||||
short_description: Various redis commands, slave and flush
|
||||
description:
|
||||
- Unified utility to interact with redis instances.
|
||||
'slave' sets a redis instance in slave or master mode.
|
||||
'flush' flushes all the instance or a specified db.
|
||||
'config' (new in 1.6), ensures a configuration setting on an instance.
|
||||
version_added: "1.3"
|
||||
options:
|
||||
command:
|
||||
description:
|
||||
- The selected redis command
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "slave", "flush", "config" ]
|
||||
login_password:
|
||||
description:
|
||||
- The password used to authenticate with (usually not used)
|
||||
required: false
|
||||
default: null
|
||||
login_host:
|
||||
description:
|
||||
- The host running the database
|
||||
required: false
|
||||
default: localhost
|
||||
login_port:
|
||||
description:
|
||||
- The port to connect to
|
||||
required: false
|
||||
default: 6379
|
||||
master_host:
|
||||
description:
|
||||
- The host of the master instance [slave command]
|
||||
required: false
|
||||
default: null
|
||||
master_port:
|
||||
description:
|
||||
- The port of the master instance [slave command]
|
||||
required: false
|
||||
default: null
|
||||
slave_mode:
|
||||
description:
|
||||
- the mode of the redis instance [slave command]
|
||||
required: false
|
||||
default: slave
|
||||
choices: [ "master", "slave" ]
|
||||
db:
|
||||
description:
|
||||
- The database to flush (used in db mode) [flush command]
|
||||
required: false
|
||||
default: null
|
||||
flush_mode:
|
||||
description:
|
||||
- Type of flush (all the dbs in a redis instance or a specific one)
|
||||
[flush command]
|
||||
required: false
|
||||
default: all
|
||||
choices: [ "all", "db" ]
|
||||
name:
|
||||
version_added: 1.6
|
||||
description:
|
||||
- A redis config key.
|
||||
required: false
|
||||
default: null
|
||||
value:
|
||||
version_added: 1.6
|
||||
description:
|
||||
- A redis config value.
|
||||
required: false
|
||||
default: null
|
||||
|
||||
|
||||
notes:
|
||||
- Requires the redis-py Python package on the remote host. You can
|
||||
install it with pip (pip install redis) or with a package manager.
|
||||
https://github.com/andymccurdy/redis-py
|
||||
- If the redis master instance we are making slave of is password protected
|
||||
this needs to be in the redis.conf in the masterauth variable
|
||||
|
||||
requirements: [ redis ]
|
||||
author: Xabier Larrakoetxea
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Set local redis instance to be slave of melee.island on port 6377
|
||||
- redis: command=slave master_host=melee.island master_port=6377
|
||||
|
||||
# Deactivate slave mode
|
||||
- redis: command=slave slave_mode=master
|
||||
|
||||
# Flush all the redis db
|
||||
- redis: command=flush flush_mode=all
|
||||
|
||||
# Flush only one db in a redis instance
|
||||
- redis: command=flush db=1 flush_mode=db
|
||||
|
||||
# Configure local redis to have 10000 max clients
|
||||
- redis: command=config name=maxclients value=10000
|
||||
|
||||
# Configure local redis to have lua time limit of 100 ms
|
||||
- redis: command=config name=lua-time-limit value=100
|
||||
'''
|
||||
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
redis_found = False
|
||||
else:
|
||||
redis_found = True
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Redis module specific support methods.
|
||||
#
|
||||
|
||||
def set_slave_mode(client, master_host, master_port):
|
||||
try:
|
||||
return client.slaveof(master_host, master_port)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def set_master_mode(client):
|
||||
try:
|
||||
return client.slaveof()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def flush(client, db=None):
|
||||
try:
|
||||
if type(db) != int:
|
||||
return client.flushall()
|
||||
else:
|
||||
# The passed client has been connected to the database already
|
||||
return client.flushdb()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
command=dict(default=None, choices=['slave', 'flush', 'config']),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default='localhost'),
|
||||
login_port=dict(default='6379'),
|
||||
master_host=dict(default=None),
|
||||
master_port=dict(default=None),
|
||||
slave_mode=dict(default='slave', choices=['master', 'slave']),
|
||||
db=dict(default=None),
|
||||
flush_mode=dict(default='all', choices=['all', 'db']),
|
||||
name=dict(default=None),
|
||||
value=dict(default=None)
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
if not redis_found:
|
||||
module.fail_json(msg="python redis module is required")
|
||||
|
||||
login_password = module.params['login_password']
|
||||
login_host = module.params['login_host']
|
||||
login_port = int(module.params['login_port'])
|
||||
command = module.params['command']
|
||||
|
||||
# Slave Command section -----------
|
||||
if command == "slave":
|
||||
master_host = module.params['master_host']
|
||||
master_port = module.params['master_port']
|
||||
try:
|
||||
master_port = int(module.params['master_port'])
|
||||
except Exception:
|
||||
pass
|
||||
mode = module.params['slave_mode']
|
||||
|
||||
#Check if we have all the data
|
||||
if mode == "slave": # Only need data if we want to be slave
|
||||
if not master_host:
|
||||
module.fail_json(
|
||||
msg='In slave mode master host must be provided')
|
||||
|
||||
if not master_port:
|
||||
module.fail_json(
|
||||
msg='In slave mode master port must be provided')
|
||||
|
||||
#Connect and check
|
||||
r = redis.StrictRedis(host=login_host,
|
||||
port=login_port,
|
||||
password=login_password)
|
||||
try:
|
||||
r.ping()
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
#Check if we are already in the mode that we want
|
||||
info = r.info()
|
||||
if mode == "master" and info["role"] == "master":
|
||||
module.exit_json(changed=False, mode=mode)
|
||||
|
||||
elif mode == "slave" and\
|
||||
info["role"] == "slave" and\
|
||||
info["master_host"] == master_host and\
|
||||
info["master_port"] == master_port:
|
||||
status = {
|
||||
'status': mode,
|
||||
'master_host': master_host,
|
||||
'master_port': master_port,
|
||||
}
|
||||
module.exit_json(changed=False, mode=status)
|
||||
else:
|
||||
# Do the stuff
|
||||
# (Check Check_mode before commands so the commands aren't evaluated
|
||||
# if not necessary)
|
||||
if mode == "slave":
|
||||
if module.check_mode or\
|
||||
set_slave_mode(r, master_host, master_port):
|
||||
info = r.info()
|
||||
status = {
|
||||
'status': mode,
|
||||
'master_host': master_host,
|
||||
'master_port': master_port,
|
||||
}
|
||||
module.exit_json(changed=True, mode=status)
|
||||
else:
|
||||
module.fail_json(msg='Unable to set slave mode')
|
||||
|
||||
else:
|
||||
if module.check_mode or set_master_mode(r):
|
||||
module.exit_json(changed=True, mode=mode)
|
||||
else:
|
||||
module.fail_json(msg='Unable to set master mode')
|
||||
|
||||
# flush Command section -----------
|
||||
elif command == "flush":
|
||||
try:
|
||||
db = int(module.params['db'])
|
||||
except Exception:
|
||||
db = 0
|
||||
mode = module.params['flush_mode']
|
||||
|
||||
#Check if we have all the data
|
||||
if mode == "db":
|
||||
if type(db) != int:
|
||||
module.fail_json(
|
||||
msg="In db mode the db number must be provided")
|
||||
|
||||
#Connect and check
|
||||
r = redis.StrictRedis(host=login_host,
|
||||
port=login_port,
|
||||
password=login_password,
|
||||
db=db)
|
||||
try:
|
||||
r.ping()
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
# Do the stuff
|
||||
# (Check Check_mode before commands so the commands aren't evaluated
|
||||
# if not necessary)
|
||||
if mode == "all":
|
||||
if module.check_mode or flush(r):
|
||||
module.exit_json(changed=True, flushed=True)
|
||||
else: # Flush never fails :)
|
||||
module.fail_json(msg="Unable to flush all databases")
|
||||
|
||||
else:
|
||||
if module.check_mode or flush(r, db):
|
||||
module.exit_json(changed=True, flushed=True, db=db)
|
||||
else: # Flush never fails :)
|
||||
module.fail_json(msg="Unable to flush '%d' database" % db)
|
||||
elif command == 'config':
|
||||
name = module.params['name']
|
||||
value = module.params['value']
|
||||
|
||||
r = redis.StrictRedis(host=login_host,
|
||||
port=login_port,
|
||||
password=login_password)
|
||||
|
||||
try:
|
||||
r.ping()
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
|
||||
try:
|
||||
old_value = r.config_get(name)[name]
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to read config: %s" % e)
|
||||
changed = old_value != value
|
||||
|
||||
if module.check_mode or not changed:
|
||||
module.exit_json(changed=changed, name=name, value=value)
|
||||
else:
|
||||
try:
|
||||
r.config_set(name, value)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="unable to write config: %s" % e)
|
||||
module.exit_json(changed=changed, name=name, value=value)
|
||||
else:
|
||||
module.fail_json(msg='A valid command must be provided')
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,255 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, James Martin <jmartin@basho.com>, Drew Kerrigan <dkerrigan@basho.com>
|
||||
#
|
||||
# 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: riak
|
||||
short_description: This module handles some common Riak operations
|
||||
description:
|
||||
- This module can be used to join nodes to a cluster, check
|
||||
the status of the cluster.
|
||||
version_added: "1.2"
|
||||
options:
|
||||
command:
|
||||
description:
|
||||
- The command you would like to perform against the cluster.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
choices: ['ping', 'kv_test', 'join', 'plan', 'commit']
|
||||
config_dir:
|
||||
description:
|
||||
- The path to the riak configuration directory
|
||||
required: false
|
||||
default: /etc/riak
|
||||
aliases: []
|
||||
http_conn:
|
||||
description:
|
||||
- The ip address and port that is listening for Riak HTTP queries
|
||||
required: false
|
||||
default: 127.0.0.1:8098
|
||||
aliases: []
|
||||
target_node:
|
||||
description:
|
||||
- The target node for certain operations (join, ping)
|
||||
required: false
|
||||
default: riak@127.0.0.1
|
||||
aliases: []
|
||||
wait_for_handoffs:
|
||||
description:
|
||||
- Number of seconds to wait for handoffs to complete.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
type: 'int'
|
||||
wait_for_ring:
|
||||
description:
|
||||
- Number of seconds to wait for all nodes to agree on the ring.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
type: 'int'
|
||||
wait_for_service:
|
||||
description:
|
||||
- Waits for a riak service to come online before continuing.
|
||||
required: false
|
||||
default: None
|
||||
aliases: []
|
||||
choices: ['kv']
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used
|
||||
on personally controlled sites using self-signed certificates.
|
||||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
version_added: 1.5.1
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Join's a Riak node to another node
|
||||
- riak: command=join target_node=riak@10.1.1.1
|
||||
|
||||
# Wait for handoffs to finish. Use with async and poll.
|
||||
- riak: wait_for_handoffs=yes
|
||||
|
||||
# Wait for riak_kv service to startup
|
||||
- riak: wait_for_service=kv
|
||||
'''
|
||||
|
||||
import urllib2
|
||||
import time
|
||||
import socket
|
||||
import sys
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
def ring_check(module, riak_admin_bin):
|
||||
cmd = '%s ringready' % riak_admin_bin
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0 and 'TRUE All nodes agree on the ring' in out:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
command=dict(required=False, default=None, choices=[
|
||||
'ping', 'kv_test', 'join', 'plan', 'commit']),
|
||||
config_dir=dict(default='/etc/riak'),
|
||||
http_conn=dict(required=False, default='127.0.0.1:8098'),
|
||||
target_node=dict(default='riak@127.0.0.1', required=False),
|
||||
wait_for_handoffs=dict(default=False, type='int'),
|
||||
wait_for_ring=dict(default=False, type='int'),
|
||||
wait_for_service=dict(
|
||||
required=False, default=None, choices=['kv']),
|
||||
validate_certs = dict(default='yes', type='bool'))
|
||||
)
|
||||
|
||||
|
||||
command = module.params.get('command')
|
||||
config_dir = module.params.get('config_dir')
|
||||
http_conn = module.params.get('http_conn')
|
||||
target_node = module.params.get('target_node')
|
||||
wait_for_handoffs = module.params.get('wait_for_handoffs')
|
||||
wait_for_ring = module.params.get('wait_for_ring')
|
||||
wait_for_service = module.params.get('wait_for_service')
|
||||
validate_certs = module.params.get('validate_certs')
|
||||
|
||||
|
||||
#make sure riak commands are on the path
|
||||
riak_bin = module.get_bin_path('riak')
|
||||
riak_admin_bin = module.get_bin_path('riak-admin')
|
||||
|
||||
timeout = time.time() + 120
|
||||
while True:
|
||||
if time.time() > timeout:
|
||||
module.fail_json(msg='Timeout, could not fetch Riak stats.')
|
||||
(response, info) = fetch_url(module, 'http://%s/stats' % (http_conn), force=True, timeout=5)
|
||||
if info['status'] == 200:
|
||||
stats_raw = response.read()
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
# here we attempt to load those stats,
|
||||
try:
|
||||
stats = json.loads(stats_raw)
|
||||
except:
|
||||
module.fail_json(msg='Could not parse Riak stats.')
|
||||
|
||||
node_name = stats['nodename']
|
||||
nodes = stats['ring_members']
|
||||
ring_size = stats['ring_creation_size']
|
||||
rc, out, err = module.run_command([riak_bin, 'version'] )
|
||||
version = out.strip()
|
||||
|
||||
result = dict(node_name=node_name,
|
||||
nodes=nodes,
|
||||
ring_size=ring_size,
|
||||
version=version)
|
||||
|
||||
if command == 'ping':
|
||||
cmd = '%s ping %s' % ( riak_bin, target_node )
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
result['ping'] = out
|
||||
else:
|
||||
module.fail_json(msg=out)
|
||||
|
||||
elif command == 'kv_test':
|
||||
cmd = '%s test' % riak_admin_bin
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
result['kv_test'] = out
|
||||
else:
|
||||
module.fail_json(msg=out)
|
||||
|
||||
elif command == 'join':
|
||||
if nodes.count(node_name) == 1 and len(nodes) > 1:
|
||||
result['join'] = 'Node is already in cluster or staged to be in cluster.'
|
||||
else:
|
||||
cmd = '%s cluster join %s' % (riak_admin_bin, target_node)
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
result['join'] = out
|
||||
result['changed'] = True
|
||||
else:
|
||||
module.fail_json(msg=out)
|
||||
|
||||
elif command == 'plan':
|
||||
cmd = '%s cluster plan' % riak_admin_bin
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
result['plan'] = out
|
||||
if 'Staged Changes' in out:
|
||||
result['changed'] = True
|
||||
else:
|
||||
module.fail_json(msg=out)
|
||||
|
||||
elif command == 'commit':
|
||||
cmd = '%s cluster commit' % riak_admin_bin
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
result['commit'] = out
|
||||
result['changed'] = True
|
||||
else:
|
||||
module.fail_json(msg=out)
|
||||
|
||||
# this could take a while, recommend to run in async mode
|
||||
if wait_for_handoffs:
|
||||
timeout = time.time() + wait_for_handoffs
|
||||
while True:
|
||||
cmd = '%s transfers' % riak_admin_bin
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if 'No transfers active' in out:
|
||||
result['handoffs'] = 'No transfers active.'
|
||||
break
|
||||
time.sleep(10)
|
||||
if time.time() > timeout:
|
||||
module.fail_json(msg='Timeout waiting for handoffs.')
|
||||
|
||||
if wait_for_service:
|
||||
cmd = [riak_admin_bin, 'wait_for_service', 'riak_%s' % wait_for_service, node_name ]
|
||||
rc, out, err = module.run_command(cmd)
|
||||
result['service'] = out
|
||||
|
||||
if wait_for_ring:
|
||||
timeout = time.time() + wait_for_ring
|
||||
while True:
|
||||
if ring_check(module, riak_admin_bin):
|
||||
break
|
||||
time.sleep(10)
|
||||
if time.time() > timeout:
|
||||
module.fail_json(msg='Timeout waiting for nodes to agree on ring.')
|
||||
|
||||
result['ring_ready'] = ring_check(module, riak_admin_bin)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.urls import *
|
||||
|
||||
main()
|
@ -1,295 +0,0 @@
|
||||
#!/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: acl
|
||||
version_added: "1.4"
|
||||
short_description: Sets and retrieves file ACL information.
|
||||
description:
|
||||
- Sets and retrieves file ACL information.
|
||||
options:
|
||||
name:
|
||||
required: true
|
||||
default: null
|
||||
description:
|
||||
- The full path of the file or object.
|
||||
aliases: ['path']
|
||||
|
||||
state:
|
||||
required: false
|
||||
default: query
|
||||
choices: [ 'query', 'present', 'absent' ]
|
||||
description:
|
||||
- defines whether the ACL should be present or not. The C(query) state gets the current acl without changing it, for use in 'register' operations.
|
||||
|
||||
follow:
|
||||
required: false
|
||||
default: yes
|
||||
choices: [ 'yes', 'no' ]
|
||||
description:
|
||||
- whether to follow symlinks on the path if a symlink is encountered.
|
||||
|
||||
default:
|
||||
version_added: "1.5"
|
||||
required: false
|
||||
default: no
|
||||
choices: [ 'yes', 'no' ]
|
||||
description:
|
||||
- if the target is a directory, setting this to yes will make it the default acl for entities created inside the directory. It causes an error if name is a file.
|
||||
|
||||
entity:
|
||||
version_added: "1.5"
|
||||
required: false
|
||||
description:
|
||||
- actual user or group that the ACL applies to when matching entity types user or group are selected.
|
||||
|
||||
etype:
|
||||
version_added: "1.5"
|
||||
required: false
|
||||
default: null
|
||||
choices: [ 'user', 'group', 'mask', 'other' ]
|
||||
description:
|
||||
- the entity type of the ACL to apply, see setfacl documentation for more info.
|
||||
|
||||
|
||||
permissions:
|
||||
version_added: "1.5"
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Permissions to apply/remove can be any combination of r, w and x (read, write and execute respectively)
|
||||
|
||||
entry:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- DEPRECATED. The acl to set or remove. This must always be quoted in the form of '<etype>:<qualifier>:<perms>'. The qualifier may be empty for some types, but the type and perms are always requried. '-' can be used as placeholder when you do not care about permissions. This is now superseded by entity, type and permissions fields.
|
||||
|
||||
author: Brian Coca
|
||||
notes:
|
||||
- The "acl" module requires that acls are enabled on the target filesystem and that the setfacl and getfacl binaries are installed.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Grant user Joe read access to a file
|
||||
- acl: name=/etc/foo.conf entity=joe etype=user permissions="r" state=present
|
||||
|
||||
# Removes the acl for Joe on a specific file
|
||||
- acl: name=/etc/foo.conf entity=joe etype=user state=absent
|
||||
|
||||
# Sets default acl for joe on foo.d
|
||||
- acl: name=/etc/foo.d entity=joe etype=user permissions=rw default=yes state=present
|
||||
|
||||
# Same as previous but using entry shorthand
|
||||
- acl: name=/etc/foo.d entry="default:user:joe:rw-" state=present
|
||||
|
||||
# Obtain the acl for a specific file
|
||||
- acl: name=/etc/foo.conf
|
||||
register: acl_info
|
||||
'''
|
||||
|
||||
def normalize_permissions(p):
|
||||
perms = ['-','-','-']
|
||||
for char in p:
|
||||
if char == 'r':
|
||||
perms[0] = 'r'
|
||||
if char == 'w':
|
||||
perms[1] = 'w'
|
||||
if char == 'x':
|
||||
perms[2] = 'x'
|
||||
return ''.join(perms)
|
||||
|
||||
def split_entry(entry):
|
||||
''' splits entry and ensures normalized return'''
|
||||
|
||||
a = entry.split(':')
|
||||
a.reverse()
|
||||
if len(a) == 3:
|
||||
a.append(False)
|
||||
try:
|
||||
p,e,t,d = a
|
||||
except ValueError, e:
|
||||
print "wtf?? %s => %s" % (entry,a)
|
||||
raise e
|
||||
|
||||
if d:
|
||||
d = True
|
||||
|
||||
if t.startswith("u"):
|
||||
t = "user"
|
||||
elif t.startswith("g"):
|
||||
t = "group"
|
||||
elif t.startswith("m"):
|
||||
t = "mask"
|
||||
elif t.startswith("o"):
|
||||
t = "other"
|
||||
else:
|
||||
t = None
|
||||
|
||||
p = normalize_permissions(p)
|
||||
|
||||
return [d,t,e,p]
|
||||
|
||||
def get_acls(module,path,follow):
|
||||
|
||||
cmd = [ module.get_bin_path('getfacl', True) ]
|
||||
if not follow:
|
||||
cmd.append('-h')
|
||||
# prevents absolute path warnings and removes headers
|
||||
cmd.append('--omit-header')
|
||||
cmd.append('--absolute-names')
|
||||
cmd.append(path)
|
||||
|
||||
return _run_acl(module,cmd)
|
||||
|
||||
def set_acl(module,path,entry,follow,default):
|
||||
|
||||
cmd = [ module.get_bin_path('setfacl', True) ]
|
||||
if not follow:
|
||||
cmd.append('-h')
|
||||
if default:
|
||||
cmd.append('-d')
|
||||
cmd.append('-m "%s"' % entry)
|
||||
cmd.append(path)
|
||||
|
||||
return _run_acl(module,cmd)
|
||||
|
||||
def rm_acl(module,path,entry,follow,default):
|
||||
|
||||
cmd = [ module.get_bin_path('setfacl', True) ]
|
||||
if not follow:
|
||||
cmd.append('-h')
|
||||
if default:
|
||||
cmd.append('-k')
|
||||
entry = entry[0:entry.rfind(':')]
|
||||
cmd.append('-x "%s"' % entry)
|
||||
cmd.append(path)
|
||||
|
||||
return _run_acl(module,cmd,False)
|
||||
|
||||
def _run_acl(module,cmd,check_rc=True):
|
||||
|
||||
try:
|
||||
(rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc)
|
||||
except Exception, e:
|
||||
module.fail_json(msg=e.strerror)
|
||||
|
||||
# trim last line as it is always empty
|
||||
ret = out.splitlines()
|
||||
return ret[0:len(ret)-1]
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True,aliases=['path'], type='str'),
|
||||
entry = dict(required=False, etype='str'),
|
||||
entity = dict(required=False, type='str', default=''),
|
||||
etype = dict(required=False, choices=['other', 'user', 'group', 'mask'], type='str'),
|
||||
permissions = dict(required=False, type='str'),
|
||||
state = dict(required=False, default='query', choices=[ 'query', 'present', 'absent' ], type='str'),
|
||||
follow = dict(required=False, type='bool', default=True),
|
||||
default= dict(required=False, type='bool', default=False),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
path = os.path.expanduser(module.params.get('name'))
|
||||
entry = module.params.get('entry')
|
||||
entity = module.params.get('entity')
|
||||
etype = module.params.get('etype')
|
||||
permissions = module.params.get('permissions')
|
||||
state = module.params.get('state')
|
||||
follow = module.params.get('follow')
|
||||
default = module.params.get('default')
|
||||
|
||||
if permissions:
|
||||
permissions = normalize_permissions(permissions)
|
||||
|
||||
if not os.path.exists(path):
|
||||
module.fail_json(msg="path not found or not accessible!")
|
||||
|
||||
if state in ['present','absent']:
|
||||
if not entry and not etype:
|
||||
module.fail_json(msg="%s requires either etype and permissions or just entry be set" % state)
|
||||
|
||||
if entry:
|
||||
if etype or entity or permissions:
|
||||
module.fail_json(msg="entry and another incompatible field (entity, etype or permissions) are also set")
|
||||
if entry.count(":") not in [2,3]:
|
||||
module.fail_json(msg="Invalid entry: '%s', it requires 3 or 4 sections divided by ':'" % entry)
|
||||
|
||||
default, etype, entity, permissions = split_entry(entry)
|
||||
|
||||
changed=False
|
||||
msg = ""
|
||||
currentacls = get_acls(module,path,follow)
|
||||
|
||||
if (state == 'present'):
|
||||
matched = False
|
||||
for oldentry in currentacls:
|
||||
if oldentry.count(":") == 0:
|
||||
continue
|
||||
old_default, old_type, old_entity, old_permissions = split_entry(oldentry)
|
||||
if old_default == default:
|
||||
if old_type == etype:
|
||||
if etype in ['user', 'group']:
|
||||
if old_entity == entity:
|
||||
matched = True
|
||||
if not old_permissions == permissions:
|
||||
changed = True
|
||||
break
|
||||
else:
|
||||
matched = True
|
||||
if not old_permissions == permissions:
|
||||
changed = True
|
||||
break
|
||||
if not matched:
|
||||
changed=True
|
||||
|
||||
if changed and not module.check_mode:
|
||||
set_acl(module,path,':'.join([etype, str(entity), permissions]),follow,default)
|
||||
msg="%s is present" % ':'.join([etype, str(entity), permissions])
|
||||
|
||||
elif state == 'absent':
|
||||
for oldentry in currentacls:
|
||||
if oldentry.count(":") == 0:
|
||||
continue
|
||||
old_default, old_type, old_entity, old_permissions = split_entry(oldentry)
|
||||
if old_default == default:
|
||||
if old_type == etype:
|
||||
if etype in ['user', 'group']:
|
||||
if old_entity == entity:
|
||||
changed=True
|
||||
break
|
||||
else:
|
||||
changed=True
|
||||
break
|
||||
if changed and not module.check_mode:
|
||||
rm_acl(module,path,':'.join([etype, entity, '---']),follow,default)
|
||||
msg="%s is absent" % ':'.join([etype, entity, '---'])
|
||||
else:
|
||||
msg="current acl"
|
||||
|
||||
if changed:
|
||||
currentacls = get_acls(module,path,follow)
|
||||
|
||||
module.exit_json(changed=changed, msg=msg, acl=currentacls)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
@ -1,200 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: assemble
|
||||
short_description: Assembles a configuration file from fragments
|
||||
description:
|
||||
- Assembles a configuration file from fragments. Often a particular
|
||||
program will take a single configuration file and does not support a
|
||||
C(conf.d) style structure where it is easy to build up the configuration
|
||||
from multiple sources. M(assemble) will take a directory of files that can be
|
||||
local or have already been transferred to the system, and concatenate them
|
||||
together to produce a destination file. Files are assembled in string sorting order.
|
||||
Puppet calls this idea I(fragments).
|
||||
version_added: "0.5"
|
||||
options:
|
||||
src:
|
||||
description:
|
||||
- An already existing directory full of source files.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
dest:
|
||||
description:
|
||||
- A file to create using the concatenation of all of the source files.
|
||||
required: true
|
||||
default: null
|
||||
backup:
|
||||
description:
|
||||
- Create a backup file (if C(yes)), including the timestamp information so
|
||||
you can get the original file back if you somehow clobbered it
|
||||
incorrectly.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
delimiter:
|
||||
description:
|
||||
- A delimiter to separate the file contents.
|
||||
version_added: "1.4"
|
||||
required: false
|
||||
default: null
|
||||
remote_src:
|
||||
description:
|
||||
- If False, it will search for src at originating/master machine, if True it will
|
||||
go to the remote/target machine for the src. Default is True.
|
||||
choices: [ "True", "False" ]
|
||||
required: false
|
||||
default: "True"
|
||||
version_added: "1.4"
|
||||
regexp:
|
||||
description:
|
||||
- Assemble files only if C(regex) matches the filename. If not set,
|
||||
all files are assembled. All "\\" (backslash) must be escaped as
|
||||
"\\\\" to comply yaml syntax. Uses Python regular expressions; see
|
||||
U(http://docs.python.org/2/library/re.html).
|
||||
required: false
|
||||
default: null
|
||||
author: Stephen Fromm
|
||||
extends_documentation_fragment: files
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example from Ansible Playbooks
|
||||
- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf
|
||||
|
||||
# When a delimiter is specified, it will be inserted in between each fragment
|
||||
- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf delimiter='### START FRAGMENT ###'
|
||||
'''
|
||||
|
||||
# ===========================================
|
||||
# Support method
|
||||
|
||||
def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None):
|
||||
''' assemble a file from a directory of fragments '''
|
||||
tmpfd, temp_path = tempfile.mkstemp()
|
||||
tmp = os.fdopen(tmpfd,'w')
|
||||
delimit_me = False
|
||||
add_newline = False
|
||||
|
||||
for f in sorted(os.listdir(src_path)):
|
||||
if compiled_regexp and not compiled_regexp.search(f):
|
||||
continue
|
||||
fragment = "%s/%s" % (src_path, f)
|
||||
if not os.path.isfile(fragment):
|
||||
continue
|
||||
fragment_content = file(fragment).read()
|
||||
|
||||
# always put a newline between fragments if the previous fragment didn't end with a newline.
|
||||
if add_newline:
|
||||
tmp.write('\n')
|
||||
|
||||
# delimiters should only appear between fragments
|
||||
if delimit_me:
|
||||
if delimiter:
|
||||
# un-escape anything like newlines
|
||||
delimiter = delimiter.decode('unicode-escape')
|
||||
tmp.write(delimiter)
|
||||
# always make sure there's a newline after the
|
||||
# delimiter, so lines don't run together
|
||||
if delimiter[-1] != '\n':
|
||||
tmp.write('\n')
|
||||
|
||||
tmp.write(fragment_content)
|
||||
delimit_me = True
|
||||
if fragment_content.endswith('\n'):
|
||||
add_newline = False
|
||||
else:
|
||||
add_newline = True
|
||||
|
||||
tmp.close()
|
||||
return temp_path
|
||||
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
# not checking because of daisy chain to file module
|
||||
argument_spec = dict(
|
||||
src = dict(required=True),
|
||||
delimiter = dict(required=False),
|
||||
dest = dict(required=True),
|
||||
backup=dict(default=False, type='bool'),
|
||||
remote_src=dict(default=False, type='bool'),
|
||||
regexp = dict(required=False),
|
||||
),
|
||||
add_file_common_args=True
|
||||
)
|
||||
|
||||
changed = False
|
||||
pathmd5 = None
|
||||
destmd5 = None
|
||||
src = os.path.expanduser(module.params['src'])
|
||||
dest = os.path.expanduser(module.params['dest'])
|
||||
backup = module.params['backup']
|
||||
delimiter = module.params['delimiter']
|
||||
regexp = module.params['regexp']
|
||||
compiled_regexp = None
|
||||
|
||||
if not os.path.exists(src):
|
||||
module.fail_json(msg="Source (%s) does not exist" % src)
|
||||
|
||||
if not os.path.isdir(src):
|
||||
module.fail_json(msg="Source (%s) is not a directory" % src)
|
||||
|
||||
if regexp != None:
|
||||
try:
|
||||
compiled_regexp = re.compile(regexp)
|
||||
except re.error, e:
|
||||
module.fail_json(msg="Invalid Regexp (%s) in \"%s\"" % (e, regexp))
|
||||
|
||||
path = assemble_from_fragments(src, delimiter, compiled_regexp)
|
||||
pathmd5 = module.md5(path)
|
||||
|
||||
if os.path.exists(dest):
|
||||
destmd5 = module.md5(dest)
|
||||
|
||||
if pathmd5 != destmd5:
|
||||
if backup and destmd5 is not None:
|
||||
module.backup_local(dest)
|
||||
shutil.copy(path, dest)
|
||||
changed = True
|
||||
|
||||
os.remove(path)
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
# Mission complete
|
||||
module.exit_json(src=src, dest=dest, md5sum=pathmd5, changed=changed, msg="OK")
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
||||
|
@ -1,254 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: copy
|
||||
version_added: "historical"
|
||||
short_description: Copies files to remote locations.
|
||||
description:
|
||||
- The M(copy) module copies a file on the local box to remote locations.
|
||||
options:
|
||||
src:
|
||||
description:
|
||||
- Local path to a file to copy to the remote server; can be absolute or relative.
|
||||
If path is a directory, it is copied recursively. In this case, if path ends
|
||||
with "/", only inside contents of that directory are copied to destination.
|
||||
Otherwise, if it does not end with "/", the directory itself with all contents
|
||||
is copied. This behavior is similar to Rsync.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
content:
|
||||
version_added: "1.1"
|
||||
description:
|
||||
- When used instead of 'src', sets the contents of a file directly to the specified value.
|
||||
required: false
|
||||
default: null
|
||||
dest:
|
||||
description:
|
||||
- Remote absolute path where the file should be copied to. If src is a directory,
|
||||
this must be a directory too.
|
||||
required: true
|
||||
default: null
|
||||
backup:
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can get
|
||||
the original file back if you somehow clobbered it incorrectly.
|
||||
version_added: "0.7"
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
force:
|
||||
description:
|
||||
- the default is C(yes), which will replace the remote file when contents
|
||||
are different than the source. If C(no), the file will only be transferred
|
||||
if the destination does not exist.
|
||||
version_added: "1.1"
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "yes"
|
||||
aliases: [ "thirsty" ]
|
||||
validate:
|
||||
description:
|
||||
- The validation command to run before copying into place. The path to the file to
|
||||
validate is passed in via '%s' which must be present as in the visudo example below.
|
||||
The command is passed securely so shell features like expansion and pipes won't work.
|
||||
required: false
|
||||
default: ""
|
||||
version_added: "1.2"
|
||||
directory_mode:
|
||||
description:
|
||||
- When doing a recursive copy set the mode for the directories. If this is not set we will use the system
|
||||
defaults. The mode is only set on directories which are newly created, and will not affect those that
|
||||
already existed.
|
||||
required: false
|
||||
version_added: "1.5"
|
||||
extends_documentation_fragment: files
|
||||
author: Michael DeHaan
|
||||
notes:
|
||||
- The "copy" module recursively copy facility does not scale to lots (>hundreds) of files.
|
||||
For alternative, see synchronize module, which is a wrapper around rsync.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example from Ansible Playbooks
|
||||
- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode=0644
|
||||
|
||||
# The same example as above, but using a symbolic mode equivalent to 0644
|
||||
- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode="u=rw,g=r,o=r"
|
||||
|
||||
# Another symbolic mode example, adding some permissions and removing others
|
||||
- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode="u+rw,g-wx,o-rwx"
|
||||
|
||||
# Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version
|
||||
- copy: src=/mine/ntp.conf dest=/etc/ntp.conf owner=root group=root mode=644 backup=yes
|
||||
|
||||
# Copy a new "sudoers" file into place, after passing validation with visudo
|
||||
- copy: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s'
|
||||
'''
|
||||
|
||||
|
||||
def split_pre_existing_dir(dirname):
|
||||
'''
|
||||
Return the first pre-existing directory and a list of the new directories that will be created.
|
||||
'''
|
||||
|
||||
head, tail = os.path.split(dirname)
|
||||
if not os.path.exists(head):
|
||||
(pre_existing_dir, new_directory_list) = split_pre_existing_dir(head)
|
||||
else:
|
||||
return (head, [ tail ])
|
||||
new_directory_list.append(tail)
|
||||
return (pre_existing_dir, new_directory_list)
|
||||
|
||||
|
||||
def adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed):
|
||||
'''
|
||||
Walk the new directories list and make sure that permissions are as we would expect
|
||||
'''
|
||||
|
||||
if len(new_directory_list) > 0:
|
||||
working_dir = os.path.join(pre_existing_dir, new_directory_list.pop(0))
|
||||
directory_args['path'] = working_dir
|
||||
changed = module.set_fs_attributes_if_different(directory_args, changed)
|
||||
changed = adjust_recursive_directory_permissions(working_dir, new_directory_list, module, directory_args, changed)
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
# not checking because of daisy chain to file module
|
||||
argument_spec = dict(
|
||||
src = dict(required=False),
|
||||
original_basename = dict(required=False), # used to handle 'dest is a directory' via template, a slight hack
|
||||
content = dict(required=False, no_log=True),
|
||||
dest = dict(required=True),
|
||||
backup = dict(default=False, type='bool'),
|
||||
force = dict(default=True, aliases=['thirsty'], type='bool'),
|
||||
validate = dict(required=False, type='str'),
|
||||
directory_mode = dict(required=False)
|
||||
),
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
src = os.path.expanduser(module.params['src'])
|
||||
dest = os.path.expanduser(module.params['dest'])
|
||||
backup = module.params['backup']
|
||||
force = module.params['force']
|
||||
original_basename = module.params.get('original_basename',None)
|
||||
validate = module.params.get('validate',None)
|
||||
follow = module.params['follow']
|
||||
|
||||
if not os.path.exists(src):
|
||||
module.fail_json(msg="Source %s failed to transfer" % (src))
|
||||
if not os.access(src, os.R_OK):
|
||||
module.fail_json(msg="Source %s not readable" % (src))
|
||||
|
||||
md5sum_src = module.md5(src)
|
||||
md5sum_dest = None
|
||||
|
||||
changed = False
|
||||
|
||||
# Special handling for recursive copy - create intermediate dirs
|
||||
if original_basename and dest.endswith("/"):
|
||||
dest = os.path.join(dest, original_basename)
|
||||
dirname = os.path.dirname(dest)
|
||||
if not os.path.exists(dirname):
|
||||
(pre_existing_dir, new_directory_list) = split_pre_existing_dir(dirname)
|
||||
os.makedirs(dirname)
|
||||
directory_args = module.load_file_common_arguments(module.params)
|
||||
directory_mode = module.params["directory_mode"]
|
||||
if directory_mode is not None:
|
||||
directory_args['mode'] = directory_mode
|
||||
else:
|
||||
directory_args['mode'] = None
|
||||
adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed)
|
||||
|
||||
if os.path.exists(dest):
|
||||
if os.path.islink(dest) and follow:
|
||||
dest = os.path.realpath(dest)
|
||||
if not force:
|
||||
module.exit_json(msg="file already exists", src=src, dest=dest, changed=False)
|
||||
if (os.path.isdir(dest)):
|
||||
basename = os.path.basename(src)
|
||||
if original_basename:
|
||||
basename = original_basename
|
||||
dest = os.path.join(dest, basename)
|
||||
if os.access(dest, os.R_OK):
|
||||
md5sum_dest = module.md5(dest)
|
||||
else:
|
||||
if not os.path.exists(os.path.dirname(dest)):
|
||||
try:
|
||||
# os.path.exists() can return false in some
|
||||
# circumstances where the directory does not have
|
||||
# the execute bit for the current user set, in
|
||||
# which case the stat() call will raise an OSError
|
||||
os.stat(os.path.dirname(dest))
|
||||
except OSError, e:
|
||||
if "permission denied" in str(e).lower():
|
||||
module.fail_json(msg="Destination directory %s is not accessible" % (os.path.dirname(dest)))
|
||||
module.fail_json(msg="Destination directory %s does not exist" % (os.path.dirname(dest)))
|
||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||
module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest)))
|
||||
|
||||
backup_file = None
|
||||
if md5sum_src != md5sum_dest or os.path.islink(dest):
|
||||
try:
|
||||
if backup:
|
||||
if os.path.exists(dest):
|
||||
backup_file = module.backup_local(dest)
|
||||
# allow for conversion from symlink.
|
||||
if os.path.islink(dest):
|
||||
os.unlink(dest)
|
||||
open(dest, 'w').close()
|
||||
if validate:
|
||||
if "%s" not in validate:
|
||||
module.fail_json(msg="validate must contain %%s: %s" % (validate))
|
||||
(rc,out,err) = module.run_command(validate % src)
|
||||
if rc != 0:
|
||||
module.fail_json(msg="failed to validate: rc:%s error:%s" % (rc,err))
|
||||
module.atomic_move(src, dest)
|
||||
except IOError:
|
||||
module.fail_json(msg="failed to copy: %s to %s" % (src, dest))
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
|
||||
res_args = dict(
|
||||
dest = dest, src = src, md5sum = md5sum_src, changed = changed
|
||||
)
|
||||
if backup_file:
|
||||
res_args['backup_file'] = backup_file
|
||||
|
||||
module.params['dest'] = dest
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed'])
|
||||
|
||||
module.exit_json(**res_args)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,67 +0,0 @@
|
||||
# this is a virtual module that is entirely implemented server side
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: fetch
|
||||
short_description: Fetches a file from remote nodes
|
||||
description:
|
||||
- This module works like M(copy), but in reverse. It is used for fetching
|
||||
files from remote machines and storing them locally in a file tree,
|
||||
organized by hostname. Note that this module is written to transfer
|
||||
log files that might not be present, so a missing remote file won't
|
||||
be an error unless fail_on_missing is set to 'yes'.
|
||||
version_added: "0.2"
|
||||
options:
|
||||
src:
|
||||
description:
|
||||
- The file on the remote system to fetch. This I(must) be a file, not a
|
||||
directory. Recursive fetching may be supported in a later release.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
dest:
|
||||
description:
|
||||
- A directory to save the file into. For example, if the I(dest)
|
||||
directory is C(/backup) a I(src) file named C(/etc/profile) on host
|
||||
C(host.example.com), would be saved into
|
||||
C(/backup/host.example.com/etc/profile)
|
||||
required: true
|
||||
default: null
|
||||
fail_on_missing:
|
||||
version_added: "1.1"
|
||||
description:
|
||||
- Makes it fails when the source file is missing.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
validate_md5:
|
||||
version_added: "1.4"
|
||||
description:
|
||||
- Verify that the source and destination md5sums match after the files are fetched.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "yes"
|
||||
flat:
|
||||
version_added: "1.2"
|
||||
description:
|
||||
Allows you to override the default behavior of prepending hostname/path/to/file to
|
||||
the destination. If dest ends with '/', it will use the basename of the source
|
||||
file, similar to the copy module. Obviously this is only handy if the filenames
|
||||
are unique.
|
||||
requirements: []
|
||||
author: Michael DeHaan
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Store file into /tmp/fetched/host.example.com/tmp/somefile
|
||||
- fetch: src=/tmp/somefile dest=/tmp/fetched
|
||||
|
||||
# Specifying a path directly
|
||||
- fetch: src=/tmp/somefile dest=/tmp/prefix-{{ ansible_hostname }} flat=yes
|
||||
|
||||
# Specifying a destination path
|
||||
- fetch: src=/tmp/uniquefile dest=/tmp/special/ flat=yes
|
||||
|
||||
# Storing in a path relative to the playbook
|
||||
- fetch: src=/tmp/uniquefile dest=special/prefix-{{ ansible_hostname }} flat=yes
|
||||
'''
|
@ -1,358 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import shutil
|
||||
import stat
|
||||
import grp
|
||||
import pwd
|
||||
try:
|
||||
import selinux
|
||||
HAVE_SELINUX=True
|
||||
except ImportError:
|
||||
HAVE_SELINUX=False
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: file
|
||||
version_added: "historical"
|
||||
short_description: Sets attributes of files
|
||||
extends_documentation_fragment: files
|
||||
description:
|
||||
- Sets attributes of files, symlinks, and directories, or removes
|
||||
files/symlinks/directories. Many other modules support the same options as
|
||||
the M(file) module - including M(copy), M(template), and M(assemble).
|
||||
notes:
|
||||
- See also M(copy), M(template), M(assemble)
|
||||
requirements: [ ]
|
||||
author: Michael DeHaan
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- 'path to the file being managed. Aliases: I(dest), I(name)'
|
||||
required: true
|
||||
default: []
|
||||
aliases: ['dest', 'name']
|
||||
state:
|
||||
description:
|
||||
- If C(directory), all immediate subdirectories will be created if they
|
||||
do not exist, since 1.7 they will be created with the supplied permissions.
|
||||
If C(file), the file will NOT be created if it does not exist, see the M(copy)
|
||||
or M(template) module if you want that behavior. If C(link), the symbolic
|
||||
link will be created or changed. Use C(hard) for hardlinks. If C(absent),
|
||||
directories will be recursively deleted, and files or symlinks will be unlinked.
|
||||
If C(touch) (new in 1.4), an empty file will be created if the c(path) does not
|
||||
exist, while an existing file or directory will receive updated file access and
|
||||
modification times (similar to the way `touch` works from the command line).
|
||||
required: false
|
||||
default: file
|
||||
choices: [ file, link, directory, hard, touch, absent ]
|
||||
src:
|
||||
required: false
|
||||
default: null
|
||||
choices: []
|
||||
description:
|
||||
- path of the file to link to (applies only to C(state=link)). Will accept absolute,
|
||||
relative and nonexisting paths. Relative paths are not expanded.
|
||||
recurse:
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
version_added: "1.1"
|
||||
description:
|
||||
- recursively set the specified file attributes (applies only to state=directory)
|
||||
force:
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
description:
|
||||
- 'force the creation of the symlinks in two cases: the source file does
|
||||
not exist (but will appear later); the destination exists and is a file (so, we need to unlink the
|
||||
"path" file and create symlink to the "src" file in place of it).'
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- file: path=/etc/foo.conf owner=foo group=foo mode=0644
|
||||
- file: src=/file/to/link/to dest=/path/to/symlink owner=foo group=foo state=link
|
||||
- file: src=/tmp/{{ item.path }} dest={{ item.dest }} state=link
|
||||
with_items:
|
||||
- { path: 'x', dest: 'y' }
|
||||
- { path: 'z', dest: 'k' }
|
||||
|
||||
# touch a file, using symbolic modes to set the permissions (equivalent to 0644)
|
||||
- file: path=/etc/foo.conf state=touch mode="u=rw,g=r,o=r"
|
||||
|
||||
# touch the same file, but add/remove some permissions
|
||||
- file: path=/etc/foo.conf state=touch mode="u+rw,g-wx,o-rwx"
|
||||
|
||||
'''
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(choices=['file','directory','link','hard','touch','absent'], default=None),
|
||||
path = dict(aliases=['dest', 'name'], required=True),
|
||||
original_basename = dict(required=False), # Internal use only, for recursive ops
|
||||
recurse = dict(default='no', type='bool'),
|
||||
force = dict(required=False,default=False,type='bool'),
|
||||
diff_peek = dict(default=None),
|
||||
validate = dict(required=False, default=None),
|
||||
src = dict(required=False, default=None),
|
||||
),
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
params = module.params
|
||||
state = params['state']
|
||||
force = params['force']
|
||||
diff_peek = params['diff_peek']
|
||||
src = params['src']
|
||||
follow = params['follow']
|
||||
|
||||
# modify source as we later reload and pass, specially relevant when used by other modules.
|
||||
params['path'] = path = os.path.expanduser(params['path'])
|
||||
|
||||
# short-circuit for diff_peek
|
||||
if diff_peek is not None:
|
||||
appears_binary = False
|
||||
try:
|
||||
f = open(path)
|
||||
b = f.read(8192)
|
||||
f.close()
|
||||
if "\x00" in b:
|
||||
appears_binary = True
|
||||
except:
|
||||
pass
|
||||
module.exit_json(path=path, changed=False, appears_binary=appears_binary)
|
||||
|
||||
# Find out current state
|
||||
prev_state = 'absent'
|
||||
if os.path.lexists(path):
|
||||
if os.path.islink(path):
|
||||
prev_state = 'link'
|
||||
elif os.path.isdir(path):
|
||||
prev_state = 'directory'
|
||||
elif os.stat(path).st_nlink > 1:
|
||||
prev_state = 'hard'
|
||||
else:
|
||||
# could be many other things, but defaulting to file
|
||||
prev_state = 'file'
|
||||
|
||||
# state should default to file, but since that creates many conflicts,
|
||||
# default to 'current' when it exists.
|
||||
if state is None:
|
||||
if prev_state != 'absent':
|
||||
state = prev_state
|
||||
else:
|
||||
state = 'file'
|
||||
|
||||
# source is both the source of a symlink or an informational passing of the src for a template module
|
||||
# or copy module, even if this module never uses it, it is needed to key off some things
|
||||
if src is not None:
|
||||
src = os.path.expanduser(src)
|
||||
|
||||
# original_basename is used by other modules that depend on file.
|
||||
if os.path.isdir(path) and state not in ["link", "absent"]:
|
||||
if params['original_basename']:
|
||||
basename = params['original_basename']
|
||||
else:
|
||||
basename = os.path.basename(src)
|
||||
params['path'] = path = os.path.join(path, basename)
|
||||
else:
|
||||
if state in ['link','hard']:
|
||||
if follow:
|
||||
# use the current target of the link as the source
|
||||
src = os.readlink(path)
|
||||
else:
|
||||
module.fail_json(msg='src and dest are required for creating links')
|
||||
|
||||
# make sure the target path is a directory when we're doing a recursive operation
|
||||
recurse = params['recurse']
|
||||
if recurse and state != 'directory':
|
||||
module.fail_json(path=path, msg="recurse option requires state to be 'directory'")
|
||||
|
||||
file_args = module.load_file_common_arguments(params)
|
||||
changed = False
|
||||
|
||||
if state == 'absent':
|
||||
if state != prev_state:
|
||||
if not module.check_mode:
|
||||
if prev_state == 'directory':
|
||||
try:
|
||||
shutil.rmtree(path, ignore_errors=False)
|
||||
except Exception, e:
|
||||
module.fail_json(msg="rmtree failed: %s" % str(e))
|
||||
else:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except Exception, e:
|
||||
module.fail_json(path=path, msg="unlinking failed: %s " % str(e))
|
||||
module.exit_json(path=path, changed=True)
|
||||
else:
|
||||
module.exit_json(path=path, changed=False)
|
||||
|
||||
elif state == 'file':
|
||||
if state != prev_state:
|
||||
# file is not absent and any other state is a conflict
|
||||
module.fail_json(path=path, msg='file (%s) is %s, cannot continue' % (path, prev_state))
|
||||
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
module.exit_json(path=path, changed=changed)
|
||||
|
||||
elif state == 'directory':
|
||||
if prev_state == 'absent':
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
changed = True
|
||||
curpath = ''
|
||||
# Split the path so we can apply filesystem attributes recursively
|
||||
# from the root (/) directory for absolute paths or the base path
|
||||
# of a relative path. We can then walk the appropriate directory
|
||||
# path to apply attributes.
|
||||
for dirname in path.strip('/').split('/'):
|
||||
curpath = '/'.join([curpath, dirname])
|
||||
# Remove leading slash if we're creating a relative path
|
||||
if not os.path.isabs(path):
|
||||
curpath = curpath.lstrip('/')
|
||||
if not os.path.exists(curpath):
|
||||
os.mkdir(curpath)
|
||||
tmp_file_args = file_args.copy()
|
||||
tmp_file_args['path']=curpath
|
||||
changed = module.set_fs_attributes_if_different(tmp_file_args, changed)
|
||||
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
|
||||
if recurse:
|
||||
for root,dirs,files in os.walk( file_args['path'] ):
|
||||
for fsobj in dirs + files:
|
||||
fsname=os.path.join(root, fsobj)
|
||||
tmp_file_args = file_args.copy()
|
||||
tmp_file_args['path']=fsname
|
||||
changed = module.set_fs_attributes_if_different(tmp_file_args, changed)
|
||||
|
||||
module.exit_json(path=path, changed=changed)
|
||||
|
||||
elif state in ['link','hard']:
|
||||
|
||||
if os.path.isdir(path) and not os.path.islink(path):
|
||||
relpath = path
|
||||
else:
|
||||
relpath = os.path.dirname(path)
|
||||
|
||||
absrc = os.path.join(relpath, src)
|
||||
if not os.path.exists(absrc) and not force:
|
||||
module.fail_json(path=path, src=src, msg='src file does not exist, use "force=yes" if you really want to create the link: %s' % absrc)
|
||||
|
||||
if state == 'hard':
|
||||
if not os.path.isabs(src):
|
||||
module.fail_json(msg="absolute paths are required")
|
||||
elif prev_state == 'directory':
|
||||
if not force:
|
||||
module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path))
|
||||
elif len(os.listdir(path)) > 0:
|
||||
# refuse to replace a directory that has files in it
|
||||
module.fail_json(path=path, msg='the directory %s is not empty, refusing to convert it' % path)
|
||||
elif prev_state in ['file', 'hard'] and not force:
|
||||
module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path))
|
||||
|
||||
if prev_state == 'absent':
|
||||
changed = True
|
||||
elif prev_state == 'link':
|
||||
old_src = os.readlink(path)
|
||||
if old_src != src:
|
||||
changed = True
|
||||
elif prev_state == 'hard':
|
||||
if not (state == 'hard' and os.stat(path).st_ino == os.stat(src).st_ino):
|
||||
changed = True
|
||||
if not force:
|
||||
module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination')
|
||||
elif prev_state in ['file', 'directory']:
|
||||
changed = True
|
||||
if not force:
|
||||
module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state)
|
||||
else:
|
||||
module.fail_json(dest=path, src=src, msg='unexpected position reached')
|
||||
|
||||
if changed and not module.check_mode:
|
||||
if prev_state != 'absent':
|
||||
# try to replace atomically
|
||||
tmppath = '/'.join([os.path.dirname(path), ".%s.%s.tmp" % (os.getpid(),time.time())])
|
||||
try:
|
||||
if prev_state == 'directory' and (state == 'hard' or state == 'link'):
|
||||
os.rmdir(path)
|
||||
if state == 'hard':
|
||||
os.link(src,tmppath)
|
||||
else:
|
||||
os.symlink(src, tmppath)
|
||||
os.rename(tmppath, path)
|
||||
except OSError, e:
|
||||
if os.path.exists(tmppath):
|
||||
os.unlink(tmppath)
|
||||
module.fail_json(path=path, msg='Error while replacing: %s' % str(e))
|
||||
else:
|
||||
try:
|
||||
if state == 'hard':
|
||||
os.link(src,path)
|
||||
else:
|
||||
os.symlink(src, path)
|
||||
except OSError, e:
|
||||
module.fail_json(path=path, msg='Error while linking: %s' % str(e))
|
||||
|
||||
if module.check_mode and not os.path.exists(path):
|
||||
module.exit_json(dest=path, src=src, changed=changed)
|
||||
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
module.exit_json(dest=path, src=src, changed=changed)
|
||||
|
||||
elif state == 'touch':
|
||||
if not module.check_mode:
|
||||
|
||||
if prev_state == 'absent':
|
||||
try:
|
||||
open(path, 'w').close()
|
||||
except OSError, e:
|
||||
module.fail_json(path=path, msg='Error, could not touch target: %s' % str(e))
|
||||
elif prev_state in ['file', 'directory']:
|
||||
try:
|
||||
os.utime(path, None)
|
||||
except OSError, e:
|
||||
module.fail_json(path=path, msg='Error while touching existing target: %s' % str(e))
|
||||
else:
|
||||
module.fail_json(msg='Cannot touch other than files and directories')
|
||||
try:
|
||||
module.set_fs_attributes_if_different(file_args, True)
|
||||
except SystemExit, e:
|
||||
if e.code:
|
||||
# We take this to mean that fail_json() was called from
|
||||
# somewhere in basic.py
|
||||
if prev_state == 'absent':
|
||||
# If we just created the file we can safely remove it
|
||||
os.remove(path)
|
||||
raise e
|
||||
|
||||
module.exit_json(dest=path, changed=True)
|
||||
|
||||
module.fail_json(path=path, msg='unexpected position reached')
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
||||
|
@ -1,207 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
|
||||
#
|
||||
# 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: ini_file
|
||||
short_description: Tweak settings in INI files
|
||||
description:
|
||||
- Manage (add, remove, change) individual settings in an INI-style file without having
|
||||
to manage the file as a whole with, say, M(template) or M(assemble). Adds missing
|
||||
sections if they don't exist.
|
||||
- Comments are discarded when the source file is read, and therefore will not
|
||||
show up in the destination file.
|
||||
version_added: "0.9"
|
||||
options:
|
||||
dest:
|
||||
description:
|
||||
- Path to the INI-style file; this file is created if required
|
||||
required: true
|
||||
default: null
|
||||
section:
|
||||
description:
|
||||
- Section name in INI file. This is added if C(state=present) automatically when
|
||||
a single value is being set.
|
||||
required: true
|
||||
default: null
|
||||
option:
|
||||
description:
|
||||
- if set (required for changing a I(value)), this is the name of the option.
|
||||
- May be omitted if adding/removing a whole I(section).
|
||||
required: false
|
||||
default: null
|
||||
value:
|
||||
description:
|
||||
- the string value to be associated with an I(option). May be omitted when removing an I(option).
|
||||
required: false
|
||||
default: null
|
||||
backup:
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can get
|
||||
the original file back if you somehow clobbered it incorrectly.
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
others:
|
||||
description:
|
||||
- all arguments accepted by the M(file) module also work here
|
||||
required: false
|
||||
notes:
|
||||
- While it is possible to add an I(option) without specifying a I(value), this makes
|
||||
no sense.
|
||||
- A section named C(default) cannot be added by the module, but if it exists, individual
|
||||
options within the section can be updated. (This is a limitation of Python's I(ConfigParser).)
|
||||
Either use M(template) to create a base INI file with a C([default]) section, or use
|
||||
M(lineinfile) to add the missing line.
|
||||
requirements: [ ConfigParser ]
|
||||
author: Jan-Piet Mens
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Ensure "fav=lemonade is in section "[drinks]" in specified file
|
||||
- ini_file: dest=/etc/conf section=drinks option=fav value=lemonade mode=0600 backup=yes
|
||||
|
||||
- ini_file: dest=/etc/anotherconf
|
||||
section=drinks
|
||||
option=temperature
|
||||
value=cold
|
||||
backup=yes
|
||||
'''
|
||||
|
||||
import ConfigParser
|
||||
import sys
|
||||
|
||||
# ==============================================================
|
||||
# do_ini
|
||||
|
||||
def do_ini(module, filename, section=None, option=None, value=None, state='present', backup=False):
|
||||
|
||||
changed = False
|
||||
if (sys.version_info[0] == 2 and sys.version_info[1] >= 7) or sys.version_info[0] >= 3:
|
||||
cp = ConfigParser.ConfigParser(allow_no_value=True)
|
||||
else:
|
||||
cp = ConfigParser.ConfigParser()
|
||||
cp.optionxform = identity
|
||||
|
||||
try:
|
||||
f = open(filename)
|
||||
cp.readfp(f)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
|
||||
if state == 'absent':
|
||||
if option is None and value is None:
|
||||
if cp.has_section(section):
|
||||
cp.remove_section(section)
|
||||
changed = True
|
||||
else:
|
||||
if option is not None:
|
||||
try:
|
||||
if cp.get(section, option):
|
||||
cp.remove_option(section, option)
|
||||
changed = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if state == 'present':
|
||||
|
||||
# DEFAULT section is always there by DEFAULT, so never try to add it.
|
||||
if cp.has_section(section) == False and section.upper() != 'DEFAULT':
|
||||
|
||||
cp.add_section(section)
|
||||
changed = True
|
||||
|
||||
if option is not None and value is not None:
|
||||
try:
|
||||
oldvalue = cp.get(section, option)
|
||||
if str(value) != str(oldvalue):
|
||||
cp.set(section, option, value)
|
||||
changed = True
|
||||
except ConfigParser.NoSectionError:
|
||||
cp.set(section, option, value)
|
||||
changed = True
|
||||
except ConfigParser.NoOptionError:
|
||||
cp.set(section, option, value)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
if backup:
|
||||
module.backup_local(filename)
|
||||
|
||||
try:
|
||||
f = open(filename, 'w')
|
||||
cp.write(f)
|
||||
except:
|
||||
module.fail_json(msg="Can't creat %s" % filename)
|
||||
|
||||
return changed
|
||||
|
||||
# ==============================================================
|
||||
# identity
|
||||
|
||||
def identity(arg):
|
||||
"""
|
||||
This function simply returns its argument. It serves as a
|
||||
replacement for ConfigParser.optionxform, which by default
|
||||
changes arguments to lower case. The identity function is a
|
||||
better choice than str() or unicode(), because it is
|
||||
encoding-agnostic.
|
||||
"""
|
||||
return arg
|
||||
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
dest = dict(required=True),
|
||||
section = dict(required=True),
|
||||
option = dict(required=False),
|
||||
value = dict(required=False),
|
||||
backup = dict(default='no', type='bool'),
|
||||
state = dict(default='present', choices=['present', 'absent'])
|
||||
),
|
||||
add_file_common_args = True
|
||||
)
|
||||
|
||||
info = dict()
|
||||
|
||||
dest = os.path.expanduser(module.params['dest'])
|
||||
section = module.params['section']
|
||||
option = module.params['option']
|
||||
value = module.params['value']
|
||||
state = module.params['state']
|
||||
backup = module.params['backup']
|
||||
|
||||
changed = do_ini(module, dest, section, option, value, state, backup)
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
changed = module.set_fs_attributes_if_different(file_args, changed)
|
||||
|
||||
# Mission complete
|
||||
module.exit_json(dest=dest, changed=changed, msg="OK")
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -1,400 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
|
||||
# (c) 2014, Ahti Kitsik <ak@ahtik.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import pipes
|
||||
import re
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: lineinfile
|
||||
author: Daniel Hokka Zakrisson, Ahti Kitsik
|
||||
short_description: Ensure a particular line is in a file, or replace an
|
||||
existing line using a back-referenced regular expression.
|
||||
description:
|
||||
- This module will search a file for a line, and ensure that it is present or absent.
|
||||
- This is primarily useful when you want to change a single line in a
|
||||
file only. For other cases, see the M(copy) or M(template) modules.
|
||||
version_added: "0.7"
|
||||
options:
|
||||
dest:
|
||||
required: true
|
||||
aliases: [ name, destfile ]
|
||||
description:
|
||||
- The file to modify.
|
||||
regexp:
|
||||
required: false
|
||||
version_added: 1.7
|
||||
description:
|
||||
- The regular expression to look for in every line of the file. For
|
||||
C(state=present), the pattern to replace if found; only the last line
|
||||
found will be replaced. For C(state=absent), the pattern of the line
|
||||
to remove. Uses Python regular expressions; see
|
||||
U(http://docs.python.org/2/library/re.html).
|
||||
state:
|
||||
required: false
|
||||
choices: [ present, absent ]
|
||||
default: "present"
|
||||
aliases: []
|
||||
description:
|
||||
- Whether the line should be there or not.
|
||||
line:
|
||||
required: false
|
||||
description:
|
||||
- Required for C(state=present). The line to insert/replace into the
|
||||
file. If C(backrefs) is set, may contain backreferences that will get
|
||||
expanded with the C(regexp) capture groups if the regexp matches. The
|
||||
backreferences should be double escaped (see examples).
|
||||
backrefs:
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
version_added: "1.1"
|
||||
description:
|
||||
- Used with C(state=present). If set, line can contain backreferences
|
||||
(both positional and named) that will get populated if the C(regexp)
|
||||
matches. This flag changes the operation of the module slightly;
|
||||
C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp)
|
||||
doesn't match anywhere in the file, the file will be left unchanged.
|
||||
If the C(regexp) does match, the last matching line will be replaced by
|
||||
the expanded line parameter.
|
||||
insertafter:
|
||||
required: false
|
||||
default: EOF
|
||||
description:
|
||||
- Used with C(state=present). If specified, the line will be inserted
|
||||
after the specified regular expression. A special value is
|
||||
available; C(EOF) for inserting the line at the end of the file.
|
||||
May not be used with C(backrefs).
|
||||
choices: [ 'EOF', '*regex*' ]
|
||||
insertbefore:
|
||||
required: false
|
||||
version_added: "1.1"
|
||||
description:
|
||||
- Used with C(state=present). If specified, the line will be inserted
|
||||
before the specified regular expression. A value is available;
|
||||
C(BOF) for inserting the line at the beginning of the file.
|
||||
May not be used with C(backrefs).
|
||||
choices: [ 'BOF', '*regex*' ]
|
||||
create:
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
description:
|
||||
- Used with C(state=present). If specified, the file will be created
|
||||
if it does not already exist. By default it will fail if the file
|
||||
is missing.
|
||||
backup:
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can
|
||||
get the original file back if you somehow clobbered it incorrectly.
|
||||
validate:
|
||||
required: false
|
||||
description:
|
||||
- validation to run before copying into place.
|
||||
Use %s in the command to indicate the current file to validate.
|
||||
The command is passed securely so shell features like
|
||||
expansion and pipes won't work.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.4"
|
||||
others:
|
||||
description:
|
||||
- All arguments accepted by the M(file) module also work here.
|
||||
required: false
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- lineinfile: dest=/etc/selinux/config regexp=^SELINUX= line=SELINUX=disabled
|
||||
|
||||
- lineinfile: dest=/etc/sudoers state=absent regexp="^%wheel"
|
||||
|
||||
- lineinfile: dest=/etc/hosts regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' owner=root group=root mode=0644
|
||||
|
||||
- lineinfile: dest=/etc/httpd/conf/httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080"
|
||||
|
||||
- lineinfile: dest=/etc/services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default"
|
||||
|
||||
# Add a line to a file if it does not exist, without passing regexp
|
||||
- lineinfile: dest=/tmp/testfile line="192.168.1.99 foo.lab.net foo"
|
||||
|
||||
# Fully quoted because of the ': ' on the line. See the Gotchas in the YAML docs.
|
||||
- lineinfile: "dest=/etc/sudoers state=present regexp='^%wheel' line='%wheel ALL=(ALL) NOPASSWD: ALL'"
|
||||
|
||||
- lineinfile: dest=/opt/jboss-as/bin/standalone.conf regexp='^(.*)Xms(\d+)m(.*)$' line='\1Xms${xms}m\3' backrefs=yes
|
||||
|
||||
# Validate a the sudoers file before saving
|
||||
- lineinfile: dest=/etc/sudoers state=present regexp='^%ADMIN ALL\=' line='%ADMIN ALL=(ALL) NOPASSWD:ALL' validate='visudo -cf %s'
|
||||
"""
|
||||
|
||||
def write_changes(module,lines,dest):
|
||||
|
||||
tmpfd, tmpfile = tempfile.mkstemp()
|
||||
f = os.fdopen(tmpfd,'wb')
|
||||
f.writelines(lines)
|
||||
f.close()
|
||||
|
||||
validate = module.params.get('validate', None)
|
||||
valid = not validate
|
||||
if validate:
|
||||
if "%s" not in validate:
|
||||
module.fail_json(msg="validate must contain %%s: %s" % (validate))
|
||||
(rc, out, err) = module.run_command(validate % tmpfile)
|
||||
valid = rc == 0
|
||||
if rc != 0:
|
||||
module.fail_json(msg='failed to validate: '
|
||||
'rc:%s error:%s' % (rc,err))
|
||||
if valid:
|
||||
module.atomic_move(tmpfile, os.path.realpath(dest))
|
||||
|
||||
def check_file_attrs(module, changed, message):
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
|
||||
if changed:
|
||||
message += " and "
|
||||
changed = True
|
||||
message += "ownership, perms or SE linux context changed"
|
||||
|
||||
return message, changed
|
||||
|
||||
|
||||
def present(module, dest, regexp, line, insertafter, insertbefore, create,
|
||||
backup, backrefs):
|
||||
|
||||
if not os.path.exists(dest):
|
||||
if not create:
|
||||
module.fail_json(rc=257, msg='Destination %s does not exist !' % dest)
|
||||
destpath = os.path.dirname(dest)
|
||||
if not os.path.exists(destpath):
|
||||
os.makedirs(destpath)
|
||||
lines = []
|
||||
else:
|
||||
f = open(dest, 'rb')
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
|
||||
msg = ""
|
||||
|
||||
if regexp is not None:
|
||||
mre = re.compile(regexp)
|
||||
|
||||
if insertafter not in (None, 'BOF', 'EOF'):
|
||||
insre = re.compile(insertafter)
|
||||
elif insertbefore not in (None, 'BOF'):
|
||||
insre = re.compile(insertbefore)
|
||||
else:
|
||||
insre = None
|
||||
|
||||
# index[0] is the line num where regexp has been found
|
||||
# index[1] is the line num where insertafter/inserbefore has been found
|
||||
index = [-1, -1]
|
||||
m = None
|
||||
for lineno, cur_line in enumerate(lines):
|
||||
if regexp is not None:
|
||||
match_found = mre.search(cur_line)
|
||||
else:
|
||||
match_found = line == cur_line.rstrip('\r\n')
|
||||
if match_found:
|
||||
index[0] = lineno
|
||||
m = match_found
|
||||
elif insre is not None and insre.search(cur_line):
|
||||
if insertafter:
|
||||
# + 1 for the next line
|
||||
index[1] = lineno + 1
|
||||
if insertbefore:
|
||||
# + 1 for the previous line
|
||||
index[1] = lineno
|
||||
|
||||
msg = ''
|
||||
changed = False
|
||||
# Regexp matched a line in the file
|
||||
if index[0] != -1:
|
||||
if backrefs:
|
||||
new_line = m.expand(line)
|
||||
else:
|
||||
# Don't do backref expansion if not asked.
|
||||
new_line = line
|
||||
|
||||
if lines[index[0]] != new_line + os.linesep:
|
||||
lines[index[0]] = new_line + os.linesep
|
||||
msg = 'line replaced'
|
||||
changed = True
|
||||
elif backrefs:
|
||||
# Do absolutely nothing, since it's not safe generating the line
|
||||
# without the regexp matching to populate the backrefs.
|
||||
pass
|
||||
# Add it to the beginning of the file
|
||||
elif insertbefore == 'BOF' or insertafter == 'BOF':
|
||||
lines.insert(0, line + os.linesep)
|
||||
msg = 'line added'
|
||||
changed = True
|
||||
# Add it to the end of the file if requested or
|
||||
# if insertafter=/insertbefore didn't match anything
|
||||
# (so default behaviour is to add at the end)
|
||||
elif insertafter == 'EOF':
|
||||
|
||||
# If the file is not empty then ensure there's a newline before the added line
|
||||
if len(lines)>0 and not (lines[-1].endswith('\n') or lines[-1].endswith('\r')):
|
||||
lines.append(os.linesep)
|
||||
|
||||
lines.append(line + os.linesep)
|
||||
msg = 'line added'
|
||||
changed = True
|
||||
# Do nothing if insert* didn't match
|
||||
elif index[1] == -1:
|
||||
pass
|
||||
# insert* matched, but not the regexp
|
||||
else:
|
||||
lines.insert(index[1], line + os.linesep)
|
||||
msg = 'line added'
|
||||
changed = True
|
||||
|
||||
backupdest = ""
|
||||
if changed and not module.check_mode:
|
||||
if backup and os.path.exists(dest):
|
||||
backupdest = module.backup_local(dest)
|
||||
write_changes(module, lines, dest)
|
||||
|
||||
msg, changed = check_file_attrs(module, changed, msg)
|
||||
module.exit_json(changed=changed, msg=msg, backup=backupdest)
|
||||
|
||||
|
||||
def absent(module, dest, regexp, line, backup):
|
||||
|
||||
if not os.path.exists(dest):
|
||||
module.exit_json(changed=False, msg="file not present")
|
||||
|
||||
msg = ""
|
||||
|
||||
f = open(dest, 'rb')
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
if regexp is not None:
|
||||
cre = re.compile(regexp)
|
||||
found = []
|
||||
|
||||
def matcher(cur_line):
|
||||
if regexp is not None:
|
||||
match_found = cre.search(cur_line)
|
||||
else:
|
||||
match_found = line == cur_line.rstrip('\r\n')
|
||||
if match_found:
|
||||
found.append(cur_line)
|
||||
return not match_found
|
||||
|
||||
lines = filter(matcher, lines)
|
||||
changed = len(found) > 0
|
||||
backupdest = ""
|
||||
if changed and not module.check_mode:
|
||||
if backup:
|
||||
backupdest = module.backup_local(dest)
|
||||
write_changes(module, lines, dest)
|
||||
|
||||
if changed:
|
||||
msg = "%s line(s) removed" % len(found)
|
||||
|
||||
msg, changed = check_file_attrs(module, changed, msg)
|
||||
module.exit_json(changed=changed, found=len(found), msg=msg, backup=backupdest)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
dest=dict(required=True, aliases=['name', 'destfile']),
|
||||
state=dict(default='present', choices=['absent', 'present']),
|
||||
regexp=dict(default=None),
|
||||
line=dict(aliases=['value']),
|
||||
insertafter=dict(default=None),
|
||||
insertbefore=dict(default=None),
|
||||
backrefs=dict(default=False, type='bool'),
|
||||
create=dict(default=False, type='bool'),
|
||||
backup=dict(default=False, type='bool'),
|
||||
validate=dict(default=None, type='str'),
|
||||
),
|
||||
mutually_exclusive=[['insertbefore', 'insertafter']],
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
params = module.params
|
||||
create = module.params['create']
|
||||
backup = module.params['backup']
|
||||
backrefs = module.params['backrefs']
|
||||
dest = os.path.expanduser(params['dest'])
|
||||
|
||||
|
||||
if os.path.isdir(dest):
|
||||
module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)
|
||||
|
||||
if params['state'] == 'present':
|
||||
if backrefs and params['regexp'] is None:
|
||||
module.fail_json(msg='regexp= is required with backrefs=true')
|
||||
|
||||
if params.get('line', None) is None:
|
||||
module.fail_json(msg='line= is required with state=present')
|
||||
|
||||
# Deal with the insertafter default value manually, to avoid errors
|
||||
# because of the mutually_exclusive mechanism.
|
||||
ins_bef, ins_aft = params['insertbefore'], params['insertafter']
|
||||
if ins_bef is None and ins_aft is None:
|
||||
ins_aft = 'EOF'
|
||||
|
||||
line = params['line']
|
||||
|
||||
# The safe_eval call will remove some quoting, but not others,
|
||||
# so we need to know if we should specifically unquote it.
|
||||
should_unquote = not is_quoted(line)
|
||||
|
||||
# always add one layer of quotes
|
||||
line = "'%s'" % line
|
||||
|
||||
# Replace escape sequences like '\n' while being sure
|
||||
# not to replace octal escape sequences (\ooo) since they
|
||||
# match the backref syntax.
|
||||
if backrefs:
|
||||
line = re.sub(r'(\\[0-9]{1,3})', r'\\\1', line)
|
||||
line = module.safe_eval(line)
|
||||
|
||||
# Now remove quotes around the string, if needed after
|
||||
# removing the layer we added above
|
||||
line = unquote(line)
|
||||
if should_unquote:
|
||||
line = unquote(line)
|
||||
|
||||
present(module, dest, params['regexp'], line,
|
||||
ins_aft, ins_bef, create, backup, backrefs)
|
||||
else:
|
||||
if params['regexp'] is None and params.get('line', None) is None:
|
||||
module.fail_json(msg='one of line= or regexp= is required with state=absent')
|
||||
|
||||
absent(module, dest, params['regexp'], params.get('line', None), backup)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.splitter import *
|
||||
|
||||
main()
|
@ -1,162 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Evan Kaufman <evan@digitalflophouse.com
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import re
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: replace
|
||||
author: Evan Kaufman
|
||||
short_description: Replace all instances of a particular string in a
|
||||
file using a back-referenced regular expression.
|
||||
description:
|
||||
- This module will replace all instances of a pattern within a file.
|
||||
- It is up to the user to maintain idempotence by ensuring that the
|
||||
same pattern would never match any replacements made.
|
||||
version_added: "1.6"
|
||||
options:
|
||||
dest:
|
||||
required: true
|
||||
aliases: [ name, destfile ]
|
||||
description:
|
||||
- The file to modify.
|
||||
regexp:
|
||||
required: true
|
||||
description:
|
||||
- The regular expression to look for in the contents of the file.
|
||||
Uses Python regular expressions; see
|
||||
U(http://docs.python.org/2/library/re.html).
|
||||
Uses multiline mode, which means C(^) and C($) match the beginning
|
||||
and end respectively of I(each line) of the file.
|
||||
replace:
|
||||
required: false
|
||||
description:
|
||||
- The string to replace regexp matches. May contain backreferences
|
||||
that will get expanded with the regexp capture groups if the regexp
|
||||
matches. If not set, matches are removed entirely.
|
||||
backup:
|
||||
required: false
|
||||
default: "no"
|
||||
choices: [ "yes", "no" ]
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can
|
||||
get the original file back if you somehow clobbered it incorrectly.
|
||||
validate:
|
||||
required: false
|
||||
description:
|
||||
- validation to run before copying into place
|
||||
required: false
|
||||
default: None
|
||||
others:
|
||||
description:
|
||||
- All arguments accepted by the M(file) module also work here.
|
||||
required: false
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- replace: dest=/etc/hosts regexp='(\s+)old\.host\.name(\s+.*)?$' replace='\1new.host.name\2' backup=yes
|
||||
|
||||
- replace: dest=/home/jdoe/.ssh/known_hosts regexp='^old\.host\.name[^\n]*\n' owner=jdoe group=jdoe mode=644
|
||||
|
||||
- replace: dest=/etc/apache/ports regexp='^(NameVirtualHost|Listen)\s+80\s*$' replace='\1 127.0.0.1:8080' validate='/usr/sbin/apache2ctl -f %s -t'
|
||||
"""
|
||||
|
||||
def write_changes(module,contents,dest):
|
||||
|
||||
tmpfd, tmpfile = tempfile.mkstemp()
|
||||
f = os.fdopen(tmpfd,'wb')
|
||||
f.write(contents)
|
||||
f.close()
|
||||
|
||||
validate = module.params.get('validate', None)
|
||||
valid = not validate
|
||||
if validate:
|
||||
if "%s" not in validate:
|
||||
module.fail_json(msg="validate must contain %%s: %s" % (validate))
|
||||
(rc, out, err) = module.run_command(validate % tmpfile)
|
||||
valid = rc == 0
|
||||
if rc != 0:
|
||||
module.fail_json(msg='failed to validate: '
|
||||
'rc:%s error:%s' % (rc,err))
|
||||
if valid:
|
||||
module.atomic_move(tmpfile, dest)
|
||||
|
||||
def check_file_attrs(module, changed, message):
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_file_attributes_if_different(file_args, False):
|
||||
|
||||
if changed:
|
||||
message += " and "
|
||||
changed = True
|
||||
message += "ownership, perms or SE linux context changed"
|
||||
|
||||
return message, changed
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
dest=dict(required=True, aliases=['name', 'destfile']),
|
||||
regexp=dict(required=True),
|
||||
replace=dict(default='', type='str'),
|
||||
backup=dict(default=False, type='bool'),
|
||||
validate=dict(default=None, type='str'),
|
||||
),
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
params = module.params
|
||||
dest = os.path.expanduser(params['dest'])
|
||||
|
||||
if os.path.isdir(dest):
|
||||
module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)
|
||||
|
||||
if not os.path.exists(dest):
|
||||
module.fail_json(rc=257, msg='Destination %s does not exist !' % dest)
|
||||
else:
|
||||
f = open(dest, 'rb')
|
||||
contents = f.read()
|
||||
f.close()
|
||||
|
||||
mre = re.compile(params['regexp'], re.MULTILINE)
|
||||
result = re.subn(mre, params['replace'], contents, 0)
|
||||
|
||||
if result[1] > 0 and contents != result[0]:
|
||||
msg = '%s replacements made' % result[1]
|
||||
changed = True
|
||||
else:
|
||||
msg = ''
|
||||
changed = False
|
||||
|
||||
if changed and not module.check_mode:
|
||||
if params['backup'] and os.path.exists(dest):
|
||||
module.backup_local(dest)
|
||||
write_changes(module, result[0], dest)
|
||||
|
||||
msg, changed = check_file_attrs(module, changed, msg)
|
||||
module.exit_json(changed=changed, msg=msg)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
main()
|
@ -1,152 +0,0 @@
|
||||
#!/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: stat
|
||||
version_added: "1.3"
|
||||
short_description: retrieve file or file system status
|
||||
description:
|
||||
- Retrieves facts for a file similar to the linux/unix 'stat' command.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The full path of the file/object to get the facts of
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
follow:
|
||||
description:
|
||||
- Whether to follow symlinks
|
||||
required: false
|
||||
default: no
|
||||
aliases: []
|
||||
get_md5:
|
||||
description:
|
||||
- Whether to return the md5 sum of the file
|
||||
required: false
|
||||
default: yes
|
||||
aliases: []
|
||||
author: Bruce Pennypacker
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Obtain the stats of /etc/foo.conf, and check that the file still belongs
|
||||
# to 'root'. Fail otherwise.
|
||||
- stat: path=/etc/foo.conf
|
||||
register: st
|
||||
- fail: msg="Whoops! file ownership has changed"
|
||||
when: st.stat.pw_name != 'root'
|
||||
|
||||
# Determine if a path exists and is a directory. Note we need to test
|
||||
# both that p.stat.isdir actually exists, and also that it's set to true.
|
||||
- stat: path=/path/to/something
|
||||
register: p
|
||||
- debug: msg="Path exists and is a directory"
|
||||
when: p.stat.isdir is defined and p.stat.isdir == true
|
||||
|
||||
# Don't do md5 checksum
|
||||
- stat: path=/path/to/myhugefile get_md5=no
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
from stat import *
|
||||
import pwd
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
path = dict(required=True),
|
||||
follow = dict(default='no', type='bool'),
|
||||
get_md5 = dict(default='yes', type='bool')
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
path = module.params.get('path')
|
||||
path = os.path.expanduser(path)
|
||||
follow = module.params.get('follow')
|
||||
get_md5 = module.params.get('get_md5')
|
||||
|
||||
try:
|
||||
if follow:
|
||||
st = os.stat(path)
|
||||
else:
|
||||
st = os.lstat(path)
|
||||
except OSError, e:
|
||||
if e.errno == errno.ENOENT:
|
||||
d = { 'exists' : False }
|
||||
module.exit_json(changed=False, stat=d)
|
||||
|
||||
module.fail_json(msg = e.strerror)
|
||||
|
||||
mode = st.st_mode
|
||||
|
||||
# back to ansible
|
||||
d = {
|
||||
'exists' : True,
|
||||
'mode' : "%04o" % S_IMODE(mode),
|
||||
'isdir' : S_ISDIR(mode),
|
||||
'ischr' : S_ISCHR(mode),
|
||||
'isblk' : S_ISBLK(mode),
|
||||
'isreg' : S_ISREG(mode),
|
||||
'isfifo' : S_ISFIFO(mode),
|
||||
'islnk' : S_ISLNK(mode),
|
||||
'issock' : S_ISSOCK(mode),
|
||||
'uid' : st.st_uid,
|
||||
'gid' : st.st_gid,
|
||||
'size' : st.st_size,
|
||||
'inode' : st.st_ino,
|
||||
'dev' : st.st_dev,
|
||||
'nlink' : st.st_nlink,
|
||||
'atime' : st.st_atime,
|
||||
'mtime' : st.st_mtime,
|
||||
'ctime' : st.st_ctime,
|
||||
'wusr' : bool(mode & stat.S_IWUSR),
|
||||
'rusr' : bool(mode & stat.S_IRUSR),
|
||||
'xusr' : bool(mode & stat.S_IXUSR),
|
||||
'wgrp' : bool(mode & stat.S_IWGRP),
|
||||
'rgrp' : bool(mode & stat.S_IRGRP),
|
||||
'xgrp' : bool(mode & stat.S_IXGRP),
|
||||
'woth' : bool(mode & stat.S_IWOTH),
|
||||
'roth' : bool(mode & stat.S_IROTH),
|
||||
'xoth' : bool(mode & stat.S_IXOTH),
|
||||
'isuid' : bool(mode & stat.S_ISUID),
|
||||
'isgid' : bool(mode & stat.S_ISGID),
|
||||
}
|
||||
|
||||
if S_ISLNK(mode):
|
||||
d['lnk_source'] = os.path.realpath(path)
|
||||
|
||||
if S_ISREG(mode) and get_md5 and os.access(path,os.R_OK):
|
||||
d['md5'] = module.md5(path)
|
||||
|
||||
|
||||
try:
|
||||
pw = pwd.getpwuid(st.st_uid)
|
||||
|
||||
d['pw_name'] = pw.pw_name
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
module.exit_json(changed=False, stat=d)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
main()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue