From d782d91b8014f76e780d266dfa8529c0e3e16019 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Wed, 18 Sep 2013 15:41:55 -0400 Subject: [PATCH 1/3] Fix some "make pep8" errors in the digital_ocean module. --- library/cloud/digital_ocean | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/cloud/digital_ocean b/library/cloud/digital_ocean index 610c18132e2..3f030416fcc 100644 --- a/library/cloud/digital_ocean +++ b/library/cloud/digital_ocean @@ -186,9 +186,9 @@ class Droplet(JsonfyMixIn): self.power_on() if wait: - end_time = time.time()+wait_timeout + end_time = time.time() + wait_timeout while time.time() < end_time: - time.sleep(min(20, end_time-time.time())) + time.sleep(min(20, end_time - time.time())) self.update_attr() if self.is_powered_on(): if not self.ip_address: @@ -283,18 +283,18 @@ def core(module): droplet = Droplet.find(module.params['id']) 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'] - ) + 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'] + ) if droplet.is_powered_on(): changed = False droplet.ensure_powered_on( - wait=getkeyordie('wait'), - wait_timeout=getkeyordie('wait_timeout') - ) + wait=getkeyordie('wait'), + wait_timeout=getkeyordie('wait_timeout') + ) module.exit_json(changed=changed, droplet=droplet.to_json()) elif state in ('absent', 'deleted'): From 6ecf41530bb097f2fa5654160c320d1bef0f74ca Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Wed, 18 Sep 2013 15:49:41 -0400 Subject: [PATCH 2/3] Fix an example with a yaml syntax error. --- library/cloud/digital_ocean | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/cloud/digital_ocean b/library/cloud/digital_ocean index 3f030416fcc..82733ae483b 100644 --- a/library/cloud/digital_ocean +++ b/library/cloud/digital_ocean @@ -102,7 +102,8 @@ EXAMPLES = ''' image_id=3 wait_timeout=500 register: my_droplet -- debug: msg="ID: {{ my_droplet.droplet.id }} IP: {{ my_droplet.droplet.ip_address }}" +- 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 From a9a2a96218bb0be732ced843859221a711dacd33 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Wed, 18 Sep 2013 16:07:22 -0400 Subject: [PATCH 3/3] Add a new unique_name param to the digital_ocean module for idempotence. As it stands now, it is difficult to write idempotent tasks for digital ocean droplets. Digital ocean assigns new nodes a random id when they are provisioned and that id is the only key that can be used to identify it in subsequent runs of that play. The workflow previously involved manual intervention: - write a play defining a new node with no specified id - run it, collect the randomly assigned id by hand - modify the play to add the id by hand so future runs don't create duplicate nodes - perform future re-runs that check if the node exists (by its id) - if it does exist then do nothing. - if it does not exist, then create it and return a *new random id* - collect the new random id by hand, modify the playbook file, and start all over. Its a huge pain. The modifications in this commit allow you to use the 'hostname' as a primary key for idempotence with digital ocean. By default, digital ocean will let you create as many hosts with the same hostname as you like. Here, we provide an option to constrain the user to using only unique hostnames. The workflow will now look like: - write a play defining a new node with a specified hostname and "unique_name: true"" - run it, create the new node and move on. - re-run it, notice that a node with that hostname is already created and move on. --- library/cloud/digital_ocean | 45 +++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/library/cloud/digital_ocean b/library/cloud/digital_ocean index 82733ae483b..31ec50888f1 100644 --- a/library/cloud/digital_ocean +++ b/library/cloud/digital_ocean @@ -45,6 +45,11 @@ options: 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, digital ocean allows multiple hosts with the same name. Setting this to "yes" allows only one host per name. Useful for idempotence. + default: "no" + choices: [ "yes", "no" ] size_id: description: - Numeric, this is the id of the size you would like the droplet created at. @@ -211,13 +216,22 @@ class Droplet(JsonfyMixIn): return droplet @classmethod - def find(cls, id): - if not id: + 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 @@ -281,7 +295,17 @@ def core(module): if command == 'droplet': Droplet.setup(client_id, api_key) if state in ('active', 'present'): - droplet = Droplet.find(module.params['id']) + + # 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'), @@ -290,18 +314,30 @@ def core(module): region_id=getkeyordie('region_id'), ssh_key_ids=module.params['ssh_key_ids'] ) + 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'): - droplet = Droplet.find(getkeyordie('id')) + # First, try to find a droplet by id. + droplet = Droplet.find(id=getkeyordie('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']) @@ -336,6 +372,7 @@ def main(): region_id = dict(type='int'), ssh_key_ids = dict(default=''), id = dict(aliases=['droplet_id'], type='int'), + unique_name = dict(type='bool', choices=BOOLEANS, default='no'), wait = dict(type='bool', choices=BOOLEANS, default='yes'), wait_timeout = dict(default=300, type='int'), ssh_pub_key = dict(type='str'),