From 9027a9b02103f27a757ef1e06784cff72a791bfd Mon Sep 17 00:00:00 2001 From: Jesse Keating Date: Sat, 30 Mar 2013 07:38:42 -0700 Subject: [PATCH 1/3] Initial commit of rax library This library provides functionality for the Rackspace Public Cloud by way of the official pyrax SDK (https://github.com/rackspace/pyrax). At this time only the cloudservers service is functional. Instances can be created or deleted. Idempotency is provided on matching instances with the same name, flavor, image, and metadata values within a given region. pyrax usage does require a credentials file written out to hold username and API key. See pyrax documentation for details (https://github.com/rackspace/pyrax/blob/master/docs/pyrax_doc.md) --- library/rax | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 library/rax diff --git a/library/rax b/library/rax new file mode 100644 index 00000000000..7b8b42af3f1 --- /dev/null +++ b/library/rax @@ -0,0 +1,247 @@ +#!/usr/bin/env 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 . + +DOCUMENTATION = ''' +--- +module: rax +short_description: create an instance in Rackspace Public Cloud, return instanceid +description: + - creates Rackspace Public Cloud instances and optionally waits for it to be 'running'. +version_added: "1.2" +options: + service: + description: + - Cloud service to interact with + required: false + choices: ['cloudservers', 'cloudfiles', 'cloud_databases', 'cloud_loadbalancers'] + default: cloudservers + state: + description: + - Indicate desired state of the resource + required: false + choices: ['present', 'active', 'absent', 'deleted'] + default: present + creds_file: + description: + - File to find the Rackspace Public Cloud credentials in + required: false + default: null + name: + description: + - Name to give the instance + required: false + default: null + flavor: + description: + - flavor to use for the instance + required: false + default: null + image: + description: + - image to use for the instance + required: false + default: null + key_name: + description: + - key pair to use on the instance + required: false + default: null + aliases: ['keypair'] + region: + description: + - Region to create an instance in + required: false + default: null + wait: + description: + - wait for the instance to be in state 'running' before returning + required: false + default: "no" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 300 +examples: + - code: 'local_action: rax creds_file=~/.raxpub service=cloudservers name=rax-test1 flavor=5 image=b11d9567-e412-4255-96b9-bd63ab23bcfe wait=yes state=present' + description: "Examples from Ansible Playbooks" +requirements: [ "pyrax" ] +author: Jesse Keating +notes: + - Two environment variables can be used, RAX_CREDS and RAX_REGION. + - RAX_CREDS points to a credentials file appropriate for pyrax + - RAX_REGION defines a Rackspace Public Cloud region (DFW, ORD, LON, ...) +''' + +import sys +import time +import os + +try: + import pyrax +except ImportError: + print("failed=True msg='pyrax required for this module'") + sys.exit(1) + +SUPPORTEDSERVICES = ['cloudservers', 'cloudfiles', 'cloud_blockstorage', + 'cloud_databases', 'cloud_loadbalancers'] + +def cloudservers(module, state, name, flavor, image, meta, key_name, wait, + wait_timeout): + # Check our args (this could be done better) + for arg in (state, name, flavor, image): + if not arg: + module.fail_json(msg='%s is required for cloudservers' % arg) + + instances = [] + changed = False + servers = [] + # See if we can find servers that match our options + for server in pyrax.cloudservers.list(): + if name != server.name: + continue + if flavor != server.flavor['id']: + continue + if image != server.image['id']: + continue + if meta != server.metadata: + continue + # Nothing else ruled us not a match, so consider it a winner + servers.append(server) + + # act on the state + if state in ('active', 'present'): + # See if we already have any servers: + if not servers: + try: + servers = [pyrax.cloudservers.servers.create(name=name, + image=image, + flavor=flavor, + key_name=key_name, + meta=meta)] + changed = True + except Exception, e: + module.fail_json(msg = '%s' % e.message) + + for server in servers: + # 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.get() + if server.status in ('ACTIVE', 'ERROR'): + break + time.sleep(5) + if wait and wait_timeout <= time.time(): + # waiting took too long + module.fail_json(msg = 'Timeout waiting on %s' % server.id) + # Get a fresh copy of the server details + server.get() + if server.status == 'ERROR': + module.fail_json(msg = '%s failed to build' % server.id) + instance = {'id': server.id, + 'accessIPv4': server.accessIPv4, + 'name': server.name, + 'status': server.status} + instances.append(instance) + + elif state in ('absent', 'deleted'): + deleted = [] + # See if we can find a server that matches our credentials + for server in servers: + if server.name == name: + if server.flavor['id'] == flavor and \ + server.image['id'] == image and \ + server.metadata == meta: + try: + server.delete() + deleted.append(server) + except Exception, e: + module.fail_json(msg = e.message) + instance = {'id': server.id, + 'accessIPv4': server.accessIPv4, + 'name': server.name, + 'status': 'DELETING'} + instances.append(instance) + changed = True + + module.exit_json(changed=changed, instances=instances) + +def main(): + module = AnsibleModule( + argument_spec = dict( + service = dict(default='cloudservers', choices=SUPPORTEDSERVICES), + state = dict(default='present', choices=['active', 'present', + 'deleted', 'absent']), + creds_file = dict(), + name = dict(), + key_name = dict(aliases = ['keypair']), + flavor = dict(), + image = dict(), + meta = dict(type='dict', default={}), + region = dict(), + wait = dict(type='bool', choices=BOOLEANS), + wait_timeout = dict(default=300), + ) + ) + + service = module.params.get('service') + state = module.params.get('state') + creds_file = module.params.get('creds_file') + name = module.params.get('name') + key_name = module.params.get('key_name') + flavor = module.params.get('flavor') + image = module.params.get('image') + meta = module.params.get('meta') + region = module.params.get('region') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + + # Setup the credentials file + if not creds_file: + try: + creds_file = os.environ['RAX_CREDS_FILE'] + except KeyError, e: + module.fail_json(msg = 'Unable to load %s' % e.message) + + # Define the region + if not region: + try: + region = os.environ['RAX_REGION'] + except KeyError, e: + module.fail_json(msg = 'Unable to load %s' % e.message) + + # setup the auth + sys.stderr.write('region is %s' % region) + try: + pyrax.set_credential_file(creds_file, region=region) + except Exception, e: + module.fail_json(msg = '%s' % e.message) + + # Act based on service + if service == 'cloudservers': + cloudservers(module, state, name, flavor, image, meta, key_name, wait, + wait_timeout) + elif service in ['cloudfiles', 'cloud_blockstorage', + 'cloud_databases', 'cloud_loadbalancers']: + module.fail_json(msg = 'Service %s is not supported at this time' % + service) + + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From c47fd199bd1be9d3b3b36b2eaea35a9f1e6aa1ba Mon Sep 17 00:00:00 2001 From: Jesse Keating Date: Sat, 30 Mar 2013 07:39:08 -0700 Subject: [PATCH 2/3] Initial commit of rax inventory plugin The rax inventory plugin provides a way to discovery inventory in the Rackspace Public Cloud by way of pyrax, the official SDK. Grouping will be done if a group:name is found in the instance metadata. When a single host is queried all the instance details are returned with a rax_ prefix. Because inventory plugins cannot take extra arguments, ENV variables must be set to point to the pyrax compatible credentials file and the region to query against. --- plugins/inventory/rax.py | 156 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100755 plugins/inventory/rax.py diff --git a/plugins/inventory/rax.py b/plugins/inventory/rax.py new file mode 100755 index 00000000000..283486ef141 --- /dev/null +++ b/plugins/inventory/rax.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +# (c) 2013, Jesse Keating +# +# 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 . + +DOCUMENTATION = ''' +--- +inventory: rax +short_description: Rackspace Public Cloud external inventory script +description: + - Generates inventory that Ansible can understand by making API request to Rackspace Public Cloud API + - | + When run against a specific host, this script returns the following variables: + rax_os-ext-sts_task_state + rax_addresses + rax_links + rax_image + rax_os-ext-sts_vm_state + rax_flavor + rax_id + rax_rax-bandwidth_bandwidth + rax_user_id + rax_os-dcf_diskconfig + rax_accessipv4 + rax_accessipv6 + rax_progress + rax_os-ext-sts_power_state + rax_metadata + rax_status + rax_updated + rax_hostid + rax_name + rax_created + rax_tenant_id + rax__loaded + + where some item can have nested structure. + - credentials are set in a credentials file +version_added: None +options: + creds_file: + description: + - File to find the Rackspace Public Cloud credentials in + required: true + default: null + region_name: + description: + - Region name to use in request + required: false + default: DFW +author: Jesse Keating +notes: + - Two environment variables need to be set, RAX_CREDS and RAX_REGION. + - RAX_CREDS points to a credentials file appropriate for pyrax + - RAX_REGION defines a Rackspace Public Cloud region (DFW, ORD, LON, ...) +requirements: [ "pyrax" ] +examples: + - description: List server instances + code: RAX_CREDS=~/.raxpub RAX_REGION=ORD rax.py --list + - description: List server instance properties + code: RAX_CREDS=~/.raxpub RAX_REGION=ORD rax.py --host +''' + +import sys +import re +import os +import argparse + +try: + import json +except: + import simplejson as json + +try: + import pyrax +except ImportError: + print('pyrax required for this module') + sys.exit(1) + +# Setup the parser +parser = argparse.ArgumentParser(description='List active instances', + epilog='List by itself will list all the active \ + instances. Listing a specific instance will show \ + all the details about the instance.') + +parser.add_argument('--list', action='store_true', default=True, + help='List active servers') +parser.add_argument('--host', + help='List details about the specific host (IP address)') + +args = parser.parse_args() + +# setup the auth +try: + creds_file = os.environ['RAX_CREDS_FILE'] + region = os.environ['RAX_REGION'] +except KeyError, e: + sys.stderr.write('Unable to load %s\n' % e.message) + sys.exit(1) + +try: + pyrax.set_credential_file(os.path.expanduser(creds_file), + region=region) +except Exception, e: + sys.stderr.write("%s: %s\n" % (e, e.message)) + sys.exit(1) + +# Execute the right stuff +if not args.host: + groups = {} + + # Cycle on servers + for server in pyrax.cloudservers.list(): + # Define group (or set to empty string) + try: + group = server.metadata['group'] + except KeyError: + group = 'undefined' + + # Create group if not exist and add the server + groups.setdefault(group, []).append(server.accessIPv4) + + # Return server list + print(json.dumps(groups)) + sys.exit(0) + +# Get the deets for the instance asked for +results = {} +# This should be only one, but loop anyway +for server in pyrax.cloudservers.list(): + if server.accessIPv4 == args.host: + for key in [key for key in vars(server) if + key not in ('manager', '_info')]: + # Extract value + value = getattr(server, key) + + # Generate sanitized key + key = 'rax_' + re.sub("[^A-Za-z0-9\-]", "_", key).lower() + results[key] = value + +print(json.dumps(results)) +sys.exit(0) From 34e585024c17c7480c46075b9fc5f0b6abe18f72 Mon Sep 17 00:00:00 2001 From: Jesse Keating Date: Sun, 31 Mar 2013 00:05:14 -0700 Subject: [PATCH 3/3] Fix up docs and add ability to insert files Files can be inserted during server creation (like a fully formed authorized_keys file). This code allows that to happen. Docs were updated for formatting, location, and to add the new entry for files. --- library/rax | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/library/rax b/library/rax index 7b8b42af3f1..2bdc1638249 100644 --- a/library/rax +++ b/library/rax @@ -46,34 +46,42 @@ options: default: null flavor: description: - - flavor to use for the instance + - flavor to use for the instance required: false default: null image: description: - - image to use for the instance + - image to use for the instance required: false default: null + meta: + description: + - A hash of metadata to associate with the instance + default: null key_name: description: - - key pair to use on the instance + - key pair to use on the instance required: false default: null aliases: ['keypair'] + files: + description: + - Files to insert into the instance. remotefilename:localcontent + default: null region: description: - - Region to create an instance in + - Region to create an instance in required: false default: null wait: description: - - wait for the instance to be in state 'running' before returning + - wait for the instance to be in state 'running' before returning required: false default: "no" choices: [ "yes", "no" ] wait_timeout: description: - - how long before wait gives up, in seconds + - how long before wait gives up, in seconds default: 300 examples: - code: 'local_action: rax creds_file=~/.raxpub service=cloudservers name=rax-test1 flavor=5 image=b11d9567-e412-4255-96b9-bd63ab23bcfe wait=yes state=present' @@ -99,8 +107,8 @@ except ImportError: SUPPORTEDSERVICES = ['cloudservers', 'cloudfiles', 'cloud_blockstorage', 'cloud_databases', 'cloud_loadbalancers'] -def cloudservers(module, state, name, flavor, image, meta, key_name, wait, - wait_timeout): +def cloudservers(module, state, name, flavor, image, meta, key_name, files, + wait, wait_timeout): # Check our args (this could be done better) for arg in (state, name, flavor, image): if not arg: @@ -124,14 +132,22 @@ def cloudservers(module, state, name, flavor, image, meta, key_name, wait, # act on the state if state in ('active', 'present'): - # See if we already have any servers: if not servers: + # Handle the file contents + for rpath in files.keys(): + lpath = os.path.expanduser(files[rpath]) + try: + fileobj = open(lpath, 'r') + files[rpath] = fileobj + except Exception, e: + module.fail_json(msg = 'Failed to load %s' % lpath) try: servers = [pyrax.cloudservers.servers.create(name=name, image=image, flavor=flavor, key_name=key_name, - meta=meta)] + meta=meta, + files=files)] changed = True except Exception, e: module.fail_json(msg = '%s' % e.message) @@ -188,10 +204,11 @@ def main(): 'deleted', 'absent']), creds_file = dict(), name = dict(), - key_name = dict(aliases = ['keypair']), flavor = dict(), image = dict(), meta = dict(type='dict', default={}), + key_name = dict(aliases = ['keypair']), + files = dict(type='dict', default={}), region = dict(), wait = dict(type='bool', choices=BOOLEANS), wait_timeout = dict(default=300), @@ -202,10 +219,11 @@ def main(): state = module.params.get('state') creds_file = module.params.get('creds_file') name = module.params.get('name') - key_name = module.params.get('key_name') flavor = module.params.get('flavor') image = module.params.get('image') meta = module.params.get('meta') + key_name = module.params.get('key_name') + files = module.params.get('files') region = module.params.get('region') wait = module.params.get('wait') wait_timeout = int(module.params.get('wait_timeout')) @@ -233,8 +251,8 @@ def main(): # Act based on service if service == 'cloudservers': - cloudservers(module, state, name, flavor, image, meta, key_name, wait, - wait_timeout) + cloudservers(module, state, name, flavor, image, meta, key_name, files, + wait, wait_timeout) elif service in ['cloudfiles', 'cloud_blockstorage', 'cloud_databases', 'cloud_loadbalancers']: module.fail_json(msg = 'Service %s is not supported at this time' %