summaryrefslogtreecommitdiffstats
path: root/ansible_collections/amazon/aws/plugins/modules/ec2_key.py
blob: ea4d7f7e4f25f5d4126c452757b4c289cbcf3f4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r"""
---
module: ec2_key
version_added: 1.0.0
short_description: Create or delete an EC2 key pair
description:
  - Create or delete an EC2 key pair.
options:
  name:
    description:
      - Name of the key pair.
    required: true
    type: str
  key_material:
    description:
      - Public key material.
    required: false
    type: str
  force:
    description:
      - Force overwrite of already existing key pair if key has changed.
    required: false
    default: true
    type: bool
  state:
    description:
      - Create or delete keypair.
    required: false
    choices: [ present, absent ]
    default: 'present'
    type: str
  key_type:
    description:
      - The type of key pair to create.
      - Note that ED25519 keys are not supported for Windows instances,
        EC2 Instance Connect, and EC2 Serial Console.
      - By default Amazon will create an RSA key.
      - Mutually exclusive with parameter I(key_material).
    type: str
    choices:
      - rsa
      - ed25519
    version_added: 3.1.0
  file_name:
    description:
      - Name of the file where the generated private key will be saved.
      - When provided, the I(key.private_key) attribute will be removed from the return value.
      - The file is written out on the 'host' side rather than the 'controller' side.
      - Ignored when I(state=absent) or I(key_material) is provided.
    type: path
    version_added: 6.4.0
notes:
  - Support for I(tags) and I(purge_tags) was added in release 2.1.0.
  - For security reasons, this module should be used with B(no_log=true) and (register) functionalities
    when creating new key pair without providing I(key_material).
extends_documentation_fragment:
  - amazon.aws.common.modules
  - amazon.aws.region.modules
  - amazon.aws.tags
  - amazon.aws.boto3

author:
  - "Vincent Viallet (@zbal)"
  - "Prasad Katti (@prasadkatti)"
"""

EXAMPLES = r"""
# Note: These examples do not set authentication details, see the AWS Guide for details.

- name: create a new EC2 key pair, returns generated private key
  # use no_log to avoid private key being displayed into output
  amazon.aws.ec2_key:
    name: my_keypair
  no_log: true
  register: aws_ec2_key_pair

- name: create key pair using provided key_material
  amazon.aws.ec2_key:
    name: my_keypair
    key_material: 'ssh-rsa AAAAxyz...== me@example.com'

- name: create key pair using key_material obtained using 'file' lookup plugin
  amazon.aws.ec2_key:
    name: my_keypair
    key_material: "{{ lookup('file', '/path/to/public_key/id_rsa.pub') }}"

- name: Create ED25519 key pair and save private key into a file
  amazon.aws.ec2_key:
    name: my_keypair
    key_type: ed25519
    file_name: /tmp/aws_ssh_rsa

# try creating a key pair with the name of an already existing keypair
# but don't overwrite it even if the key is different (force=false)
- name: try creating a key pair with name of an already existing keypair
  amazon.aws.ec2_key:
    name: my_existing_keypair
    key_material: 'ssh-rsa AAAAxyz...== me@example.com'
    force: false

- name: remove key pair from AWS by name
  amazon.aws.ec2_key:
    name: my_keypair
    state: absent
"""

RETURN = r"""
changed:
  description: whether a keypair was created/deleted
  returned: always
  type: bool
  sample: true
msg:
  description: short message describing the action taken
  returned: always
  type: str
  sample: key pair created
key:
  description: details of the keypair (this is set to null when state is absent)
  returned: always
  type: complex
  contains:
    fingerprint:
      description: fingerprint of the key
      returned: when state is present
      type: str
      sample: 'b0:22:49:61:d9:44:9d:0c:7e:ac:8a:32:93:21:6c:e8:fb:59:62:43'
    name:
      description: name of the keypair
      returned: when state is present
      type: str
      sample: my_keypair
    id:
      description: id of the keypair
      returned: when state is present
      type: str
      sample: key-123456789abc
    tags:
      description: a dictionary representing the tags attached to the key pair
      returned: when state is present
      type: dict
      sample: '{"my_key": "my value"}'
    private_key:
      description: private key of a newly created keypair
      returned: when a new keypair is created by AWS (I(key_material) is not provided) and I(file_name) is not provided.
      type: str
      sample: '-----BEGIN RSA PRIVATE KEY-----
        MIIEowIBAAKC...
        -----END RSA PRIVATE KEY-----'
    type:
      description: type of a newly created keypair
      returned: when a new keypair is created by AWS
      type: str
      sample: rsa
      version_added: 3.1.0
"""

