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:
parent
64f9ced750
commit
900abcddb3
5 changed files with 299 additions and 2 deletions
|
@ -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__':
|
||||
|
|
30
test/integration/targets/iosxr_user/files/private
Normal file
30
test/integration/targets/iosxr_user/files/private
Normal 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-----
|
1
test/integration/targets/iosxr_user/files/public.pub
Normal file
1
test/integration/targets/iosxr_user/files/public.pub
Normal file
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAkvLTTJdwZ0lg1cUCn13Hi3+ho2+G6/96XuAP7jA7Ghz9NPbC/eqXnjvb27BA8CxtFXYuXR5eZWSq2UN5zFcfrFb57XFxdAg2q21hGEX+FGiTUuRZh8+ByVEh0LUetFTwsEZ1iGv6GZiLBt7IJvClXbyNTJEt3DZncHfGwudyGFviV4dGrzusDAGAcoHqvD/5uXYl4PjMH9oSfraO3sG4Q7soQwxNeiM8qOLf3c1SabHBAtSewwnA0E/jhzpOLD2QUncU5s+Oa9PvEXXhGv5eZo9lp71brsgyWj32m2UuXx/n+EZg78GVJT5mFO7LG239n3gTnwkMVdr6zVBFNX5Mvw== rsa-key-20171025
|
1
test/integration/targets/iosxr_user/files/public2.pub
Normal file
1
test/integration/targets/iosxr_user/files/public2.pub
Normal file
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAhTxbibM8hKZn7xDURs15L3gkcsnpDoZ+tNm5zpP9dcboASnIyJzfC7J/RdRCQsO/pDmUY4y/tsTx18uenyfazxtNkyCHdANlp8XVF1fGNv5GM+QbsDqxe54sdG9csASX0/Ljvl538IbcLFVH0zxyKspbDOgkAkUSuKIAH5x+/GhkAoGQO2tOhYjqofNtUxLSvfRsf4Gm1M0WgdWmz3MW4NOdZhsL4S+STgRPU1jy1dKGj7BKY9cpnCWBFHa2wSaOXJEBZEKNaFVxlBBrFs5brjRQA0mVPmE+pz+/+IJeSNEEma9cXur0ONeb6OoXvkManxKfkaswT2ybOChAzJR8dQ== T-MOBILE
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue