1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: Ansible Project
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15
16DOCUMENTATION = '''
17---
18module: digital_ocean_sshkey
19short_description: Manage DigitalOcean SSH keys
20description:
21     - Create/delete DigitalOcean SSH keys.
22version_added: "2.4"
23author: "Patrick Marques (@pmarques)"
24options:
25  state:
26    description:
27     - Indicate desired state of the target.
28    default: present
29    choices: ['present', 'absent']
30  fingerprint:
31    description:
32     - This is a unique identifier for the SSH key used to delete a key
33    version_added: 2.4
34    aliases: ['id']
35  name:
36    description:
37     - The name for the SSH key
38  ssh_pub_key:
39    description:
40     - The Public SSH key to add.
41  oauth_token:
42    description:
43     - DigitalOcean OAuth token.
44    required: true
45    version_added: 2.4
46notes:
47  - Version 2 of DigitalOcean API is used.
48requirements:
49  - "python >= 2.6"
50'''
51
52
53EXAMPLES = '''
54- name: "Create ssh key"
55  digital_ocean_sshkey:
56    oauth_token: "{{ oauth_token }}"
57    name: "My SSH Public Key"
58    ssh_pub_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example"
59    state: present
60  register: result
61
62- name: "Delete ssh key"
63  digital_ocean_sshkey:
64    oauth_token: "{{ oauth_token }}"
65    state: "absent"
66    fingerprint: "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa"
67'''
68
69
70RETURN = '''
71# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#list-all-keys
72data:
73    description: This is only present when C(state=present)
74    returned: when C(state=present)
75    type: dict
76    sample: {
77        "ssh_key": {
78            "id": 512189,
79            "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa",
80            "name": "My SSH Public Key",
81            "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example"
82        }
83    }
84'''
85
86import json
87import hashlib
88import base64
89
90from ansible.module_utils.basic import env_fallback
91from ansible.module_utils.basic import AnsibleModule
92from ansible.module_utils.urls import fetch_url
93
94
95class Response(object):
96
97    def __init__(self, resp, info):
98        self.body = None
99        if resp:
100            self.body = resp.read()
101        self.info = info
102
103    @property
104    def json(self):
105        if not self.body:
106            if "body" in self.info:
107                return json.loads(self.info["body"])
108            return None
109        try:
110            return json.loads(self.body)
111        except ValueError:
112            return None
113
114    @property
115    def status_code(self):
116        return self.info["status"]
117
118
119class Rest(object):
120
121    def __init__(self, module, headers):
122        self.module = module
123        self.headers = headers
124        self.baseurl = 'https://api.digitalocean.com/v2'
125
126    def _url_builder(self, path):
127        if path[0] == '/':
128            path = path[1:]
129        return '%s/%s' % (self.baseurl, path)
130
131    def send(self, method, path, data=None, headers=None):
132        url = self._url_builder(path)
133        data = self.module.jsonify(data)
134        timeout = self.module.params['timeout']
135
136        resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method, timeout=timeout)
137
138        # Exceptions in fetch_url may result in a status -1, the ensures a
139        if info['status'] == -1:
140            self.module.fail_json(msg=info['msg'])
141
142        return Response(resp, info)
143
144    def get(self, path, data=None, headers=None):
145        return self.send('GET', path, data, headers)
146
147    def put(self, path, data=None, headers=None):
148        return self.send('PUT', path, data, headers)
149
150    def post(self, path, data=None, headers=None):
151        return self.send('POST', path, data, headers)
152
153    def delete(self, path, data=None, headers=None):
154        return self.send('DELETE', path, data, headers)
155
156
157def core(module):
158    api_token = module.params['oauth_token']
159    state = module.params['state']
160    fingerprint = module.params['fingerprint']
161    name = module.params['name']
162    ssh_pub_key = module.params['ssh_pub_key']
163
164    rest = Rest(module, {'Authorization': 'Bearer {0}'.format(api_token),
165                         'Content-type': 'application/json'})
166
167    fingerprint = fingerprint or ssh_key_fingerprint(ssh_pub_key)
168    response = rest.get('account/keys/{0}'.format(fingerprint))
169    status_code = response.status_code
170    json = response.json
171
172    if status_code not in (200, 404):
173        module.fail_json(msg='Error getting ssh key [{0}: {1}]'.format(
174            status_code, response.json['message']), fingerprint=fingerprint)
175
176    if state in ('present'):
177        if status_code == 404:
178            # IF key not found create it!
179
180            if module.check_mode:
181                module.exit_json(changed=True)
182
183            payload = {
184                'name': name,
185                'public_key': ssh_pub_key
186            }
187            response = rest.post('account/keys', data=payload)
188            status_code = response.status_code
189            json = response.json
190            if status_code == 201:
191                module.exit_json(changed=True, data=json)
192
193            module.fail_json(msg='Error creating ssh key [{0}: {1}]'.format(
194                status_code, response.json['message']))
195
196        elif status_code == 200:
197            # If key found was found, check if name needs to be updated
198            if name is None or json['ssh_key']['name'] == name:
199                module.exit_json(changed=False, data=json)
200
201            if module.check_mode:
202                module.exit_json(changed=True)
203
204            payload = {
205                'name': name,
206            }
207            response = rest.put('account/keys/{0}'.format(fingerprint), data=payload)
208            status_code = response.status_code
209            json = response.json
210            if status_code == 200:
211                module.exit_json(changed=True, data=json)
212
213            module.fail_json(msg='Error updating ssh key name [{0}: {1}]'.format(
214                status_code, response.json['message']), fingerprint=fingerprint)
215
216    elif state in ('absent'):
217        if status_code == 404:
218            module.exit_json(changed=False)
219
220        if module.check_mode:
221            module.exit_json(changed=True)
222
223        response = rest.delete('account/keys/{0}'.format(fingerprint))
224        status_code = response.status_code
225        json = response.json
226        if status_code == 204:
227            module.exit_json(changed=True)
228
229        module.fail_json(msg='Error creating ssh key [{0}: {1}]'.format(
230            status_code, response.json['message']))
231
232
233def ssh_key_fingerprint(ssh_pub_key):
234    key = ssh_pub_key.split(None, 2)[1]
235    fingerprint = hashlib.md5(base64.b64decode(key)).hexdigest()
236    return ':'.join(a + b for a, b in zip(fingerprint[::2], fingerprint[1::2]))
237
238
239def main():
240    module = AnsibleModule(
241        argument_spec=dict(
242            state=dict(choices=['present', 'absent'], default='present'),
243            fingerprint=dict(aliases=['id'], required=False),
244            name=dict(required=False),
245            ssh_pub_key=dict(required=False),
246            oauth_token=dict(
247                no_log=True,
248                # Support environment variable for DigitalOcean OAuth Token
249                fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN']),
250                required=True,
251            ),
252            validate_certs=dict(type='bool', default=True),
253            timeout=dict(type='int', default=30),
254        ),
255        required_one_of=(
256            ('fingerprint', 'ssh_pub_key'),
257        ),
258        supports_check_mode=True,
259    )
260
261    core(module)
262
263
264if __name__ == '__main__':
265    main()
266