mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
aci_aaa_user: Manage AAA users on ACI fabrics (#35543)
* aci_aaa_user: Manage AAA users on ACI fabrics * Fix module documentation * Ensure we allow to not set accountStatus and expires * Added aaa_password_lifetime and aaa_password_update_required support * Improvements to integration tests in light of issue 35544 * Fix ACI ISO 8601 formatted string * Add HAS_DATEUTIL
This commit is contained in:
parent
512d6f6ac6
commit
12b8b8dcf2
4 changed files with 439 additions and 1 deletions
|
@ -186,6 +186,14 @@ class ACIModule(object):
|
|||
elif self.params['password'] is not None:
|
||||
self.module.warn('When doing ACI signatured-based authentication, a password is not required')
|
||||
|
||||
def iso8601_format(self, dt):
|
||||
''' Return an ACI-compatible ISO8601 formatted time: 2123-12-12T00:00:00.000+00:00 '''
|
||||
try:
|
||||
return dt.isoformat(timespec='milliseconds')
|
||||
except:
|
||||
tz = dt.strftime('%z')
|
||||
return '%s.%03d%s:%s' % (dt.strftime('%Y-%m-%dT%H:%M:%S'), dt.microsecond / 1000, tz[:3], tz[3:])
|
||||
|
||||
def define_protocol(self):
|
||||
''' Set protocol based on use_ssl parameter '''
|
||||
|
||||
|
@ -315,7 +323,6 @@ class ACIModule(object):
|
|||
def query(self, path):
|
||||
''' Perform a query with no payload '''
|
||||
|
||||
# Ensure method is set
|
||||
self.result['path'] = path
|
||||
|
||||
if 'port' in self.params and self.params['port'] is not None:
|
||||
|
|
256
lib/ansible/modules/network/aci/aci_aaa_user.py
Normal file
256
lib/ansible/modules/network/aci/aci_aaa_user.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Dag Wieers (dagwieers) <dag@wieers.com>
|
||||
# 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 = r'''
|
||||
---
|
||||
module: aci_aaa_user
|
||||
short_description: Manage AAA users (aaa:User)
|
||||
description:
|
||||
- Manage AAA users.
|
||||
- More information from the internal APIC class I(aaa:User) at
|
||||
U(https://developer.cisco.com/docs/apic-mim-ref/).
|
||||
author:
|
||||
- Dag Wieers (@dagwieers)
|
||||
notes:
|
||||
- This module is not idempotent when C(aaa_password) is being used
|
||||
(even if that password was already set identically). This
|
||||
appears to be an inconsistency wrt. the idempotent nature
|
||||
of the APIC REST API.
|
||||
requirements:
|
||||
- python-dateutil
|
||||
version_added: '2.5'
|
||||
options:
|
||||
aaa_password:
|
||||
description:
|
||||
- The password of the locally-authenticated user.
|
||||
aaa_password_lifetime:
|
||||
description:
|
||||
- The lifetime of the locally-authenticated user password.
|
||||
aaa_password_update_required:
|
||||
description:
|
||||
- Whether this account needs password update.
|
||||
type: bool
|
||||
aaa_user:
|
||||
description:
|
||||
- The name of the locally-authenticated user user to add.
|
||||
aliases: [ name, user ]
|
||||
clear_password_history:
|
||||
description:
|
||||
- Whether to clear the password history of a locally-authenticated user.
|
||||
type: bool
|
||||
description:
|
||||
description:
|
||||
- Description for the AAA user.
|
||||
aliases: [ descr ]
|
||||
email:
|
||||
description:
|
||||
- The email address of the locally-authenticated user.
|
||||
enabled:
|
||||
description:
|
||||
- The status of the locally-authenticated user account.
|
||||
type: bool
|
||||
expiration:
|
||||
description:
|
||||
- The expiration date of the locally-authenticated user account.
|
||||
expires:
|
||||
description:
|
||||
- Whether to enable an expiration date for the locally-authenticated user account.
|
||||
type: bool
|
||||
first_name:
|
||||
description:
|
||||
- The first name of the locally-authenticated user.
|
||||
last_name:
|
||||
description:
|
||||
- The last name of the locally-authenticated user.
|
||||
phone:
|
||||
description:
|
||||
- The phone number of the locally-authenticated user.
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
- Use C(query) for listing an object or multiple objects.
|
||||
choices: [ absent, present, query ]
|
||||
default: present
|
||||
extends_documentation_fragment: aci
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Add a user
|
||||
aci_aaa_user:
|
||||
host: apic
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
aaa_user: dag
|
||||
aaa_password: AnotherSecretPassword
|
||||
expiration: never
|
||||
expires: no
|
||||
email: dag@wieers.com
|
||||
phone: 1-234-555-678
|
||||
first_name: Dag
|
||||
last_name: Wieers
|
||||
state: present
|
||||
|
||||
- name: Remove a user
|
||||
aci_aaa_user:
|
||||
host: apic
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
aaa_user: dag
|
||||
state: absent
|
||||
|
||||
- name: Query a user
|
||||
aci_aaa_user:
|
||||
host: apic
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
aaa_user: dag
|
||||
state: query
|
||||
|
||||
- name: Query all users
|
||||
aci_aaa_user:
|
||||
host: apic
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
state: query
|
||||
'''
|
||||
|
||||
RETURN = r''' # '''
|
||||
|
||||
from ansible.module_utils.network.aci.aci import ACIModule, aci_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
try:
|
||||
from dateutil.tz import tzutc
|
||||
import dateutil.parser
|
||||
HAS_DATEUTIL = True
|
||||
except ImportError:
|
||||
HAS_DATEUTIL = False
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = aci_argument_spec()
|
||||
argument_spec.update(
|
||||
aaa_password=dict(type='str', no_log=True),
|
||||
aaa_password_lifetime=dict(type='int'),
|
||||
aaa_password_update_required=dict(type='bool'),
|
||||
aaa_user=dict(type='str', required=True, aliases=['name']),
|
||||
clear_password_history=dict(type='bool'),
|
||||
description=dict(type='str', aliases=['descr']),
|
||||
email=dict(type='str'),
|
||||
enabled=dict(type='bool'),
|
||||
expiration=dict(type='str'),
|
||||
expires=dict(type='bool'),
|
||||
first_name=dict(type='str'),
|
||||
last_name=dict(type='str'),
|
||||
phone=dict(type='str'),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present', 'query']),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_if=[
|
||||
['state', 'absent', ['aaa_user']],
|
||||
['state', 'present', ['aaa_user']],
|
||||
['expires', True, ['expiration']],
|
||||
],
|
||||
)
|
||||
|
||||
if not HAS_DATEUTIL:
|
||||
module.fail_json(msg='dateutil required for this module')
|
||||
|
||||
aaa_password = module.params['aaa_password']
|
||||
aaa_password_lifetime = module.params['aaa_password_lifetime']
|
||||
aaa_password_update_required = module.params['aaa_password_update_required']
|
||||
aaa_user = module.params['aaa_user']
|
||||
clear_password_history = module.params['clear_password_history']
|
||||
description = module.params['description']
|
||||
email = module.params['email']
|
||||
enabled = module.params['enabled']
|
||||
first_name = module.params['first_name']
|
||||
last_name = module.params['last_name']
|
||||
phone = module.params['phone']
|
||||
state = module.params['state']
|
||||
|
||||
aci = ACIModule(module)
|
||||
|
||||
if module.params['enabled'] is True:
|
||||
enabled = 'active'
|
||||
elif module.params['enabled'] is False:
|
||||
enabled = 'inactive'
|
||||
else:
|
||||
enabled = None
|
||||
|
||||
expiration = module.params['expiration']
|
||||
if expiration is not None and expiration != 'never':
|
||||
try:
|
||||
expiration = aci.iso8601_format(dateutil.parser.parse(expiration).replace(tzinfo=tzutc()))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to parse date format '%s', %s" % (module.params['expiration'], e))
|
||||
|
||||
expires = module.params['expires']
|
||||
if expires is True:
|
||||
expires = 'yes'
|
||||
elif expires is False:
|
||||
expires = 'no'
|
||||
|
||||
aaa_password_update_required = module.params['aaa_password_update_required']
|
||||
if aaa_password_update_required is True:
|
||||
aaa_password_update_required = 'yes'
|
||||
elif aaa_password_update_required is False:
|
||||
aaa_password_update_required = 'no'
|
||||
|
||||
aci.construct_url(
|
||||
root_class=dict(
|
||||
aci_class='aaaUser',
|
||||
aci_rn='userext/user-{0}'.format(aaa_user),
|
||||
filter_target='eq(aaaUser.name, "{0}")'.format(aaa_user),
|
||||
module_object=aaa_user,
|
||||
),
|
||||
)
|
||||
aci.get_existing()
|
||||
|
||||
if state == 'present':
|
||||
# Filter out module params with null values
|
||||
aci.payload(
|
||||
aci_class='aaaUser',
|
||||
class_config=dict(
|
||||
accountStatus=enabled,
|
||||
clearPwdHistory=clear_password_history,
|
||||
email=email,
|
||||
expiration=expiration,
|
||||
expires=expires,
|
||||
firstName=first_name,
|
||||
lastName=last_name,
|
||||
name=aaa_user,
|
||||
phone=phone,
|
||||
pwd=aaa_password,
|
||||
pwdLifeTime=aaa_password_lifetime,
|
||||
pwdUpdateRequired=aaa_password_update_required,
|
||||
),
|
||||
)
|
||||
|
||||
# Generate config diff which will be used as POST request body
|
||||
aci.get_diff(aci_class='aaaUser')
|
||||
|
||||
# Submit changes if module not in check_mode and the proposed is different than existing
|
||||
aci.post_config()
|
||||
|
||||
elif state == 'absent':
|
||||
aci.delete_config()
|
||||
|
||||
module.exit_json(**aci.result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
0
test/integration/targets/aci_aaa_user/aliases
Normal file
0
test/integration/targets/aci_aaa_user/aliases
Normal file
175
test/integration/targets/aci_aaa_user/tasks/main.yml
Normal file
175
test/integration/targets/aci_aaa_user/tasks/main.yml
Normal file
|
@ -0,0 +1,175 @@
|
|||
# Test code for the ACI modules
|
||||
# Copyright: (c) 2017, Dag Wieers (dagwieers) <dag@wieers.com>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
- name: Test that we have an ACI APIC host, ACI username and ACI password
|
||||
fail:
|
||||
msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.'
|
||||
when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined
|
||||
|
||||
|
||||
# CLEAN ENVIRONMENT
|
||||
- name: Remove any pre-existing user
|
||||
aci_aaa_user: &user_absent
|
||||
host: '{{ aci_hostname }}'
|
||||
username: '{{ aci_username }}'
|
||||
password: '{{ aci_password }}'
|
||||
validate_certs: '{{ aci_validate_certs | default(false) }}'
|
||||
use_ssl: '{{ aci_use_ssl | default(true) }}'
|
||||
use_proxy: '{{ aci_use_proxy | default(true) }}'
|
||||
aaa_user: ansible
|
||||
state: absent
|
||||
|
||||
|
||||
# ADD USER
|
||||
- name: Add user (check_mode)
|
||||
aci_aaa_user: &user_present
|
||||
host: '{{ aci_hostname }}'
|
||||
username: '{{ aci_username }}'
|
||||
password: '{{ aci_password }}'
|
||||
validate_certs: '{{ aci_validate_certs | default(false) }}'
|
||||
use_ssl: '{{ aci_use_ssl | default(true) }}'
|
||||
use_proxy: '{{ aci_use_proxy | default(true) }}'
|
||||
aaa_user: ansible
|
||||
description: Ansible test user
|
||||
email: ansible@ansible.lan
|
||||
enabled: yes
|
||||
expiration: never
|
||||
expires: no
|
||||
first_name: Test
|
||||
last_name: User
|
||||
phone: 1-234-555-678
|
||||
check_mode: yes
|
||||
register: cm_add_user
|
||||
|
||||
# NOTE: Setting password is not idempotent, see #35544
|
||||
- name: Add user (normal mode)
|
||||
aci_aaa_user:
|
||||
<<: *user_present
|
||||
aaa_password: 12!Ab:cD!34
|
||||
register: nm_add_user
|
||||
|
||||
- name: Add user again (check mode)
|
||||
aci_aaa_user: *user_present
|
||||
check_mode: yes
|
||||
register: cm_add_user_again
|
||||
|
||||
- name: Add user again (normal mode)
|
||||
aci_aaa_user: *user_present
|
||||
register: nm_add_user_again
|
||||
|
||||
- name: Verify add user
|
||||
assert:
|
||||
that:
|
||||
- cm_add_user.changed == nm_add_user.changed == true
|
||||
- cm_add_user_again.changed == nm_add_user_again.changed == false
|
||||
|
||||
|
||||
# MODIFY USER
|
||||
- name: Modify user (check_mode)
|
||||
aci_aaa_user: &user_changed
|
||||
host: '{{ aci_hostname }}'
|
||||
username: '{{ aci_username }}'
|
||||
password: '{{ aci_password }}'
|
||||
validate_certs: '{{ aci_validate_certs | default(false) }}'
|
||||
use_ssl: '{{ aci_use_ssl | default(true) }}'
|
||||
use_proxy: '{{ aci_use_proxy | default(true) }}'
|
||||
aaa_user: ansible
|
||||
description: Ansible test user for integration tests
|
||||
email: aci-ansible@ansible.lan
|
||||
expiration: '2123-12-12'
|
||||
expires: yes
|
||||
phone: 2-345-555-678
|
||||
check_mode: yes
|
||||
register: cm_modify_user
|
||||
|
||||
- name: Modify user (normal mode)
|
||||
aci_aaa_user: *user_changed
|
||||
register: nm_modify_user
|
||||
|
||||
- name: Modify user again (check mode)
|
||||
aci_aaa_user: *user_changed
|
||||
check_mode: yes
|
||||
register: cm_modify_user_again
|
||||
|
||||
- name: Modify user again (normal mode)
|
||||
aci_aaa_user: *user_changed
|
||||
register: nm_modify_user_again
|
||||
|
||||
- name: Verify modify user
|
||||
assert:
|
||||
that:
|
||||
- cm_modify_user.changed == nm_modify_user.changed == true
|
||||
- cm_modify_user_again.changed == nm_modify_user_again.changed == false
|
||||
|
||||
|
||||
# QUERY ALL USERS
|
||||
- name: Query all users (check_mode)
|
||||
aci_aaa_user: &user_query
|
||||
host: '{{ aci_hostname }}'
|
||||
username: '{{ aci_username }}'
|
||||
password: '{{ aci_password }}'
|
||||
validate_certs: '{{ aci_validate_certs | default(false) }}'
|
||||
use_ssl: '{{ aci_use_ssl | default(true) }}'
|
||||
use_proxy: '{{ aci_use_proxy | default(true) }}'
|
||||
aaa_user: ansible
|
||||
state: query
|
||||
check_mode: yes
|
||||
register: cm_query_all_users
|
||||
|
||||
- name: Query all users (normal mode)
|
||||
aci_aaa_user: *user_query
|
||||
register: nm_query_all_users
|
||||
|
||||
- name: Verify query_all_users
|
||||
assert:
|
||||
that:
|
||||
- cm_query_all_users.changed == nm_query_all_users.changed == false
|
||||
# NOTE: Order of users is not stable between calls
|
||||
#- cm_query_all_users == nm_query_all_users
|
||||
|
||||
|
||||
# QUERY OUR USER
|
||||
- name: Query our user (check_mode)
|
||||
aci_aaa_user:
|
||||
<<: *user_query
|
||||
check_mode: yes
|
||||
register: cm_query_user
|
||||
|
||||
- name: Query our user (normal mode)
|
||||
aci_aaa_user:
|
||||
<<: *user_query
|
||||
register: nm_query_user
|
||||
|
||||
- name: Verify query_user
|
||||
assert:
|
||||
that:
|
||||
- cm_query_user.changed == nm_query_user.changed == false
|
||||
- cm_query_user == nm_query_user
|
||||
|
||||
|
||||
# REMOVE USER
|
||||
- name: Remove user (check_mode)
|
||||
aci_aaa_user: *user_absent
|
||||
check_mode: yes
|
||||
register: cm_remove_user
|
||||
|
||||
- name: Remove user (normal mode)
|
||||
aci_aaa_user: *user_absent
|
||||
register: nm_remove_user
|
||||
|
||||
- name: Remove user again (check_mode)
|
||||
aci_aaa_user: *user_absent
|
||||
check_mode: yes
|
||||
register: cm_remove_user_again
|
||||
|
||||
- name: Remove user again (normal mode)
|
||||
aci_aaa_user: *user_absent
|
||||
register: nm_remove_user_again
|
||||
|
||||
- name: Verify remove_user
|
||||
assert:
|
||||
that:
|
||||
- cm_remove_user.changed == nm_remove_user.changed == true
|
||||
- cm_remove_user_again.changed == nm_remove_user_again.changed == false
|
Loading…
Reference in a new issue