import os
import uuid

try:
    import botocore
except ImportError:
    pass  # caught by AnsibleAWSModule

from ansible.module_utils._text import to_bytes

from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags
from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_specifications
from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters


class Ec2KeyFailure(Exception):
    def __init__(self, message=None, original_e=None):
        super().__init__(message)
        self.original_e = original_e
        self.message = message


def _import_key_pair(ec2_client, name, key_material, tag_spec=None):
    params = {"KeyName": name, "PublicKeyMaterial": to_bytes(key_material), "TagSpecifications": tag_spec}

    params = scrub_none_parameters(params)

    try:
        key = ec2_client.import_key_pair(aws_retry=True, **params)
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as err:
        raise Ec2KeyFailure(err, "error importing key")
    return key


def extract_key_data(key, key_type=None, file_name=None):
    data = {
        "name": key["KeyName"],
        "fingerprint": key["KeyFingerprint"],
        "id": key["KeyPairId"],
        "tags": boto3_tag_list_to_ansible_dict(key.get("Tags") or []),
        # KeyMaterial is returned by create_key_pair, but not by describe_key_pairs
        "private_key": key.get("KeyMaterial"),
        # KeyType is only set by describe_key_pairs
        "type": key.get("KeyType") or key_type,
    }

    # Write the private key to disk and remove it from the return value
    if file_name and data["private_key"] is not None:
        data = _write_private_key(data, file_name)
    return scrub_none_parameters(data)


def get_key_fingerprint(check_mode, ec2_client, key_material):
    """
    EC2's fingerprints are non-trivial to generate, so push this key
    to a temporary name and make ec2 calculate the fingerprint for us.
    http://blog.jbrowne.com/?p=23
    https://forums.aws.amazon.com/thread.jspa?messageID=352828
    """
    # find an unused name
    name_in_use = True
    while name_in_use:
        random_name = "ansible-" + str(uuid.uuid4())
        name_in_use = find_key_pair(ec2_client, random_name)
    temp_key = _import_key_pair(ec2_client, random_name, key_material)
    delete_key_pair(check_mode, ec2_client, random_name, finish_task=False)
    return temp_key["KeyFingerprint"]


def find_key_pair(ec2_client, name):
    try:
        key = ec2_client.describe_key_pairs(aws_retry=True, KeyNames=[name])
    except is_boto3_error_code("InvalidKeyPair.NotFound"):
        return None
    except (
        botocore.exceptions.ClientError,
        botocore.exceptions.BotoCoreError,
    ) as err:  # pylint: disable=duplicate-except
        raise Ec2KeyFailure(err, "error finding keypair")
    except IndexError:
        key = None

    return key["KeyPairs"][0]


def _create_key_pair(ec2_client, name, tag_spec, key_type):
    params = {
        "KeyName": name,
        "TagSpecifications": tag_spec,
        "KeyType": key_type,
    }

    params = scrub_none_parameters(params)

    try:
        key = ec2_client.create_key_pair(aws_retry=True, **params)
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as err:
        raise Ec2KeyFailure(err, "error creating key")
    return key


