From 8276face94207cbbb6a803a3be38a36f46e3a9f6 Mon Sep 17 00:00:00 2001 From: Michael Gregson Date: Mon, 30 Sep 2013 19:22:07 -0600 Subject: [PATCH 1/5] [digital_ocean] Don't die when the id parameter is missing It's okay for this to happen now, because we might move on to the name parameter if unique_name is enabled. --- library/cloud/digital_ocean | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/cloud/digital_ocean b/library/cloud/digital_ocean index cd485aedf91..e5d24608fa7 100644 --- a/library/cloud/digital_ocean +++ b/library/cloud/digital_ocean @@ -328,13 +328,15 @@ def core(module): elif state in ('absent', 'deleted'): # First, try to find a droplet by id. - droplet = Droplet.find(id=getkeyordie('id')) + droplet = None + if 'id' in module.params: + 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 not droplet and module.params['unique_name'] and 'name' in module.params: + droplet = Droplet.find(name=module.params['name']) if not droplet: module.exit_json(changed=False, msg='The droplet is not found.') From b47df3e1f16edf04f69067440e4c24f09dd02619 Mon Sep 17 00:00:00 2001 From: Michael Gregson Date: Mon, 30 Sep 2013 20:50:22 -0600 Subject: [PATCH 2/5] Add basic domain handling to digital_ocean cloud action --- library/cloud/digital_ocean | 140 +++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/library/cloud/digital_ocean b/library/cloud/digital_ocean index e5d24608fa7..fe304e0151e 100644 --- a/library/cloud/digital_ocean +++ b/library/cloud/digital_ocean @@ -27,7 +27,7 @@ options: description: - Which target you want to operate on. default: droplet - choices: ['droplet', 'ssh'] + choices: ['droplet', 'ssh', 'domain'] state: description: - Indicate desired state of the target. @@ -44,7 +44,7 @@ options: - 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. + - 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. 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. @@ -75,6 +75,9 @@ options: ssh_pub_key: description: - The public SSH key you want to add to your account. + ip: + description: + - The IP address to point a domain at. notes: - Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY. @@ -141,6 +144,31 @@ EXAMPLES = ''' size_id=1 region_id=2 image_id=3 + +# Create a domain record + +- digital_ocean: > + state=present + command=domain + name=my.digitalocean.domain + ip=127.0.0.1 + +# Create a droplet and a corresponding domain record + +- digital_cean: > + state=present + command=droplet + name=test_droplet + size_id=1 + region_id=2 + image_id=3 + register: test_droplet + +- digital_ocean: > + state=present + command=domain + name={{ test_droplet.name }}.my.domain + ip={{ test_droplet.ip_address }} ''' import sys @@ -275,6 +303,72 @@ class SSH(JsonfyMixIn): json = cls.manager.new_ssh_key(name, key_pub) return cls(json) +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] @@ -361,11 +455,50 @@ def core(module): key.destroy() module.exit_json(changed=True) + elif command == 'domain': + Domain.setup(client_id, api_key) + if state in ('present', 'active'): + 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', 'deleted'): + 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( - command = dict(choices=['droplet', 'ssh'], default='droplet'), + command = dict(choices=['droplet', 'ssh', 'domain'], 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), @@ -379,6 +512,7 @@ def main(): wait = dict(type='bool', choices=BOOLEANS, default='yes'), wait_timeout = dict(default=300, type='int'), ssh_pub_key = dict(type='str'), + ip = dict(type='str'), ), required_together = ( ['size_id', 'image_id', 'region_id'], From b4fdb4c86bd7a1b69c45d212143417b462d18d28 Mon Sep 17 00:00:00 2001 From: Michael Gregson Date: Tue, 1 Oct 2013 15:12:41 -0600 Subject: [PATCH 3/5] [digital_ocean] revert original module and add one new module for each command --- library/cloud/digital_ocean | 148 +------------ library/cloud/digital_ocean_domain | 242 +++++++++++++++++++++ library/cloud/digital_ocean_droplet | 320 ++++++++++++++++++++++++++++ library/cloud/digital_ocean_ssh | 179 ++++++++++++++++ 4 files changed, 747 insertions(+), 142 deletions(-) create mode 100644 library/cloud/digital_ocean_domain create mode 100644 library/cloud/digital_ocean_droplet create mode 100644 library/cloud/digital_ocean_ssh diff --git a/library/cloud/digital_ocean b/library/cloud/digital_ocean index fe304e0151e..cd485aedf91 100644 --- a/library/cloud/digital_ocean +++ b/library/cloud/digital_ocean @@ -27,7 +27,7 @@ options: description: - Which target you want to operate on. default: droplet - choices: ['droplet', 'ssh', 'domain'] + choices: ['droplet', 'ssh'] state: description: - Indicate desired state of the target. @@ -44,7 +44,7 @@ options: - 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. + - 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. @@ -75,9 +75,6 @@ options: ssh_pub_key: description: - The public SSH key you want to add to your account. - ip: - description: - - The IP address to point a domain at. notes: - Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY. @@ -144,31 +141,6 @@ EXAMPLES = ''' size_id=1 region_id=2 image_id=3 - -# Create a domain record - -- digital_ocean: > - state=present - command=domain - name=my.digitalocean.domain - ip=127.0.0.1 - -# Create a droplet and a corresponding domain record - -- digital_cean: > - state=present - command=droplet - name=test_droplet - size_id=1 - region_id=2 - image_id=3 - register: test_droplet - -- digital_ocean: > - state=present - command=domain - name={{ test_droplet.name }}.my.domain - ip={{ test_droplet.ip_address }} ''' import sys @@ -303,72 +275,6 @@ class SSH(JsonfyMixIn): json = cls.manager.new_ssh_key(name, key_pub) return cls(json) -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] @@ -422,15 +328,13 @@ def core(module): elif state in ('absent', 'deleted'): # First, try to find a droplet by id. - droplet = None - if 'id' in module.params: - droplet = Droplet.find(id=module.params['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'] and 'name' in module.params: - droplet = Droplet.find(name=module.params['name']) + 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.') @@ -455,50 +359,11 @@ def core(module): key.destroy() module.exit_json(changed=True) - elif command == 'domain': - Domain.setup(client_id, api_key) - if state in ('present', 'active'): - 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', 'deleted'): - 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( - command = dict(choices=['droplet', 'ssh', 'domain'], default='droplet'), + 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), @@ -512,7 +377,6 @@ def main(): wait = dict(type='bool', choices=BOOLEANS, default='yes'), wait_timeout = dict(default=300, type='int'), ssh_pub_key = dict(type='str'), - ip = dict(type='str'), ), required_together = ( ['size_id', 'image_id', 'region_id'], diff --git a/library/cloud/digital_ocean_domain b/library/cloud/digital_ocean_domain new file mode 100644 index 00000000000..21a9132381d --- /dev/null +++ b/library/cloud/digital_ocean_domain @@ -0,0 +1,242 @@ +#!/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 . +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.4" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'active', 'absent', 'deleted'] + client_id: + description: + - Digital Ocean manager id. + api_key: + description: + - Digital Ocean 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_cean_droplet: > + 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.name }}.my.domain + ip={{ test_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)) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() diff --git a/library/cloud/digital_ocean_droplet b/library/cloud/digital_ocean_droplet new file mode 100644 index 00000000000..b59d79ce605 --- /dev/null +++ b/library/cloud/digital_ocean_droplet @@ -0,0 +1,320 @@ +#!/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 . +DOCUMENTATION = ''' +--- +module: digital_ocean_droplet +short_description: Create/delete a droplet in DigitalOcean +description: + - Create/delete a droplet in DigitalOcean and optionally waits for it to be 'running'. +version_added: "1.4" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'absent'] + client_id: + description: + - Digital Ocean manager id. + api_key: + description: + - Digital Ocean 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. + 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. + 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" + ssh_key_ids: + description: + - Optional, comma separated list of ssh_key_ids that you would like to be added to the server + 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 + +notes: + - Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY. +''' + +EXAMPLES = ''' +# Create a new Droplet +# Will return the droplet details including the droplet id (used for idempotence) + +- digital_ocean_droplet: > + state=present + name=my_new_droplet + 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_droplet: > + state=present + id=123 + name=my_new_droplet + 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_droplet: > + state=present + ssh_key_ids=id1,id2 + name=my_new_droplet + client_id=XXX + api_key=XXX + size_id=1 + region_id=2 + image_id=3 +''' + +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 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) + + @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): + json = cls.manager.new_droplet(name, size_id, image_id, region_id, ssh_key_ids) + 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) + +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'] + + Droplet.setup(client_id, api_key) + if state in ('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'] + ) + + 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'): + # First, try to find a droplet by id. + droplet = None + if 'id' in module.params: + 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'] and 'name' in module.params: + droplet = Droplet.find(name=module.params['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']) + +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'), + size_id = dict(type='int'), + image_id = dict(type='int'), + 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'), + ), + required_together = ( + ['size_id', 'image_id', 'region_id'], + ), + 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)) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() diff --git a/library/cloud/digital_ocean_ssh b/library/cloud/digital_ocean_ssh new file mode 100644 index 00000000000..1c361f69d77 --- /dev/null +++ b/library/cloud/digital_ocean_ssh @@ -0,0 +1,179 @@ +#!/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 . +DOCUMENTATION = ''' +--- +module: digital_ocean_ssh +short_description: Create/delete an SSH key in DigitalOcean +description: + - Create/delete an SSH key. +version_added: "1.4" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'absent'] + client_id: + description: + - Digital Ocean manager id. + api_key: + description: + - Digital Ocean 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_ssh: > + state=present + command=ssh + 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)) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 7e0a56d2a45df3c1de91c895ed94c8b31ddac1d8 Mon Sep 17 00:00:00 2001 From: Michael Gregson Date: Tue, 1 Oct 2013 15:18:15 -0600 Subject: [PATCH 4/5] [digital_ocean] Remove command usage from digital_ocean_ssh examples I seem to have forgotten to do this in my last commit. --- library/cloud/digital_ocean_ssh | 1 - 1 file changed, 1 deletion(-) diff --git a/library/cloud/digital_ocean_ssh b/library/cloud/digital_ocean_ssh index 1c361f69d77..f03fae14f80 100644 --- a/library/cloud/digital_ocean_ssh +++ b/library/cloud/digital_ocean_ssh @@ -56,7 +56,6 @@ EXAMPLES = ''' - digital_ocean_ssh: > state=present - command=ssh name=my_ssh_key ssh_pub_key='ssh-rsa AAAA...' client_id=XXX From 3ac4611093f57745db4a6c73db61ee259bd6e070 Mon Sep 17 00:00:00 2001 From: Michael Gregson Date: Wed, 11 Dec 2013 12:03:53 -0700 Subject: [PATCH 5/5] Rename digital_ocean_ssh to digital_ocean_sshkey per https://github.com/ansible/ansible/pull/4315#issuecomment-30286556 --- library/cloud/{digital_ocean_ssh => digital_ocean_sshkey} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename library/cloud/{digital_ocean_ssh => digital_ocean_sshkey} (98%) diff --git a/library/cloud/digital_ocean_ssh b/library/cloud/digital_ocean_sshkey similarity index 98% rename from library/cloud/digital_ocean_ssh rename to library/cloud/digital_ocean_sshkey index f03fae14f80..19305c1e42e 100644 --- a/library/cloud/digital_ocean_ssh +++ b/library/cloud/digital_ocean_sshkey @@ -17,7 +17,7 @@ # along with Ansible. If not, see . DOCUMENTATION = ''' --- -module: digital_ocean_ssh +module: digital_ocean_sshkey short_description: Create/delete an SSH key in DigitalOcean description: - Create/delete an SSH key. @@ -54,7 +54,7 @@ EXAMPLES = ''' # 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_ssh: > +- digital_ocean_sshkey: > state=present name=my_ssh_key ssh_pub_key='ssh-rsa AAAA...'