Integrated publickey import and removal in iosxr_user (#32115)

* Integrated publickey import and removal in iosxr_user

* Fixed linting issues

* Added version added for publickeyfile option

* Added quotation marks to version, oops

* Added some integration tests, added some checks to prevent aggregate users with public keys.

* Added some integration test files

* Created mutually exclusive public_key and public_key_contents versions of config

* Modified tests to use both methods and test more logins

* Added supports for aggregates

* Incorporated provider auth

* Fixed some lint issues

* Fixed a YAML lint issue

* Implemented catches for unconfigured providers.

* Fixed catches, hopefully

* Another test

* Added groups support so you can add users to multiple groups

* Trailing whitespace
This commit is contained in:
Sebastiaan van Doesselaar 2017-11-15 15:34:36 +01:00 committed by Chris Alfonso
parent 64f9ced750
commit 900abcddb3
5 changed files with 299 additions and 2 deletions

View file

@ -16,7 +16,9 @@ DOCUMENTATION = """
---
module: iosxr_user
version_added: "2.4"
author: "Trishna Guha (@trishnaguha)"
author:
- "Trishna Guha (@trishnaguha)"
- "Sebastiaan van Doesselaar (@sebasdoes)"
short_description: Manage the aggregate of local users on Cisco IOS XR device
description:
- This module provides declarative management of the local usernames
@ -60,6 +62,14 @@ options:
device running configuration. The argument accepts a string value
defining the group name. This argument does not check if the group
has been configured on the device, alias C(role).
groups:
version_added: "2.5"
description:
- Configures the groups for the username in the device running
configuration. The argument accepts a list of group names.
This argument does not check if the group has been configured
on the device. It is similar to the aggregrate command for
usernames, but lets you configure multiple groups for the user(s).
purge:
description:
- Instructs the module to consider the
@ -77,6 +87,29 @@ options:
in the device active configuration
default: present
choices: ['present', 'absent']
public_key:
version_added: "2.5"
description:
- Configures the contents of the public keyfile to upload to the IOS-XR node.
This enables users to login using the accompanying private key. IOS-XR
only accepts base64 decoded files, so this will be decoded and uploaded
to the node. Do note that this requires an OpenSSL public key file,
PuTTy generated files will not work! Mutually exclusive with
public_key_contents. If used with multiple users in aggregates, then the
same key file is used for all users.
public_key_contents:
version_added: "2.5"
description:
- Configures the contents of the public keyfile to upload to the IOS-XR node.
This enables users to login using the accompanying private key. IOS-XR
only accepts base64 decoded files, so this will be decoded and uploaded
to the node. Do note that this requires an OpenSSL public key file,
PuTTy generated files will not work! Mutually exclusive with
public_key.If used with multiple users in aggregates, then the
same key file is used for all users.
requirements:
- base64 when using I(public_key_contents) or I(public_key)
- paramiko when using I(public_key_contents) or I(public_key)
"""
EXAMPLES = """
@ -95,12 +128,26 @@ EXAMPLES = """
- name: netend
group: sysadmin
state: present
- name: set multiple users to multiple groups
iosxr_user:
aggregate:
- name: netop
- name: netend
groups:
- sysadmin
- root-system
state: present
- name: Change Password for User netop
iosxr_user:
name: netop
configured_password: "{{ new_password }}"
update_password: always
state: present
- name: Add private key authentication for user netop
iosxr_user:
name: netop
state: present
public_key_contents: "{{ lookup('file', '/home/netop/.ssh/id_rsa.pub' }}"
"""
RETURN = """
@ -121,6 +168,18 @@ from ansible.module_utils.network_common import remove_default_spec
from ansible.module_utils.iosxr import get_config, load_config
from ansible.module_utils.iosxr import iosxr_argument_spec, check_args
try:
from base64 import b64decode
HAS_B64 = True
except ImportError:
HAS_B64 = False
try:
import paramiko
HAS_PARAMIKO = True
except ImportError:
HAS_PARAMIKO = False
def search_obj_in_list(name, lst):
for o in lst:
@ -150,6 +209,9 @@ def map_obj_to_commands(updates, module):
commands.append(user_cmd + ' secret ' + w['configured_password'])
if w['group']:
commands.append(user_cmd + ' group ' + w['group'])
elif w['groups']:
for group in w['groups']:
commands.append(user_cmd + ' group ' + group)
elif state == 'present' and obj_in_have:
user_cmd = 'username ' + name
@ -158,6 +220,9 @@ def map_obj_to_commands(updates, module):
commands.append(user_cmd + ' secret ' + w['configured_password'])
if w['group'] and w['group'] != obj_in_have['group']:
commands.append(user_cmd + ' group ' + w['group'])
elif w['groups']:
for group in w['groups']:
commands.append(user_cmd + ' group ' + group)
return commands
@ -215,6 +280,7 @@ def get_param_value(key, item, module):
def map_params_to_obj(module):
users = module.params['aggregate']
if not users:
if not module.params['name'] and module.params['purge']:
return list()
@ -238,12 +304,100 @@ def map_params_to_obj(module):
get_value = partial(get_param_value, item=item, module=module)
item['configured_password'] = get_value('configured_password')
item['group'] = get_value('group')
item['groups'] = get_value('groups')
item['state'] = get_value('state')
objects.append(item)
return objects
def convert_key_to_base64(module):
""" IOS-XR only accepts base64 decoded files, this converts the public key to a temp file.
"""
if module.params['aggregate']:
name = 'aggregate'
else:
name = module.params['name']
if module.params['public_key_contents']:
key = module.params['public_key_contents']
elif module.params['public_key']:
readfile = open(module.params['public_key'], 'r')
key = readfile.read()
splitfile = key.split()[1]
base64key = b64decode(splitfile)
base64file = open('/tmp/publickey_%s.b64' % (name), 'w')
base64file.write(base64key)
base64file.close()
return '/tmp/publickey_%s.b64' % (name)
def copy_key_to_node(module, base64keyfile):
""" Copy key to IOS-XR node. We use SFTP because older IOS-XR versions don't handle SCP very well.
"""
if (module.params['host'] is None or module.params['provider']['host'] is None):
return False
if (module.params['username'] is None or module.params['provider']['username'] is None):
return False
if module.params['aggregate']:
name = 'aggregate'
else:
name = module.params['name']
src = base64keyfile
dst = '/harddisk:/publickey_%s.b64' % (name)
user = module.params['username'] or module.params['provider']['username']
node = module.params['host'] or module.params['provider']['host']
password = module.params['password'] or module.params['provider']['password']
ssh_keyfile = module.params['ssh_keyfile'] or module.params['provider']['ssh_keyfile']
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if not ssh_keyfile:
ssh.connect(node, username=user, password=password)
else:
ssh.connect(node, username=user, allow_agent=True)
sftp = ssh.open_sftp()
sftp.put(src, dst)
sftp.close()
ssh.close()
def addremovekey(module, command):
""" Add or remove key based on command
"""
if (module.params['host'] is None or module.params['provider']['host'] is None):
return False
if (module.params['username'] is None or module.params['provider']['username'] is None):
return False
user = module.params['username'] or module.params['provider']['username']
node = module.params['host'] or module.params['provider']['host']
password = module.params['password'] or module.params['provider']['password']
ssh_keyfile = module.params['ssh_keyfile'] or module.params['provider']['ssh_keyfile']
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if not ssh_keyfile:
ssh.connect(node, username=user, password=password)
else:
ssh.connect(node, username=user, allow_agent=True)
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command('%s \r' % (command))
readmsg = ssh_stdout.read(100) # We need to read a bit to actually apply for some reason
if ('already' in readmsg) or ('removed' in readmsg) or ('really' in readmsg):
ssh_stdin.write('yes\r')
ssh_stdout.read(1) # We need to read a bit to actually apply for some reason
ssh.close()
return readmsg
def main():
""" main entry point for module execution
"""
@ -253,7 +407,12 @@ def main():
configured_password=dict(no_log=True),
update_password=dict(default='always', choices=['on_create', 'always']),
public_key=dict(),
public_key_contents=dict(),
group=dict(aliases=['role']),
groups=dict(type='list', elements='dict'),
state=dict(default='present', choices=['present', 'absent'])
)
aggregate_spec = deepcopy(element_spec)
@ -269,12 +428,24 @@ def main():
argument_spec.update(element_spec)
argument_spec.update(iosxr_argument_spec)
mutually_exclusive = [('name', 'aggregate')]
mutually_exclusive = [('name', 'aggregate'), ('public_key', 'public_key_contents'), ('group', 'groups')]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True)
if (module.params['public_key_contents'] or module.params['public_key']):
if not HAS_B64:
module.fail_json(
msg='library base64 is required but does not appear to be '
'installed. It can be installed using `pip install base64`'
)
if not HAS_PARAMIKO:
module.fail_json(
msg='library paramiko is required but does not appear to be '
'installed. It can be installed using `pip install paramiko`'
)
warnings = list()
if module.params['password'] and not module.params['configured_password']:
warnings.append(
@ -309,6 +480,44 @@ def main():
load_config(module, commands, result['warnings'], commit=True)
result['changed'] = True
if module.params['state'] == 'present' and (module.params['public_key_contents'] or module.params['public_key']):
if not module.check_mode:
key = convert_key_to_base64(module)
copykeys = copy_key_to_node(module, key)
if copykeys is False:
warnings.append('Please set up your provider before running this playbook')
if module.params['aggregate']:
for user in module.params['aggregate']:
cmdtodo = "admin crypto key import authentication rsa username %s harddisk:/publickey_aggregate.b64" % (user)
addremove = addremovekey(module, cmdtodo)
if addremove is False:
warnings.append('Please set up your provider before running this playbook')
else:
cmdtodo = "admin crypto key import authentication rsa username %s harddisk:/publickey_%s.b64" % (module.params['name'], module.params['name'])
addremove = addremovekey(module, cmdtodo)
if addremove is False:
warnings.append('Please set up your provider before running this playbook')
elif module.params['state'] == 'absent':
if not module.check_mode:
if module.params['aggregate']:
for user in module.params['aggregate']:
cmdtodo = "admin crypto key zeroize authentication rsa username %s" % (user)
addremove = addremovekey(module, cmdtodo)
if addremove is False:
warnings.append('Please set up your provider before running this playbook')
else:
cmdtodo = "admin crypto key zeroize authentication rsa username %s" % (module.params['name'])
addremove = addremovekey(module, cmdtodo)
if addremove is False:
warnings.append('Please set up your provider before running this playbook')
elif module.params['purge'] is True:
if not module.check_mode:
cmdtodo = "admin crypto key zeroize authentication rsa all"
addremove = addremovekey(module, cmdtodo)
if addremove is False:
warnings.append('Please set up your provider before running this playbook')
module.exit_json(**result)
if __name__ == '__main__':