def _write_private_key(key_data, file_name):
    """
    Write the private key data to the specified file, and remove 'private_key'
    from the ouput. This ensures we don't expose the key data in logs or task output.
    """
    try:
        file = os.open(file_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
        os.write(file, key_data["private_key"].encode("utf-8"))
        os.close(file)
    except (IOError, OSError) as e:
        raise Ec2KeyFailure(e, "Could not save private key to specified path. Private key is irretrievable.")

    del key_data["private_key"]
    return key_data


def create_new_key_pair(ec2_client, name, key_material, key_type, tags, file_name, check_mode):
    """
    key does not exist, we create new key
    """
    if check_mode:
        return {"changed": True, "key": None, "msg": "key pair created"}

    tag_spec = boto3_tag_specifications(tags, ["key-pair"])
    if key_material:
        key = _import_key_pair(ec2_client, name, key_material, tag_spec)
    else:
        key = _create_key_pair(ec2_client, name, tag_spec, key_type)
    key_data = extract_key_data(key, key_type, file_name)

    result = {"changed": True, "key": key_data, "msg": "key pair created"}
    return result


def update_key_pair_by_key_material(check_mode, ec2_client, name, key, key_material, tag_spec):
    if check_mode:
        return {"changed": True, "key": None, "msg": "key pair updated"}
    new_fingerprint = get_key_fingerprint(check_mode, ec2_client, key_material)
    changed = False
    msg = "key pair already exists"
    if key["KeyFingerprint"] != new_fingerprint:
        delete_key_pair(check_mode, ec2_client, name, finish_task=False)
        key = _import_key_pair(ec2_client, name, key_material, tag_spec)
        msg = "key pair updated"
        changed = True
    key_data = extract_key_data(key)
    return {"changed": changed, "key": key_data, "msg": msg}


def update_key_pair_by_key_type(check_mode, ec2_client, name, key_type, tag_spec, file_name):
    if check_mode:
        return {"changed": True, "key": None, "msg": "key pair updated"}
    else:
        delete_key_pair(check_mode, ec2_client, name, finish_task=False)
        key = _create_key_pair(ec2_client, name, tag_spec, key_type)
        key_data = extract_key_data(key, key_type, file_name)
        return {"changed": True, "key": key_data, "msg": "key pair updated"}


def _delete_key_pair(ec2_client, key_name):
    try:
        ec2_client.delete_key_pair(aws_retry=True, KeyName=key_name)
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as err:
        raise Ec2KeyFailure(err, "error deleting key")


def delete_key_pair(check_mode, ec2_client, name, finish_task=True):
    key = find_key_pair(ec2_client, name)

    if key and check_mode:
        result = {"changed": True, "key": None, "msg": "key deleted"}
    elif not key:
        result = {"key": None, "msg": "key did not exist"}
        return result
    else:
        _delete_key_pair(ec2_client, name)
        if not finish_task:
            return
        result = {"changed": True, "key": None, "msg": "key deleted"}

    return result


def handle_existing_key_pair_update(module, ec2_client, name, key):
    key_material = module.params.get("key_material")
    force = module.params.get("force")
    key_type = module.params.get("key_type")
    tags = module.params.get("tags")
    purge_tags = module.params.get("purge_tags")
    tag_spec = boto3_tag_specifications(tags, ["key-pair"])
    check_mode = module.check_mode
    file_name = module.params.get("file_name")
    if key_material and force:
        result = update_key_pair_by_key_material(check_mode, ec2_client, name, key, key_material, tag_spec)
    elif key_type and key_type != key["KeyType"]:
        result = update_key_pair_by_key_type(check_mode, ec2_client, name, key_type, tag_spec, file_name)
    else:
        changed = False
        changed |= ensure_ec2_tags(ec2_client, module, key["KeyPairId"], tags=tags, purge_tags=purge_tags)
        key = find_key_pair(ec2_client, name)
        key_data = extract_key_data(key, file_name=file_name)
        result = {"changed": changed, "key": key_data, "msg": "key pair already exists"}
    return result


def main():
    argument_spec = dict(
        name=dict(required=True),
        key_material=dict(no_log=False),
        force=dict(type="bool", default=True),
        state=dict(default="present", choices=["present", "absent"]),
        tags=dict(type="dict", aliases=["resource_tags"]),
        purge_tags=dict(type="bool", default=True),
        key_type=dict(type="str", choices=["rsa", "ed25519"]),
        file_name=dict(type="path", required=False),
    )

    module = AnsibleAWSModule(
        argument_spec=argument_spec,
        mutually_exclusive=[["key_material", "key_type"]],
        supports_check_mode=True,
    )

    ec2_client = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff())

    name = module.params["name"]
    state = module.params.get("state")
    key_material = module.params.get("key_material")
    key_type = module.params.get("key_type")
    tags = module.params.get("tags")
    file_name = module.params.get("file_name")

    result = {}

    try:
        if state == "absent":
            result = delete_key_pair(module.check_mode, ec2_client, name)

        elif state == "present":
            # check if key already exists
            key = find_key_pair(ec2_client, name)
            if key:
                result = handle_existing_key_pair_update(module, ec2_client, name, key)
            else:
                result = create_new_key_pair(
                    ec2_client, name, key_material, key_type, tags, file_name, module.check_mode
                )

    except Ec2KeyFailure as e:
        if e.original_e:
            module.fail_json_aws(e.original_e, e.message)
        else:
            module.fail_json(e.message)

    module.exit_json(**result)


if __name__ == "__main__":
    main()