Last active
August 29, 2015 14:13
-
-
Save bbhoss/82e821252ffc1b0194c3 to your computer and use it in GitHub Desktop.
Joyent/SDC Ansible Module (WIP, not fully functional yet). I am not a Pythonista
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env 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: sdc | |
short_description: create or terminate a virtualmachine or smartmachine in SDC | |
description: | |
- Creates or terminates SDC instances. When created optionally waits for it to be 'running'. This module has a dependency on smartdc >= 0.2.0 which is available through pip | |
options: | |
machine_id: | |
description: | |
- id of machine. Used only for termination | |
name: | |
description: | |
- Friendly name for this machine; default is a randomly generated name | |
required: false | |
default: null | |
location: | |
description: | |
- hostname or fqdn for the SDC location you wish to use | |
required: true | |
default: us-east-1 | |
account: | |
description: | |
- sdc account name | |
required: true | |
default: null | |
key_id: | |
description: | |
- The fingerprint of an SSH public key that has been added to the account set in account | |
required: true | |
default: null | |
secret_key_path: | |
description: | |
- Path to the private key that corresponds to key_id | |
required: false | |
default: ~/.ssh/id_rsa | |
image: | |
description: | |
- The image UUID to use for this machine | |
required: true | |
default: null | |
package: | |
description: | |
- UUID of the package to use on provisioning | |
required: false | |
networks: | |
description: | |
- Comma separated list of desired networks ids, obtained from ListNetworks | |
required: false | |
count: | |
description: | |
- An integer value which indicates how many instances that should be created. Only used if count_tag or exact_count are missing (not recommended). | |
required: false | |
default: 1 | |
aliases: [] | |
exact_count: | |
description: | |
- An integer value which indicates how many instances that match the 'count_tag' parameter should be running. | |
required: false | |
default: null | |
aliases: [] | |
count_tag: | |
description: | |
- Used with 'exact_count' to determine how many nodes based on a specific tag criteria should be running. This can be expressed in multiple ways and is shown in the EXAMPLES section. For instance, one can request 25 servers that are tagged with "class=webserver". Count tag(s) will always be added to created machines | |
required: false | |
default: null | |
aliases: [] | |
tags: | |
description: | |
- Tags to assign to this instance. | |
required: false | |
default: null | |
aliases: [] | |
wait: | |
description: | |
- wait for the instance to be in state 'running' before returning | |
required: false | |
default: "yes" | |
choices: [ "yes", "no" ] | |
aliases: [] | |
wait_timeout: | |
description: | |
- how long before wait gives up, in seconds | |
default: 600 | |
aliases: [] | |
state: | |
description: | |
- create or terminate instances | |
required: false | |
default: 'present' | |
aliases: [] | |
requirements: [ "sdc" ] | |
author: Preston Marshall | |
''' | |
EXAMPLES = ''' | |
# Provision single machine example | |
- local_action: | |
module: sdc | |
account: myaccount | |
key_id: /myaccount/keys/mykey | |
name: my-virtual-machine | |
package: 486bb054-6a97-4ba3-97b7-2413d5f8e849 | |
image: 62f148f8-6e84-11e4-82c5-efca60348b9f | |
location: 'us-east-1.api.joyentcloud.com' | |
# Ensure 5 machines with tag "webserver" are running, does not destroy existing machines | |
- local_action: | |
module: sdc | |
account: myaccount | |
key_id: /myaccount/keys/mykey | |
package: 486bb054-6a97-4ba3-97b7-2413d5f8e849 | |
image: 62f148f8-6e84-11e4-82c5-efca60348b9f | |
location: 'us-east-1.api.joyentcloud.com' | |
count_tag: | |
role: webserver | |
exact_count: 5 | |
# Terminate machines by their tags | |
- local_action: | |
module: sdc | |
account: myaccount | |
key_id: /myaccount/keys/mykey | |
location: 'us-east-1.api.joyentcloud.com' | |
tags: | |
role: webserver | |
state: absent | |
# Terminate machine by its id | |
- local_action: | |
module: sdc | |
account: myaccount | |
key_id: /myaccount/keys/mykey | |
machine_id: 51307f2f-02a3-4bdc-9b38-2f263eb43e2d | |
location: 'us-east-1.api.joyentcloud.com' | |
state: absent | |
''' | |
import os | |
import sys | |
import time | |
try: | |
import smartdc | |
from smartdc import DataCenter | |
except ImportError: | |
print "failed=True msg='smartdc required for this module'" | |
sys.exit(1) | |
import json | |
def _wait_for_status(module, machines, status): | |
wait_timeout = int(module.params.get('wait_timeout')) | |
wait_timeout_time = time.time() + wait_timeout | |
while not all( sm.status() == status for sm in machines ) and wait_timeout_time > time.time(): | |
time.sleep(2) | |
# Check again that all are in expected state. If not, we timed out | |
if not all( sm.status() == status for sm in machines ): | |
module.fail_json(msg="Timed out creating machines") | |
def list_existing_machines(sdc, tags): | |
assert tags, "Must have tags to list existing machines for termination/counting" | |
machines = sdc.machines(tags=tags, state='running') | |
return machines | |
def create_virtual_machine(module, sdc): | |
""" | |
Create new virtual machine | |
module : AnsibleModule object | |
sdc: authenticated sdc Datacenter object | |
Returns: | |
True if a new machine was created, false otherwise | |
""" | |
name = module.params.get('name') | |
package = module.params.get('package') | |
image = module.params.get('image') | |
networks = module.params.get('networks') | |
wait = module.params.get('wait') | |
wait_timeout = int(module.params.get('wait_timeout')) | |
tags = module.params.get('tags') | |
exact_count = int(module.params.get('exact_count')) | |
count_tag = module.params.get('count_tag') | |
count = int(module.params.get('count')) | |
changed = False | |
created_machines = [] | |
existing_machines = [] | |
full_tags = dict(tags.items() + count_tag.items()) #Force count_tag(s) to be a part of the instance tags to prevent user error | |
# Get existing machines if we're using exact_count | |
if count_tag and exact_count: | |
existing_machines = list_existing_machines(sdc, count_tag) | |
count_needed = exact_count - len(existing_machines) | |
else: | |
count_needed = count | |
#Make sure we actually need to create machines, otherwise we can just give info about the existing ones | |
if count_needed > 0: | |
changed = True | |
# Batch create, we'll wait later | |
for x in range(count_needed): | |
sm = sdc.create_machine(name=name, package=package, image=image, | |
networks=networks, tags=full_tags) | |
created_machines.append(sm) | |
# Wait on machines to be running if requested | |
if wait: | |
_wait_for_status(module, created_machines, 'running') | |
all_machines = existing_machines + created_machines | |
return changed, [sdc.raw_machine_data(sm.id) for sm in all_machines] | |
def terminate_machines(module, sdc): | |
""" | |
Terminates machines | |
module : AnsibleModule object | |
sdc: authenticated sdc Datacenter object | |
Returns: | |
True if machines were deleted, false otherwise | |
""" | |
machines = [] | |
if module.params.get('tags'): | |
machines = sdc.machines(tags=module.params.get('tags')) | |
elif module.params.get('machine_id'): | |
machines = [sdc.machine(module.params.get('machine_id'))] | |
changed = False | |
if machines: | |
changed = True | |
# Stop machines first, as the API docs say it is required, even though calling delete | |
# works fine without stopping first. Stopping first also ensures that we avoid races with | |
# the count_tag/exact_count functionality. Just deleting instances leaves them in state 'running' | |
# for a bit which could cause subtle issues | |
[sm.stop() for sm in machines] | |
_wait_for_status(module, machines, 'stopped') | |
[sm.delete() for sm in machines] | |
return changed | |
def get_sdc_creds(module): | |
# Check module args for credentials, then check environment vars | |
# TODO: Figure out how to pull from local environment variables here | |
account = module.params.get('account') | |
if not account: | |
module.fail_json(msg="No account provided") | |
key_id = module.params.get('key_id') | |
if not key_id: | |
module.fail_json(msg="No key_id provided") | |
return account, key_id | |
def main(): | |
module = AnsibleModule( | |
argument_spec=dict( | |
machine_id=dict(), | |
name=dict(), | |
location=dict(), | |
account=dict(), | |
key_id=dict(), | |
image=dict(), | |
package=dict(), | |
tags=dict(type='dict',default={}), | |
count=dict(type='int', default=1), | |
count_tag=dict(type='dict',default={}), | |
exact_count=dict(type='int'), | |
networks=dict(type='list'), | |
secret_key_path=dict(default='~/.ssh/id_rsa'), | |
state=dict(default='present'), | |
wait=dict(type='bool', default=True), | |
wait_timeout=dict(default=600,type='int') | |
) | |
) | |
# get credentials for creating a Datacenter object | |
account, key_id = get_sdc_creds(module) | |
# sdc top-level Datacenter object | |
sdc = DataCenter(location=module.params.get('location'), key_id=key_id, login=account, secret=module.params.get('secret_key_path')) | |
machines = [] | |
if module.params.get('state') == 'absent': | |
changed = terminate_machines(module, sdc) | |
elif module.params.get('state') == 'present': | |
# Changed is always set to true when provisioning new instances | |
if not module.params.get('image'): | |
module.fail_json(msg='image parameter is required for new instance') | |
(changed, machines) = create_virtual_machine(module, sdc) | |
module.exit_json(changed=changed, machines=json.loads(json.dumps(machines))) | |
# import module snippets | |
from ansible.module_utils.basic import * | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment