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__':

View file

@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,A823A6B5ED873917
mLZ1xM1+xwutkRy+K/c9QsstDPQ9F6UWtDpoYyIgs7n9VgMjhIMbWQC9CkTvnFJM
ey+iwGdQZZOThwxalm+k3pMibwRjhnF+PNFhiVkzWH8/K8QvXRQiW/vYmE/QB9pY
T0IWbMcC7/ktEfQn+6GLXoe/L7yH+aNv/2Flsa2jN2cfSXpzbneUA06/LVVOw6E+
C74NKRWUmMPA39Zd4WOeBoWUdS5Kgwl57SOtrKs1LIGh33+TPu+Go8gJ7h/t/kaN
kverVSz+0eeX+exKumejfo1UfosplRhcjRG8YgiQ8l7SN3NBF/gXiiSrH3fLwmRJ
hbokJ8TmCozrYBs1MNe3LoU2iuIqVnJ5Sd6DJELs6vCuFz+v6J/s80NaaYMlBCbB
1lahelYqoyLb4uiDd4zQSpaxzO+Cx/d50Wpee8mFxbAL/YxacOzD3b/VCBgB+AZN
TTHr1ayd+ITd8gewXAyERKWyrDcC2beJI0fOil23PYowWvEncS6I1f4hKQY28sRf
vHSbwQdltky/xiib2/feQTaMSQFvsY67uTHipMwl5wJNOKcbeqDVMWPYST3XUsBg
LRlbT+VTUEehbOJAJ6Hh7Yv4nqu7fEh95HUQK7Ed56rMLKpmdorYO49JtewkEUsj
LJn7tcxMUuOcWKHMPu6vB/63f6Ulthqp1SEG8aNBaZMuPyLWAPAJc2okOmkiSbvO
0Hxe6BtAGn2fUo2jK6E3tD/dsIR2qqMlL09FkACGT8D5Lfh5d3z+lo9DxpXl281R
ablehPyHgHcIC6cD2/7FwwjzUuyj/kYcETnMs51agcWFAXTom/ehqD+IQ8jZ73zT
5O4FFgslnNmB/vddh9PeYpjDYdR4y5xMrlMxJ+qcZuQOq7dfaiodq8oj+XPmwgxA
audX/sHMutOpmOagrsQfaQXaPqRXdQTnuwHacQfwq+tBBhrft5gwt1HE7Ir2ulwD
Q19kefchkJu/0c1cAGg1VHtQic0a6tX6PrwqZOMDfpSywcImMCF4KHgD2EC5/8h6
tq0PqPLNcwiM2NhpypCuYmkYZ0gnJ/xAwtM85Ck9nmPFptLSd0b7YB7dtGsFYY5A
rhIcq5lZhy06/RRAPluIkniscA50iEO/EXKwzYzovBJh6jQz7oYsbEUW5kwg0gm/
YPSa6lqv2kTpXS+UiGyeNWdUkr5DpdwKe4lrAsN94HE9/SoLgFvz0X5/WyTssSzo
IO3WfLfBc7SOkZK1ibcleIqilzd+LSoIqqGrft2yonXgJD3p9xO+Hlldczx2kHmu
z4lZBq53AkVAQ4os5L7ZRnmxoqKn2XAQRwVH3M9ZFYFEqEyDmZhlFdJSGEnKws81
Ej48t6KWwqml02cx675bSYI22tL3+RL7AGmlC0/Xh8wIVesgulsYmnhW4BtpBYf2
fwv5esJJMjkh2LvLNG3edYChugudeZXtcBJdNr0GYRbBAhvO25bRcr6z8nYDusKX
e/+30vATOcBO/zaOYIwDGT5ZwMQAV1aQl8HyeyYESNjb0fBXQ3OYObOrTTs8MLyC
I4b6wr1vlbN+lMOm+RIXCDgmC3COdlgCHyo3qiIu2YNYQVoNF4NN4A==
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAkvLTTJdwZ0lg1cUCn13Hi3+ho2+G6/96XuAP7jA7Ghz9NPbC/eqXnjvb27BA8CxtFXYuXR5eZWSq2UN5zFcfrFb57XFxdAg2q21hGEX+FGiTUuRZh8+ByVEh0LUetFTwsEZ1iGv6GZiLBt7IJvClXbyNTJEt3DZncHfGwudyGFviV4dGrzusDAGAcoHqvD/5uXYl4PjMH9oSfraO3sG4Q7soQwxNeiM8qOLf3c1SabHBAtSewwnA0E/jhzpOLD2QUncU5s+Oa9PvEXXhGv5eZo9lp71brsgyWj32m2UuXx/n+EZg78GVJT5mFO7LG239n3gTnwkMVdr6zVBFNX5Mvw== rsa-key-20171025

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAhTxbibM8hKZn7xDURs15L3gkcsnpDoZ+tNm5zpP9dcboASnIyJzfC7J/RdRCQsO/pDmUY4y/tsTx18uenyfazxtNkyCHdANlp8XVF1fGNv5GM+QbsDqxe54sdG9csASX0/Ljvl538IbcLFVH0zxyKspbDOgkAkUSuKIAH5x+/GhkAoGQO2tOhYjqofNtUxLSvfRsf4Gm1M0WgdWmz3MW4NOdZhsL4S+STgRPU1jy1dKGj7BKY9cpnCWBFHa2wSaOXJEBZEKNaFVxlBBrFs5brjRQA0mVPmE+pz+/+IJeSNEEma9cXur0ONeb6OoXvkManxKfkaswT2ybOChAzJR8dQ== T-MOBILE

View file

@ -25,6 +25,62 @@
that:
- results.failed
- name: create user with private key (contents input)
iosxr_user:
name: auth_user
state: present
public_key_contents: "{{ lookup('file', \"{{ output_dir }}/public.pub\") }}"
- name: test login with private key
expect:
command: "ssh auth_user@{{ ansible_ssh_host }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i {{output_dir}}/private show version"
responses:
(?i)passphrase: 'pass123'
- name: remove user and key
iosxr_user:
name: auth_user
state: absent
- name: test login with private key (should fail, no user)
expect:
command: "ssh auth_user@{{ ansible_ssh_host }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i {{output_dir}}/private show version"
responses:
(?i)passphrase: 'pass123'
ignore_errors: yes
register: results
- name: create user with private key (path input)
iosxr_user:
name: auth_user
state: present
public_key: "{{ output_dir }}/public.pub"
- name: test login with private key
expect:
command: "ssh auth_user@{{ ansible_ssh_host }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i {{output_dir}}/private show version"
responses:
(?i)passphrase: 'pass123'
- name: change private key for user
iosxr_user:
name: auth_user
state: present
public_key_contents: "{{ lookup('file', \"{{ output_dir }}/public2.pub\") }}"
- name: test login with invalid private key (should fail)
expect:
command: "ssh auth_user@{{ ansible_ssh_host }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i {{output_dir}}/private show version"
responses:
(?i)passphrase: "pass123"
ignore_errors: yes
register: results
- name: check that attempt failed
assert:
that:
- results.failed
always:
- name: delete user
iosxr_user: