From 9c26b9ddcefc5ee1dff58ed0e7b6807e8b53711b Mon Sep 17 00:00:00 2001 From: Vincent Viallet Date: Fri, 21 Jun 2013 13:41:33 +0800 Subject: [PATCH 1/5] Add linode cloud provider. --- library/cloud/linode | 426 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 library/cloud/linode diff --git a/library/cloud/linode b/library/cloud/linode new file mode 100644 index 00000000000..99503646119 --- /dev/null +++ b/library/cloud/linode @@ -0,0 +1,426 @@ +#!/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 / 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: + service: + description: + - Cloud service to interact with + choices: ['cloudservers'] + default: cloudservers + state: + description: + - Indicate desired state of the resource + choices: ['present', 'active', 'absent', 'deleted'] + default: present + creds_file: + description: + - File to find the Rackspace Public Cloud credentials in + default: null + name: + description: + - Name to give the instance + default: null + flavor: + description: + - flavor to use for the instance + default: null + image: + description: + - image to use for the instance + 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 + default: null + aliases: ['keypair'] + files: + description: + - Files to insert into the instance. remotefilename:localcontent + default: null + region: + description: + - Region to create an instance in + default: null + 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: [ "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, ...) +''' + +EXAMPLES = ''' +# Create a server +- local_action: + module: rax + creds_file: ~/.raxpub + service: cloudservers + name: rax-test1 + flavor: 5 + image: b11d9567-e412-4255-96b9-bd63ab23bcfe + wait: yes + state: present +''' + +import sys +import time +import os + +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 + 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 not instance['ipv4']: + 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, flavor, image, region, linode_id, + payment_term, password, ssh_pub_key, swap, 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 + 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) + 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 flavor / image / region 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: + new_server = True + # TODO - improve + for arg in (name, flavor, image, region): + if not arg: + module.fail_json(msg='%s is required for active state' % arg) + # Create linode entity + try: + res = api.linode_create(DatacenterID=region, PlanID=flavor, + PaymentTerm=payment_term) + linode_id = res['LinodeID'] + # Update linode Label to match name + api.linode_update(LinodeId=linode_id, Label=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: + new_server = True + # TODO - improve + for arg in (linode_id, name, image): + if not arg: + module.fail_json(msg='%s is required for active state' % arg) + # Create disks (1 from distrib, 1 for SWAP) + 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=image, + rootPass=password, rootSSHKey=ssh_pub_key, + Label='%s data disk' % name, Size=size) + else: + res = api.linode_disk_createfromdistribution( + LinodeId=linode_id, DistributionID=image, + rootPass=password, Label='%s data disk' % name, + Size=size) + jobs.append(res['JobID']) + # Create SWAP disk + res = api.linode_disk_create(LinodeId=linode_id, Type='swap', + Label='%s swap disk' % name, Size=swap) + jobs.append(res['JobID']) + except Exception, e: + # TODO: destroy linode ? + module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE']) + + if not configs: + new_server = True + # TODO - improve + for arg in (linode_id, name, image): + if not arg: + module.fail_json(msg='%s is required for active state' % arg) + # Check architecture + for distrib in api.avail_distributions(): + if distrib['DISTRIBUTIONID'] != image: + 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(): + if disk['TYPE'] == 'ext3': + disks_id.insert(0, disk['DISKID']) + continue + disks_id.append(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 + try: + res = api.linode_config_create(LinodeId=linode_id, KernelId=kernel_id, + Disklist=disks_list, Label='%s config' % name) + config_id = res['ConfigId'] + 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 - %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 - %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 (linode_id, name): + if not arg: + module.fail_json(msg='%s is required for stopped state' % arg) + if not servers: + module.fail_json(msg = 'Server %s (%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 ('linode_id', 'name'): + if not arg: + module.fail_json(msg='%s is required for restarted state' % arg) + if not servers: + module.fail_json(msg = 'Server %s (%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']), + api_key = dict(), + name = dict(type='str'), + flavor = dict(type='int'), + image = dict(type='int'), + region = dict(type='int'), + linode_id = dict(type='int'), + payment_term = dict(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', choices=BOOLEANS, default=True), + wait_timeout = dict(default=500), + ) + ) + + state = module.params.get('state') + api_key = module.params.get('api_key') + name = module.params.get('name') + flavor = module.params.get('flavor') + image = module.params.get('image') + region = module.params.get('region') + 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, flavor, image, region, linode_id, + payment_term, password, ssh_pub_key, swap, wait, wait_timeout) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 17a55d6adf19a917912487c7d3a3d20b7c4d47f7 Mon Sep 17 00:00:00 2001 From: Vincent Viallet Date: Fri, 21 Jun 2013 13:59:09 +0800 Subject: [PATCH 2/5] Update documentation --- library/cloud/linode | 123 +++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/library/cloud/linode b/library/cloud/linode index 99503646119..01e2aeadc98 100644 --- a/library/cloud/linode +++ b/library/cloud/linode @@ -16,25 +16,20 @@ DOCUMENTATION = ''' --- -module: rax -short_description: create / delete an instance in Rackspace Public Cloud +module: linode +short_description: create / delete / stop / restart an instance in Linode Public Cloud description: - - creates / deletes a Rackspace Public Cloud instance and optionally waits for it to be 'running'. -version_added: "1.2" + - creates / deletes a Linode Public Cloud instance and optionally waits for it to be 'running'. +version_added: "1.3" options: - service: - description: - - Cloud service to interact with - choices: ['cloudservers'] - default: cloudservers state: description: - Indicate desired state of the resource - choices: ['present', 'active', 'absent', 'deleted'] + choices: ['present', 'active', 'started', absent', 'deleted', 'stopped', 'restarted'] default: present - creds_file: + api_key: description: - - File to find the Rackspace Public Cloud credentials in + - Linode API key default: null name: description: @@ -42,29 +37,40 @@ options: default: null flavor: description: - - flavor to use for the instance + - flavor to use for the instance (Linode plan) default: null - image: + type: integer + payment_term: description: - - image to use for the instance - default: null - meta: + - payment term to use for the instance (payment term in months) + default: 1 + type: integer + choices: [1, 12, 24] + password: description: - - A hash of metadata to associate with the instance + - root password to apply to a new server (auto generated if missing) default: null - key_name: + type: string + ssh_pub_key: description: - - key pair to use on the instance + - SSH public key applied to root user default: null - aliases: ['keypair'] - files: + type: string + swap: + description: + - swap size in MB + default: 512 + type: integer + image: description: - - Files to insert into the instance. remotefilename:localcontent + - image to use for the instance (Linode Distribution) default: null + type: integer region: description: - - Region to create an instance in + - Region to create an instance in (Linode Datacenter) default: null + type: integer wait: description: - wait for the instance to be in state 'running' before returning @@ -74,25 +80,67 @@ options: description: - how long before wait gives up, in seconds default: 300 -requirements: [ "pyrax" ] -author: Jesse Keating +requirements: [ "linode-python" ] +author: Vincent Viallet 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, ...) + - LINODE_API_KEY env variable can be used instead ''' EXAMPLES = ''' # Create a server - local_action: - module: rax - creds_file: ~/.raxpub - service: cloudservers - name: rax-test1 - flavor: 5 - image: b11d9567-e412-4255-96b9-bd63ab23bcfe + module: linode + api_key: 'longStringFromLinodeApi' + name: linode-test1 + flavor: 1 + region: 2 + image: 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 + flavor: 1 + region: 2 + image: 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 @@ -148,11 +196,6 @@ def getInstanceDetails(api, server): def linodeServers(module, api, state, name, flavor, image, region, linode_id, payment_term, password, ssh_pub_key, swap, 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 new_server = False From 2eaf5ee206359368cf18187d4e926f8032fb6dc4 Mon Sep 17 00:00:00 2001 From: Vincent Viallet Date: Fri, 21 Jun 2013 17:01:29 +0800 Subject: [PATCH 3/5] Fix last bugs, complete all tests. --- library/cloud/linode | 162 +++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 66 deletions(-) diff --git a/library/cloud/linode b/library/cloud/linode index 01e2aeadc98..11ab11a4b50 100644 --- a/library/cloud/linode +++ b/library/cloud/linode @@ -1,4 +1,4 @@ -#!/usr/bin/env python -tt +#!/usr/bin/python # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -33,11 +33,19 @@ options: default: null name: description: - - Name to give the instance + - Name to give the instance (alphanumeric, dashes, underscore) + - To keep sanity on the Linode Web Console, name is prepended with LinodeID_ default: null - flavor: + type: string + linode_id: description: - - flavor to use for the instance (Linode plan) + - 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: @@ -61,14 +69,14 @@ options: - swap size in MB default: 512 type: integer - image: + distribution: description: - - image to use for the instance (Linode Distribution) + - distribution to use for the instance (Linode Distribution) default: null type: integer - region: + datacenter: description: - - Region to create an instance in (Linode Datacenter) + - datacenter to create an instance in (Linode Datacenter) default: null type: integer wait: @@ -92,9 +100,9 @@ EXAMPLES = ''' module: linode api_key: 'longStringFromLinodeApi' name: linode-test1 - flavor: 1 - region: 2 - image: 99 + plan: 1 + datacenter: 2 + distribution: 99 password: 'superSecureRootPassword' ssh_pub_key: 'ssh-rsa qwerty' swap: 768 @@ -108,9 +116,9 @@ EXAMPLES = ''' api_key: 'longStringFromLinodeApi' name: linode-test1 linode_id: 12345678 - flavor: 1 - region: 2 - image: 99 + plan: 1 + datacenter: 2 + distribution: 99 password: 'superSecureRootPassword' ssh_pub_key: 'ssh-rsa qwerty' swap: 768 @@ -148,6 +156,9 @@ import time import os try: + # linode module raise warning due to ssl - silently ignore them ... + import warnings + warnings.simplefilter("ignore") from linode import api as linode_api except ImportError: print("failed=True msg='linode-python required for this module'") @@ -181,7 +192,7 @@ def getInstanceDetails(api, server): # Populate with ips for ip in api.linode_ip_list(LinodeId=server['LINODEID']): - if ip['ISPUBLIC'] and not instance['ipv4']: + if ip['ISPUBLIC'] and 'ipv4' not in instance: instance['ipv4'] = ip['IPADDRESS'] instance['fqdn'] = ip['RDNS_NAME'] if ip['ISPUBLIC']: @@ -194,7 +205,7 @@ def getInstanceDetails(api, server): 'ip_id': ip['IPADDRESSID']}) return instance -def linodeServers(module, api, state, name, flavor, image, region, linode_id, +def linodeServers(module, api, state, name, plan, distribution, datacenter, linode_id, payment_term, password, ssh_pub_key, swap, wait, wait_timeout): instances = [] changed = False @@ -209,12 +220,15 @@ def linodeServers(module, api, state, name, flavor, image, region, 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) - disks = api.linode_disk_list(LinodeId=linode_id) - configs = api.linode_config_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 flavor / image / region are valid + # TODO: validate all the plan / distribution / datacenter are valid # Multi step process/validation: # - need linode_id (entity) @@ -225,16 +239,22 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, if not servers: new_server = True # TODO - improve - for arg in (name, flavor, image, region): - if not arg: - module.fail_json(msg='%s is required for active state' % arg) + if not name: + module.fail_json(msg='%s is required for active state' % 'name') + if not plan: + module.fail_json(msg='%s is required for active state' % 'plan') + if not distribution: + module.fail_json(msg='%s is required for active state' % 'distribution') + if not datacenter: + module.fail_json(msg='%s is required for active state' % 'datacenter') + # Create linode entity try: - res = api.linode_create(DatacenterID=region, PlanID=flavor, + 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=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: @@ -243,9 +263,13 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, if not disks: new_server = True # TODO - improve - for arg in (linode_id, name, image): - if not arg: - module.fail_json(msg='%s is required for active state' % arg) + if not name: + module.fail_json(msg='%s is required for active state' % 'name') + if not linode_id: + module.fail_json(msg='%s is required for active state' % 'linode_id') + if not distribution: + module.fail_json(msg='%s is required for active state' % 'distribution') + # Create disks (1 from distrib, 1 for SWAP) try: if not password: @@ -257,18 +281,18 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, size = servers[0]['TOTALHD'] - swap if ssh_pub_key: res = api.linode_disk_createfromdistribution( - LinodeId=linode_id, DistributionID=image, + LinodeId=linode_id, DistributionID=distribution, rootPass=password, rootSSHKey=ssh_pub_key, - Label='%s data disk' % name, Size=size) + Label='%s data disk (lid: %s)' % (name, linode_id), Size=size) else: res = api.linode_disk_createfromdistribution( - LinodeId=linode_id, DistributionID=image, - rootPass=password, Label='%s data disk' % name, - Size=size) + 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' % name, Size=swap) + Label='%s swap disk (lid: %s)' % (name, linode_id), + Size=swap) jobs.append(res['JobID']) except Exception, e: # TODO: destroy linode ? @@ -277,12 +301,16 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, if not configs: new_server = True # TODO - improve - for arg in (linode_id, name, image): - if not arg: - module.fail_json(msg='%s is required for active state' % arg) + if not name: + module.fail_json(msg='%s is required for active state' % 'name') + if not linode_id: + module.fail_json(msg='%s is required for active state' % 'linode_id') + if not distribution: + module.fail_json(msg='%s is required for active state' % 'distribution') + # Check architecture for distrib in api.avail_distributions(): - if distrib['DISTRIBUTIONID'] != image: + if distrib['DISTRIBUTIONID'] != distribution: continue arch = '32' if distrib['IS64BIT']: @@ -298,11 +326,11 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, # Get disk list disks_id = [] - for disk in api.linode_disk_list(): + for disk in api.linode_disk_list(LinodeId=linode_id): if disk['TYPE'] == 'ext3': - disks_id.insert(0, disk['DISKID']) + disks_id.insert(0, str(disk['DISKID'])) continue - disks_id.append(disk['DISKID']) + disks_id.append(str(disk['DISKID'])) # Trick to get the 9 items in the list while len(disks_id) < 9: disks_id.append('') @@ -310,9 +338,8 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, # Create config try: - res = api.linode_config_create(LinodeId=linode_id, KernelId=kernel_id, - Disklist=disks_list, Label='%s config' % name) - config_id = res['ConfigId'] + 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']) @@ -340,12 +367,12 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, time.sleep(5) if wait and wait_timeout <= time.time(): # waiting took too long - module.fail_json(msg = 'Timeout waiting on %s - %s' % + 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 - %s failed to boot' % + 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 @@ -362,12 +389,13 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, instance['password'] = password instances.append(instance) elif state in ('stopped'): - for arg in (linode_id, name): - if not arg: - module.fail_json(msg='%s is required for stopped state' % arg) + if not name: + module.fail_json(msg='%s is required for stopped state' % 'name') + if not linode_id: + module.fail_json(msg='%s is required for stopped state' % 'linode_id') + if not servers: - module.fail_json(msg = 'Server %s (%s) not found' % - (name, linode_id)) + 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: @@ -381,12 +409,13 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, instance['status'] = 'Stopped' instances.append(instance) elif state in ('restarted'): - for arg in ('linode_id', 'name'): - if not arg: - module.fail_json(msg='%s is required for restarted state' % arg) + if not name: + module.fail_json(msg='%s is required for restarted state' % 'name') + if not linode_id: + module.fail_json(msg='%s is required for restarted state' % 'linode_id') + if not servers: - module.fail_json(msg = 'Server %s (%s) not found' % - (name, linode_id)) + module.fail_json(msg = 'Server %s (lid: %s) not found' % (name, linode_id)) for server in servers: instance = getInstanceDetails(api, server) try: @@ -415,18 +444,19 @@ def linodeServers(module, api, state, name, flavor, image, region, linode_id, def main(): module = AnsibleModule( argument_spec = dict( - state = dict(default='present', choices=['active', 'present', 'started' - 'deleted', 'absent', 'stopped']), + state = dict(default='present', choices=['active', 'present', 'started', + 'deleted', 'absent', 'stopped', + 'restarted']), api_key = dict(), name = dict(type='str'), - flavor = dict(type='int'), - image = dict(type='int'), - region = dict(type='int'), - linode_id = dict(type='int'), + plan = dict(type='int'), + distribution = dict(type='int'), + datacenter = dict(type='int'), + linode_id = dict(type='int', aliases=['lid']), payment_term = dict(default=1, choices=[1, 12, 24]), password = dict(type='str'), ssh_pub_key = dict(type='str'), - swap = dict(type='int', default=512) + swap = dict(type='int', default=512), wait = dict(type='bool', choices=BOOLEANS, default=True), wait_timeout = dict(default=500), ) @@ -435,9 +465,9 @@ def main(): state = module.params.get('state') api_key = module.params.get('api_key') name = module.params.get('name') - flavor = module.params.get('flavor') - image = module.params.get('image') - region = module.params.get('region') + 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') @@ -460,7 +490,7 @@ def main(): except Exception, e: module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE']) - linodeServers(module, api, state, name, flavor, image, region, linode_id, + linodeServers(module, api, state, name, plan, distribution, datacenter, linode_id, payment_term, password, ssh_pub_key, swap, wait, wait_timeout) # this is magic, see lib/ansible/module_common.py From c124411c367061ae1c30e4d0000863be3582f719 Mon Sep 17 00:00:00 2001 From: Vincent Viallet Date: Fri, 21 Jun 2013 18:03:55 +0900 Subject: [PATCH 4/5] Consistent wait_timeout between doc and module --- library/cloud/linode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/cloud/linode b/library/cloud/linode index 11ab11a4b50..2bf2cd6e242 100644 --- a/library/cloud/linode +++ b/library/cloud/linode @@ -458,7 +458,7 @@ def main(): ssh_pub_key = dict(type='str'), swap = dict(type='int', default=512), wait = dict(type='bool', choices=BOOLEANS, default=True), - wait_timeout = dict(default=500), + wait_timeout = dict(default=300), ) ) From 7b0b9016b59458f0853f939b11bed8cf34dda6d6 Mon Sep 17 00:00:00 2001 From: Vincent Viallet Date: Fri, 21 Jun 2013 21:41:00 +0800 Subject: [PATCH 5/5] Improve error messages on missing required variables. --- library/cloud/linode | 59 +++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/library/cloud/linode b/library/cloud/linode index 2bf2cd6e242..2116123bf92 100644 --- a/library/cloud/linode +++ b/library/cloud/linode @@ -237,18 +237,11 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino # Any create step triggers a job that need to be waited for. if not servers: - new_server = True - # TODO - improve - if not name: - module.fail_json(msg='%s is required for active state' % 'name') - if not plan: - module.fail_json(msg='%s is required for active state' % 'plan') - if not distribution: - module.fail_json(msg='%s is required for active state' % 'distribution') - if not datacenter: - module.fail_json(msg='%s is required for active state' % 'datacenter') - + 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) @@ -261,16 +254,11 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE']) if not disks: - new_server = True - # TODO - improve - if not name: - module.fail_json(msg='%s is required for active state' % 'name') - if not linode_id: - module.fail_json(msg='%s is required for active state' % 'linode_id') - if not distribution: - module.fail_json(msg='%s is required for active state' % 'distribution') - + 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 @@ -299,14 +287,9 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino module.fail_json(msg = '%s' % e.value[0]['ERRORMESSAGE']) if not configs: - new_server = True - # TODO - improve - if not name: - module.fail_json(msg='%s is required for active state' % 'name') - if not linode_id: - module.fail_json(msg='%s is required for active state' % 'linode_id') - if not distribution: - module.fail_json(msg='%s is required for active state' % 'distribution') + 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(): @@ -337,6 +320,7 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino 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) @@ -388,14 +372,15 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino if new_server and not ssh_pub_key: instance['password'] = password instances.append(instance) + elif state in ('stopped'): - if not name: - module.fail_json(msg='%s is required for stopped state' % 'name') - if not linode_id: - module.fail_json(msg='%s is required for stopped state' % 'linode_id') + 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: @@ -408,14 +393,15 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino else: instance['status'] = 'Stopped' instances.append(instance) + elif state in ('restarted'): - if not name: - module.fail_json(msg='%s is required for restarted state' % 'name') - if not linode_id: - module.fail_json(msg='%s is required for restarted state' % 'linode_id') + 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: @@ -425,6 +411,7 @@ def linodeServers(module, api, state, name, plan, distribution, datacenter, lino instance['status'] = 'Restarting' changed = True instances.append(instance) + elif state in ('absent', 'deleted'): for server in servers: instance = getInstanceDetails(api, server)