Add openssh_keypair module (#46436)

* add openssh_keypair module
This commit is contained in:
lolcube 2018-10-24 12:51:45 +02:00 committed by John R Barker
parent 20b95adf2b
commit eb7f6a5e62
9 changed files with 394 additions and 0 deletions

View file

@ -0,0 +1,315 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: openssh_keypair
author: "David Kainz (@lolcube)"
version_added: "2.8"
short_description: Generate OpenSSH private and public keys.
description:
- "This module allows one to (re)generate OpenSSH private and public keys. It uses
ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
or C(ecdsa) private keys."
requirements:
- "ssh-keygen"
options:
state:
required: false
default: present
choices: [ present, absent ]
description:
- Whether the private and public keys should exist or not, taking action if the state is different from what is stated.
size:
required: false
description:
- "Specifies the number of bits in the private key to create. For RSA keys, the minimum size is 1024 bits and the default is 4096 bits.
Generally, 2048 bits is considered sufficient. DSA keys must be exactly 1024 bits as specified by FIPS 186-2.
For ECDSA keys, size determines the key length by selecting from one of three elliptic curve sizes: 256, 384 or 521 bits.
Attempting to use bit lengths other than these three values for ECDSA keys will cause this module to fail.
Ed25519 keys have a fixed length and the size will be ignored."
type:
required: false
default: rsa
choices: ['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']
description:
- "The algorithm used to generate the SSH private key. C(rsa1) is for protocol version 1.
C(rsa1) is deprecated and may not be supported by every version of ssh-keygen."
force:
required: false
default: false
type: bool
description:
- Should the key be regenerated even if it already exists
path:
required: true
description:
- Name of the files containing the public and private key. The file containing the public key will have the extension C(.pub).
comment:
required: false
description:
- Provides a new comment to the public key. When checking if the key is in the correct state this will be ignored.
extends_documentation_fragment: files
'''
EXAMPLES = '''
# Generate an OpenSSH keypair with the default values (4096 bits, rsa)
- openssh_keypair:
path: /tmp/id_ssh_rsa
# Generate an OpenSSH rsa keypair with a different size (2048 bits)
- openssh_keypair:
path: /tmp/id_ssh_rsa
size: 2048
# Force regenerate an OpenSSH keypair if it already exists
- openssh_keypair:
path: /tmp/id_ssh_rsa
force: True
# Generate an OpenSSH keypair with a different algorithm (dsa)
- openssh_keypair:
path: /tmp/id_ssh_dsa
type: dsa
'''
RETURN = '''
size:
description: Size (in bits) of the SSH private key
returned: changed or success
type: int
sample: 4096
type:
description: Algorithm used to generate the SSH private key
returned: changed or success
type: string
sample: rsa
filename:
description: Path to the generated SSH private key file
returned: changed or success
type: string
sample: /tmp/id_ssh_rsa
fingerprint:
description: The fingerprint of the key.
returned: changed or success
type: string
sample: 4096 SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM example@example.com (RSA)
'''
import os
import errno
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
class KeypairError(Exception):
pass
class Keypair(object):
def __init__(self, module):
self.path = module.params['path']
self.state = module.params['state']
self.force = module.params['force']
self.size = module.params['size']
self.type = module.params['type']
self.comment = module.params['comment']
self.changed = False
self.check_mode = module.check_mode
self.privatekey = None
self.fingerprint = {}
if self.type in ('rsa', 'rsa1'):
self.size = 4096 if self.size is None else self.size
if self.size < 1024:
module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. '
'Attempting to use bit lengths under 1024 will cause the module to fail.'))
if self.type == 'dsa':
self.size = 1024 if self.size is None else self.size
if self.size != 1024:
module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.'))
if self.type == 'ecdsa':
self.size = 256 if self.size is None else self.size
if self.size not in (256, 384, 521):
module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from '
'one of three elliptic curve sizes: 256, 384 or 521 bits. '
'Attempting to use bit lengths other than these three values for '
'ECDSA keys will cause this module to fail. '))
if self.type == 'ed25519':
self.size = 256
def generate(self, module):
# generate a keypair
if not self.isValid(module, perms_required=False) or self.force:
args = [
module.get_bin_path('ssh-keygen', True),
'-q',
'-N', '',
'-b', str(self.size),
'-t', self.type,
'-f', self.path,
]
if self.comment:
args.extend(['-C', self.comment])
else:
args.extend(['-C', ""])
try:
self.changed = True
module.run_command(args)
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
self.fingerprint = proc[1].split()
except Exception as e:
self.remove()
module.fail_json(msg="%s" % to_native(e))
file_args = module.load_file_common_arguments(module.params)
if module.set_fs_attributes_if_different(file_args, False):
self.changed = True
def isValid(self, module, perms_required=True):
# check if the key is correct
def _check_state():
return os.path.exists(self.path)
if _check_state():
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
fingerprint = proc[1].split()
keysize = int(fingerprint[0])
keytype = fingerprint[-1][1:-1].lower()
else:
return False
def _check_perms(module):
file_args = module.load_file_common_arguments(module.params)
return not module.set_fs_attributes_if_different(file_args, False)
def _check_type():
return self.type == keytype
def _check_size():
return self.size == keysize
self.fingerprint = fingerprint
if not perms_required:
return _check_state() and _check_type() and _check_size()
return _check_state() and _check_perms(module) and _check_type() and _check_size()
def dump(self):
# return result as a dict
"""Serialize the object into a dictionary."""
result = {
'changed': self.changed,
'size': self.size,
'type': self.type,
'filename': self.path,
'fingerprint': self.fingerprint,
}
return result
def remove(self):
"""Remove the resource from the filesystem."""
try:
os.remove(self.path)
self.changed = True
except OSError as exc:
if exc.errno != errno.ENOENT:
raise KeypairError(exc)
else:
pass
if os.path.exists(self.path + ".pub"):
try:
os.remove(self.path + ".pub")
self.changed = True
except OSError as exc:
if exc.errno != errno.ENOENT:
raise KeypairError(exc)
else:
pass
def main():
# Define Ansible Module
module = AnsibleModule(
argument_spec=dict(
state=dict(default='present', choices=['present', 'absent'], type='str'),
size=dict(type='int'),
type=dict(default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519'], type='str'),
force=dict(default=False, type='bool'),
path=dict(required=True, type='path'),
comment=dict(type='str'),
),
supports_check_mode=True,
add_file_common_args=True,
)
# Check if Path exists
base_dir = os.path.dirname(module.params['path'])
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir
)
keypair = Keypair(module)
if keypair.state == 'present':
if module.check_mode:
result = keypair.dump()
result['changed'] = module.params['force'] or not keypair.isValid(module)
module.exit_json(**result)
try:
keypair.generate(module)
except Exception as exc:
module.fail_json(msg=to_native(exc))
else:
if module.check_mode:
keypair.changed = os.path.exists(module.params['path'])
if keypair.changed:
keypair.fingerprint = {}
result = keypair.dump()
module.exit_json(**result)
try:
keypair.remove()
except Exception as exc:
module.fail_json(msg=to_native(exc))
result = keypair.dump()
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
shippable/posix/group1
destructive

View file

@ -0,0 +1,2 @@
dependencies:
- setup_ssh_keygen

View file

@ -0,0 +1,25 @@
- name: Generate privatekey1 - standard
connection: local
openssh_keypair:
path: '{{ output_dir }}/privatekey1'
- name: Generate privatekey2 - size 2048
openssh_keypair:
path: '{{ output_dir }}/privatekey2'
size: 2048
- name: Generate privatekey3 - type dsa
openssh_keypair:
path: '{{ output_dir }}/privatekey3'
type: dsa
- name: Generate privatekey4 - standard
openssh_keypair:
path: '{{ output_dir }}/privatekey4'
- name: Delete privatekey4 - standard
openssh_keypair:
state: absent
path: '{{ output_dir }}/privatekey4'
- import_tasks: ../tests/validate.yml

View file

@ -0,0 +1,39 @@
- name: Validate privatekey1 (test - RSA key with size 4096 bits)
shell: "ssh-keygen -lf {{ output_dir }}/privatekey1 | grep -o -E '^[0-9]+'"
register: privatekey1
- name: Validate privatekey1 (assert - RSA key with size 4096 bits)
assert:
that:
- privatekey1.stdout == '4096'
- name: Validate privatekey2 (test - RSA key with size 2048 bits)
shell: "ssh-keygen -lf {{ output_dir }}/privatekey2 | grep -o -E '^[0-9]+'"
register: privatekey2
- name: Validate privatekey2 (assert - RSA key with size 2048 bits)
assert:
that:
- privatekey2.stdout == '2048'
- name: Validate privatekey3 (test - DSA key with size 1024 bits)
shell: "ssh-keygen -lf {{ output_dir }}/privatekey3 | grep -o -E '^[0-9]+'"
register: privatekey3
- name: Validate privatekey3 (assert - DSA key with size 4096 bits)
assert:
that:
- privatekey3.stdout == '1024'
- name: Validate privatekey4 (test - Ensure key has been removed)
stat:
path: '{{ output_dir }}/privatekey4'
register: privatekey4
- name: Validate privatekey4 (assert - Ensure key has been removed)
assert:
that:
- privatekey4.stat.exists == False

View file

@ -0,0 +1,8 @@
- name: Include OS-specific variables
include_vars: '{{ ansible_os_family }}.yml'
when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD"
- name: Install ssh-keygen
package:
name: '{{ openssh_client_package_name }}'
when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD"

View file

@ -0,0 +1 @@
openssh_client_package_name: openssh-client

View file

@ -0,0 +1 @@
openssh_client_package_name: openssh-clients

View file

@ -0,0 +1 @@
openssh_client_package_name: openssh