MSC: Various bugfixes and features enhancements (#50200)
* MSC: Various bugfixes and features enhancements This PR includes: - Lookups of roles, labels and domains - Auto-create new labels - Improvements to comparing complex datastructures - Force removal of sites - Support non top-level queries - Document internal functions - Add parameter types to modules - Fix documentation examples - Improvements to idempotency wrt. returning changed - Support site locations - Update permission list - Various improvements to integration tests * Fix Ci issues
This commit is contained in:
parent
f55481863d
commit
14b03ac15f
9 changed files with 295 additions and 91 deletions
|
@ -38,19 +38,30 @@ from ansible.module_utils._text import to_native, to_bytes
|
|||
|
||||
def issubset(subset, superset):
|
||||
''' Recurse through nested dictionary and compare entries '''
|
||||
|
||||
# Both objects are the same object
|
||||
if subset is superset:
|
||||
return True
|
||||
|
||||
# Both objects are identical
|
||||
if subset == superset:
|
||||
return True
|
||||
|
||||
# Both objects have a different type
|
||||
if type(subset) != type(superset):
|
||||
return False
|
||||
|
||||
for key, value in subset.items():
|
||||
# Item from subset is missing from superset
|
||||
if key not in superset:
|
||||
return False
|
||||
elif isinstance(value, str):
|
||||
if value != superset[key]:
|
||||
return False
|
||||
elif isinstance(value, dict):
|
||||
|
||||
# Item has different types in subset and superset
|
||||
if type(superset[key]) != type(value):
|
||||
return False
|
||||
|
||||
# Compare if item values are subset
|
||||
if isinstance(value, dict):
|
||||
if not issubset(superset[key], value):
|
||||
return False
|
||||
elif isinstance(value, list):
|
||||
|
@ -62,9 +73,16 @@ def issubset(subset, superset):
|
|||
else:
|
||||
if not value == superset[key]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_qs(params):
|
||||
''' Append key-value pairs to self.filter_string '''
|
||||
accepted_params = dict((k, v) for (k, v) in params.items() if v)
|
||||
return '?' + '&'.join(['%s=%s' % (k, v) for (k, v) in accepted_params.items()])
|
||||
|
||||
|
||||
def msc_argument_spec():
|
||||
return dict(
|
||||
host=dict(type='str', required=True, aliases=['hostname']),
|
||||
|
@ -146,7 +164,7 @@ class MSCModule(object):
|
|||
|
||||
self.headers['Authorization'] = 'Bearer {token}'.format(**payload)
|
||||
|
||||
def request(self, path, method=None, data=None):
|
||||
def request(self, path, method=None, data=None, qs=None):
|
||||
''' Generic HTTP method for MSC requests. '''
|
||||
self.path = path
|
||||
|
||||
|
@ -154,6 +172,10 @@ class MSCModule(object):
|
|||
self.method = method
|
||||
|
||||
self.url = urljoin(self.baseuri, path)
|
||||
|
||||
if qs is not None:
|
||||
self.url = self.url + update_qs(qs)
|
||||
|
||||
resp, info = fetch_url(self.module,
|
||||
self.url,
|
||||
headers=self.headers,
|
||||
|
@ -168,8 +190,8 @@ class MSCModule(object):
|
|||
# 200: OK, 201: Created, 202: Accepted, 204: No Content
|
||||
if self.status in (200, 201, 202, 204):
|
||||
output = resp.read()
|
||||
if self.method in ('DELETE', 'PATCH', 'POST', 'PUT') and self.status in (200, 201, 204):
|
||||
self.result['changed'] = True
|
||||
# if self.method in ('DELETE', 'PATCH', 'POST', 'PUT') and self.status in (200, 201, 204):
|
||||
# self.result['changed'] = True
|
||||
if output:
|
||||
return json.loads(output)
|
||||
|
||||
|
@ -192,20 +214,26 @@ class MSCModule(object):
|
|||
|
||||
return {}
|
||||
|
||||
def query_objs(self, path, **kwargs):
|
||||
def query_objs(self, path, key=None, **kwargs):
|
||||
''' Query the MSC REST API for objects in a path '''
|
||||
found = []
|
||||
objs = self.request(path, method='GET')
|
||||
for obj in objs[path]:
|
||||
for key in kwargs.keys():
|
||||
if kwargs[key] is None:
|
||||
|
||||
if key is None:
|
||||
key = path
|
||||
|
||||
for obj in objs[key]:
|
||||
for kw_key, kw_value in kwargs.items():
|
||||
if kw_value is None:
|
||||
continue
|
||||
if obj[key] != kwargs[key]:
|
||||
if obj[kw_key] != kw_value:
|
||||
break
|
||||
else:
|
||||
found.append(obj)
|
||||
return found
|
||||
|
||||
def get_obj(self, path, **kwargs):
|
||||
''' Get a specific object from a set of MSC REST objects '''
|
||||
objs = self.query_objs(path, **kwargs)
|
||||
if len(objs) == 0:
|
||||
return {}
|
||||
|
@ -213,7 +241,54 @@ class MSCModule(object):
|
|||
self.fail_json(msg='More than one object matches unique filter: {0}'.format(kwargs))
|
||||
return objs[0]
|
||||
|
||||
def lookup_domain(self, domain):
|
||||
''' Look up a domain and return its id '''
|
||||
if domain is None:
|
||||
return domain
|
||||
|
||||
d = self.get_obj('auth/domains', key='domains', name=domain)
|
||||
if not d:
|
||||
self.module.fail_json(msg="Domain '%s' is not valid." % domain)
|
||||
if 'id' not in d:
|
||||
self.module.fail_json(msg="Domain lookup failed for '%s': %s" % (domain, d))
|
||||
return d['id']
|
||||
|
||||
def lookup_roles(self, roles):
|
||||
''' Look up roles and return their ids '''
|
||||
if roles is None:
|
||||
return roles
|
||||
|
||||
ids = []
|
||||
for role in roles:
|
||||
r = self.get_obj('roles', name=role)
|
||||
if not r:
|
||||
self.module.fail_json(msg="Role '%s' is not valid." % role)
|
||||
if 'id' not in r:
|
||||
self.module.fail_json(msg="Role lookup failed for '%s': %s" % (role, r))
|
||||
ids.append(dict(roleId=r['id']))
|
||||
return ids
|
||||
|
||||
def create_label(self, label, label_type):
|
||||
''' Create a new label '''
|
||||
return self.request('labels', method='POST', data=dict(displayName=label, type=label_type))
|
||||
|
||||
def lookup_labels(self, labels, label_type):
|
||||
''' Look up labels and return their ids (create if necessary) '''
|
||||
if labels is None:
|
||||
return None
|
||||
|
||||
ids = []
|
||||
for label in labels:
|
||||
l = self.get_obj('labels', displayName=label)
|
||||
if not l:
|
||||
l = self.create_label(label, label_type)
|
||||
if 'id' not in l:
|
||||
self.module.fail_json(msg="Label lookup failed for '%s': %s" % (label, l))
|
||||
ids.append(l['id'])
|
||||
return ids
|
||||
|
||||
def sanitize(self, updates, collate=False, required_keys=None):
|
||||
''' Clean up unset keys from a request payload '''
|
||||
if required_keys is None:
|
||||
required_keys = []
|
||||
self.proposed = deepcopy(self.existing)
|
||||
|
|
|
@ -23,21 +23,25 @@ options:
|
|||
label_id:
|
||||
description:
|
||||
- The ID of the label.
|
||||
required: yes
|
||||
type: str
|
||||
label:
|
||||
description:
|
||||
- The name of the label.
|
||||
- Alternative to the name, you can use C(label_id).
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ label_name, name ]
|
||||
type:
|
||||
description:
|
||||
- The type of the label.
|
||||
type: str
|
||||
choices: [ site ]
|
||||
default: site
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
- Use C(query) for listing an object or multiple objects.
|
||||
type: str
|
||||
choices: [ absent, present, query ]
|
||||
default: present
|
||||
extends_documentation_fragment: msc
|
||||
|
@ -49,9 +53,8 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
label_id: 101
|
||||
description: North European Datacenter
|
||||
label: Belgium
|
||||
type: site
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -60,7 +63,7 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
label: Belgium
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -69,7 +72,7 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
label: Belgium
|
||||
state: query
|
||||
delegate_to: localhost
|
||||
register: query_result
|
||||
|
@ -151,11 +154,13 @@ def main():
|
|||
elif state == 'present':
|
||||
msc.previous = msc.existing
|
||||
|
||||
msc.sanitize(dict(
|
||||
payload = dict(
|
||||
id=label_id,
|
||||
displayName=label,
|
||||
type=label_type,
|
||||
), collate=True)
|
||||
)
|
||||
|
||||
msc.sanitize(payload, collate=True)
|
||||
|
||||
if msc.existing:
|
||||
if not issubset(msc.sent, msc.existing):
|
||||
|
|
|
@ -23,28 +23,39 @@ options:
|
|||
role_id:
|
||||
description:
|
||||
- The ID of the role.
|
||||
required: yes
|
||||
type: str
|
||||
role:
|
||||
description:
|
||||
- The name of the role.
|
||||
- Alternative to the name, you can use C(role_id).
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ name, role_name ]
|
||||
display_name:
|
||||
description:
|
||||
- The name of the role to be displayed in the web UI.
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- The description of the role.
|
||||
type: str
|
||||
permissions:
|
||||
description:
|
||||
- A list of permissions tied to this role.
|
||||
type: list
|
||||
choices:
|
||||
- backup-db
|
||||
- manage-audit-records
|
||||
- manage-labels
|
||||
- manage-roles
|
||||
- manage-schemas
|
||||
- manage-sites
|
||||
- manage-tenants
|
||||
- manage-tenant-schemas
|
||||
- manage-users
|
||||
- platform-logs
|
||||
- view-all-audit-records
|
||||
- view-labels
|
||||
- view-roles
|
||||
- view-schemas
|
||||
- view-sites
|
||||
|
@ -55,6 +66,7 @@ options:
|
|||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
- Use C(query) for listing an object or multiple objects.
|
||||
type: str
|
||||
choices: [ absent, present, query ]
|
||||
default: present
|
||||
extends_documentation_fragment: msc
|
||||
|
@ -66,9 +78,16 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
role_id: 101
|
||||
description: North European Datacenter
|
||||
role: readOnly
|
||||
display_name: Read Only
|
||||
description: Read-only access for troubleshooting
|
||||
permissions:
|
||||
- view-roles
|
||||
- view-schemas
|
||||
- view-sites
|
||||
- view-tenants
|
||||
- view-tenant-schemas
|
||||
- view-users
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -77,7 +96,7 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
role: readOnly
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -86,7 +105,7 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
role: readOnly
|
||||
state: query
|
||||
delegate_to: localhost
|
||||
register: query_result
|
||||
|
@ -116,11 +135,18 @@ def main():
|
|||
display_name=dict(type='str'),
|
||||
description=dict(type='str'),
|
||||
permissions=dict(type='list', choices=[
|
||||
'backup-db',
|
||||
'manage-audit-records',
|
||||
'manage-labels',
|
||||
'manage-roles',
|
||||
'manage-schemas',
|
||||
'manage-sites',
|
||||
'manage-tenants',
|
||||
'manage-tenant-schemas',
|
||||
'manage-users',
|
||||
'platform-logs',
|
||||
'view-all-audit-records',
|
||||
'view-labels',
|
||||
'view-roles',
|
||||
'view-schemas',
|
||||
'view-sites',
|
||||
|
@ -183,13 +209,15 @@ def main():
|
|||
elif state == 'present':
|
||||
msc.previous = msc.existing
|
||||
|
||||
msc.sanitize(dict(
|
||||
payload = dict(
|
||||
id=role_id,
|
||||
name=role,
|
||||
displayName=role,
|
||||
description=description,
|
||||
permissions=permissions,
|
||||
), collate=True)
|
||||
)
|
||||
|
||||
msc.sanitize(payload, collate=True)
|
||||
|
||||
if msc.existing:
|
||||
if not issubset(msc.sent, msc.existing):
|
||||
|
|
|
@ -40,17 +40,30 @@ options:
|
|||
description:
|
||||
- The ID of the site.
|
||||
type: str
|
||||
required: yes
|
||||
site:
|
||||
description:
|
||||
- The name of the site.
|
||||
- Alternative to the name, you can use C(site_id).
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ name, site_name ]
|
||||
labels:
|
||||
description:
|
||||
- The labels for this site.
|
||||
- Labels that do not already exist will be automatically created.
|
||||
type: list
|
||||
location:
|
||||
description:
|
||||
- Location of the site.
|
||||
suboptions:
|
||||
latitude:
|
||||
description:
|
||||
- The latitude of the location of the site.
|
||||
type: float
|
||||
longitude:
|
||||
description:
|
||||
- The longititude of the location of the site.
|
||||
type: float
|
||||
urls:
|
||||
description:
|
||||
- A list of URLs to reference the APICs.
|
||||
|
@ -72,8 +85,21 @@ EXAMPLES = r'''
|
|||
username: admin
|
||||
password: SomeSecretPassword
|
||||
site: north_europe
|
||||
site_id: 101
|
||||
description: North European Datacenter
|
||||
apic_username: msc_admin
|
||||
apic_password: AnotherSecretPassword
|
||||
apic_site_id: 12
|
||||
urls:
|
||||
- 10.2.3.4
|
||||
- 10.2.4.5
|
||||
- 10.3.5.6
|
||||
labels:
|
||||
- NEDC
|
||||
- Europe
|
||||
- Diegem
|
||||
location:
|
||||
latitude: 50.887318
|
||||
longitude: 4.447084
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -114,12 +140,18 @@ from ansible.module_utils.network.aci.msc import MSCModule, msc_argument_spec, i
|
|||
|
||||
|
||||
def main():
|
||||
location_arg_spec = dict(
|
||||
latitude=dict(type='float'),
|
||||
longitude=dict(type='float'),
|
||||
)
|
||||
|
||||
argument_spec = msc_argument_spec()
|
||||
argument_spec.update(
|
||||
apic_password=dict(type='str', no_log=True),
|
||||
apic_site_id=dict(type='str'),
|
||||
apic_username=dict(type='str', default='admin'),
|
||||
labels=dict(type='list'),
|
||||
location=dict(type='dict', options=location_arg_spec),
|
||||
site=dict(type='str', required=False, aliases=['name', 'site_name']),
|
||||
site_id=dict(type='str', required=False),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present', 'query']),
|
||||
|
@ -140,6 +172,10 @@ def main():
|
|||
apic_site_id = module.params['apic_site_id']
|
||||
site = module.params['site']
|
||||
site_id = module.params['site_id']
|
||||
location = module.params['location']
|
||||
if location is not None:
|
||||
latitude = module.params['location']['latitude']
|
||||
longitude = module.params['location']['longitude']
|
||||
state = module.params['state']
|
||||
urls = module.params['urls']
|
||||
|
||||
|
@ -147,6 +183,9 @@ def main():
|
|||
|
||||
path = 'sites'
|
||||
|
||||
# Convert labels
|
||||
labels = msc.lookup_labels(module.params['labels'], 'site')
|
||||
|
||||
# Query for msc.existing object(s)
|
||||
if site_id is None and site is None:
|
||||
msc.existing = msc.query_objs(path)
|
||||
|
@ -175,31 +214,43 @@ def main():
|
|||
if module.check_mode:
|
||||
msc.existing = {}
|
||||
else:
|
||||
msc.existing = msc.request(path, method='DELETE')
|
||||
msc.existing = msc.request(path, method='DELETE', qs=dict(force='true'))
|
||||
|
||||
elif state == 'present':
|
||||
msc.previous = msc.existing
|
||||
|
||||
msc.sanitize(dict(
|
||||
payload = dict(
|
||||
apicSiteId=apic_site_id,
|
||||
id=site_id,
|
||||
name=site,
|
||||
urls=urls,
|
||||
labels=labels,
|
||||
username=apic_username,
|
||||
password=apic_password,
|
||||
), collate=True)
|
||||
)
|
||||
|
||||
if location is not None:
|
||||
payload['location'] = dict(
|
||||
lat=latitude,
|
||||
long=longitude,
|
||||
)
|
||||
|
||||
msc.sanitize(payload, collate=True)
|
||||
|
||||
if msc.existing:
|
||||
if not issubset(msc.sent, msc.existing):
|
||||
if module.check_mode:
|
||||
msc.existing = msc.proposed
|
||||
else:
|
||||
msc.request(path, method='PUT', data=msc.sent)
|
||||
msc.existing = msc.request(path, method='PUT', data=msc.sent)
|
||||
else:
|
||||
if module.check_mode:
|
||||
msc.existing = msc.proposed
|
||||
else:
|
||||
msc.request(path, method='POST', data=msc.sent)
|
||||
msc.existing = msc.request(path, method='POST', data=msc.sent)
|
||||
|
||||
if 'password' in msc.existing:
|
||||
msc.existing['password'] = '******'
|
||||
|
||||
msc.exit_json()
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@ options:
|
|||
description:
|
||||
- The ID of the tenant.
|
||||
type: str
|
||||
required: yes
|
||||
tenant:
|
||||
description:
|
||||
- The name of the tenant.
|
||||
- Alternative to the name, you can use C(tenant_id).
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ name, tenant_name ]
|
||||
|
@ -161,14 +161,20 @@ def main():
|
|||
elif state == 'present':
|
||||
msc.previous = msc.existing
|
||||
|
||||
msc.sanitize(dict(
|
||||
payload = dict(
|
||||
description=description,
|
||||
id=tenant_id,
|
||||
name=tenant,
|
||||
displayName=display_name,
|
||||
siteAssociations=[],
|
||||
userAssociations=[dict(userId="0000ffff0000000000000020")],
|
||||
), collate=True)
|
||||
)
|
||||
|
||||
msc.sanitize(payload, collate=True)
|
||||
|
||||
# Ensure displayName is not undefined
|
||||
if msc.sent.get('displayName') is None:
|
||||
msc.sent['displayName'] = tenant
|
||||
|
||||
if msc.existing:
|
||||
if not issubset(msc.sent, msc.existing):
|
||||
|
|
|
@ -23,55 +23,87 @@ options:
|
|||
user_id:
|
||||
description:
|
||||
- The ID of the user.
|
||||
required: yes
|
||||
type: str
|
||||
user:
|
||||
description:
|
||||
- The name of the user.
|
||||
- Alternative to the name, you can use C(user_id).
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ name, user_name ]
|
||||
user_password:
|
||||
description:
|
||||
- The password of the user.
|
||||
type: str
|
||||
first_name:
|
||||
description:
|
||||
- The first name of the user.
|
||||
- This parameter is required when creating new users.
|
||||
type: str
|
||||
last_name:
|
||||
description:
|
||||
- The last name of the user.
|
||||
- This parameter is required when creating new users.
|
||||
type: str
|
||||
email:
|
||||
description:
|
||||
- The email address of the user.
|
||||
- This parameter is required when creating new users.
|
||||
type: str
|
||||
phone:
|
||||
description:
|
||||
- The phone number of the user.
|
||||
- This parameter is required when creating new users.
|
||||
type: str
|
||||
account_status:
|
||||
description:
|
||||
- The status of the user account.
|
||||
type: str
|
||||
choices: [ active ]
|
||||
domain:
|
||||
description:
|
||||
- The domain this user belongs to.
|
||||
- When creating new users, this defaults to C(Local).
|
||||
type: str
|
||||
roles:
|
||||
description:
|
||||
- The roles this user has.
|
||||
- The roles for this user.
|
||||
type: list
|
||||
state:
|
||||
description:
|
||||
- Use C(present) or C(absent) for adding or removing.
|
||||
- Use C(query) for listing an object or multiple objects.
|
||||
type: str
|
||||
choices: [ absent, present, query ]
|
||||
default: present
|
||||
notes:
|
||||
- A default installation of ACI Multi-Site ships with admin password 'we1come!' which requires a password change on first login.
|
||||
See the examples of how to change the 'admin' password using Ansible.
|
||||
extends_documentation_fragment: msc
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Update initial admin password
|
||||
msc_user:
|
||||
host: msc_host
|
||||
username: admin
|
||||
password: we1come!
|
||||
user_name: admin
|
||||
user_password: SomeSecretPassword
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Add a new user
|
||||
msc_user:
|
||||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
user_id: 101
|
||||
description: North European Datacenter
|
||||
user_name: dag
|
||||
description: Test user
|
||||
first_name: Dag
|
||||
last_name: Wieers
|
||||
email: dag@wieers.com
|
||||
phone: +32 478 436 299
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -80,7 +112,7 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
user_name: dag
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
|
||||
|
@ -89,7 +121,7 @@ EXAMPLES = r'''
|
|||
host: msc_host
|
||||
username: admin
|
||||
password: SomeSecretPassword
|
||||
name: north_europe
|
||||
user_name: dag
|
||||
state: query
|
||||
delegate_to: localhost
|
||||
register: query_result
|
||||
|
@ -120,7 +152,7 @@ def main():
|
|||
last_name=dict(type='str'),
|
||||
email=dict(type='str'),
|
||||
phone=dict(type='str'),
|
||||
# FIXME: What possible options do we have ?
|
||||
# TODO: What possible options do we have ?
|
||||
account_status=dict(type='str', choices=['active']),
|
||||
domain=dict(type='str'),
|
||||
roles=dict(type='list'),
|
||||
|
@ -132,7 +164,7 @@ def main():
|
|||
supports_check_mode=True,
|
||||
required_if=[
|
||||
['state', 'absent', ['user_name']],
|
||||
['state', 'present', ['user_name', 'password', 'first_name', 'last_name', 'email', 'phone', 'account_status']],
|
||||
['state', 'present', ['user_name']],
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -144,18 +176,13 @@ def main():
|
|||
email = module.params['email']
|
||||
phone = module.params['phone']
|
||||
account_status = module.params['account_status']
|
||||
# FIXME: Look up domain
|
||||
domain = module.params['domain']
|
||||
# FIXME: Look up roles
|
||||
roles = module.params['roles']
|
||||
roles_dict = list()
|
||||
if roles:
|
||||
for role in roles:
|
||||
roles_dict.append(dict(roleId=role))
|
||||
state = module.params['state']
|
||||
|
||||
msc = MSCModule(module)
|
||||
|
||||
roles = msc.lookup_roles(module.params['roles'])
|
||||
domain = msc.lookup_domain(module.params['domain'])
|
||||
|
||||
path = 'users'
|
||||
|
||||
# Query for existing object(s)
|
||||
|
@ -191,7 +218,7 @@ def main():
|
|||
elif state == 'present':
|
||||
msc.previous = msc.existing
|
||||
|
||||
msc.sanitize(dict(
|
||||
payload = dict(
|
||||
id=user_id,
|
||||
username=user_name,
|
||||
password=user_password,
|
||||
|
@ -199,19 +226,23 @@ def main():
|
|||
lastName=last_name,
|
||||
emailAddress=email,
|
||||
phoneNumber=phone,
|
||||
# accountStatus={},
|
||||
accountStatus=account_status,
|
||||
needsPasswordUpdate=False,
|
||||
domainId=domain,
|
||||
roles=roles_dict,
|
||||
roles=roles,
|
||||
# active=True,
|
||||
# remote=True,
|
||||
), collate=True)
|
||||
)
|
||||
|
||||
msc.sanitize(payload, collate=True)
|
||||
|
||||
if msc.sent.get('accountStatus') is None:
|
||||
msc.sent['accountStatus'] = 'active'
|
||||
|
||||
if msc.existing:
|
||||
if not issubset(msc.sent, msc.existing):
|
||||
# NOTE: Since MSC always returns '******' as password, we need to assume a change
|
||||
if 'password' in msc.sent:
|
||||
if 'password' in msc.proposed:
|
||||
msc.module.warn("A password change is assumed, as the MSC REST API does not return passwords we do not know.")
|
||||
msc.result['changed'] = True
|
||||
|
||||
if module.check_mode:
|
||||
|
|
|
@ -48,18 +48,18 @@ options:
|
|||
description:
|
||||
- If C(no), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
|
||||
type: bool
|
||||
default: 'yes'
|
||||
default: yes
|
||||
use_ssl:
|
||||
description:
|
||||
- If C(no), an HTTP connection will be used instead of the default HTTPS connection.
|
||||
type: bool
|
||||
default: 'yes'
|
||||
default: yes
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated.
|
||||
- This should only set to C(no) when used on personally controlled sites using self-signed certificates.
|
||||
type: bool
|
||||
default: 'yes'
|
||||
default: yes
|
||||
notes:
|
||||
- Please read the :ref:`aci_guide` for more detailed information on how to manage your ACI infrastructure using Ansible.
|
||||
'''
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
|
||||
# CLEAN ENVIRONMENT
|
||||
- name: Remove site ansible_test2
|
||||
- name: Remove site 2
|
||||
msc_site: &site_absent
|
||||
host: '{{ msc_hostname }}'
|
||||
username: '{{ msc_username }}'
|
||||
|
@ -19,13 +19,13 @@
|
|||
use_ssl: '{{ msc_use_ssl | default(true) }}'
|
||||
use_proxy: '{{ msc_use_proxy | default(true) }}'
|
||||
output_level: '{{ msc_output_level | default("info") }}'
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
state: absent
|
||||
|
||||
- name: Remove site ansible_test
|
||||
- name: Remove site
|
||||
msc_site:
|
||||
<<: *site_absent
|
||||
site: ansible_test
|
||||
site: '{{ msc_site | default("ansible_test") }}'
|
||||
register: cm_remove_site
|
||||
|
||||
|
||||
|
@ -39,12 +39,19 @@
|
|||
use_ssl: '{{ msc_use_ssl | default(true) }}'
|
||||
use_proxy: '{{ msc_use_proxy | default(true) }}'
|
||||
output_level: '{{ msc_output_level | default("info") }}'
|
||||
site: ansible_test
|
||||
site: '{{ msc_site | default("ansible_test") }}'
|
||||
apic_username: admin
|
||||
apic_password: '{{ apic_password }}'
|
||||
apic_site_id: 101
|
||||
urls:
|
||||
- https://{{ apic_hostname }}/
|
||||
- https://{{ apic_hostname }}
|
||||
location:
|
||||
latitude: 50.887318
|
||||
longitude: 4.447084
|
||||
labels:
|
||||
- Diegem
|
||||
- EMEA
|
||||
- POD51
|
||||
state: present
|
||||
check_mode: yes
|
||||
register: cm_add_site
|
||||
|
@ -55,7 +62,7 @@
|
|||
- cm_add_site is changed
|
||||
- cm_add_site.previous == {}
|
||||
- cm_add_site.current.id is not defined
|
||||
- cm_add_site.current.name == 'ansible_test'
|
||||
- cm_add_site.current.name == msc_site|default("ansible_test")
|
||||
|
||||
- name: Add site (normal mode)
|
||||
msc_site: *site_present
|
||||
|
@ -67,7 +74,7 @@
|
|||
- nm_add_site is changed
|
||||
- nm_add_site.previous == {}
|
||||
- nm_add_site.current.id is defined
|
||||
- nm_add_site.current.name == 'ansible_test'
|
||||
- nm_add_site.current.name == msc_site|default("ansible_test")
|
||||
|
||||
- name: Add site again (check_mode)
|
||||
msc_site: *site_present
|
||||
|
@ -78,9 +85,9 @@
|
|||
assert:
|
||||
that:
|
||||
- cm_add_site_again is not changed
|
||||
- cm_add_site_again.previous.name == 'ansible_test'
|
||||
- cm_add_site_again.previous.name == msc_site|default("ansible_test")
|
||||
- cm_add_site_again.current.id == nm_add_site.current.id
|
||||
- cm_add_site_again.current.name == 'ansible_test'
|
||||
- cm_add_site_again.current.name == msc_site|default("ansible_test")
|
||||
|
||||
- name: Add site again (normal mode)
|
||||
msc_site: *site_present
|
||||
|
@ -90,9 +97,9 @@
|
|||
assert:
|
||||
that:
|
||||
- nm_add_site_again is not changed
|
||||
- nm_add_site_again.previous.name == 'ansible_test'
|
||||
- nm_add_site_again.previous.name == msc_site|default("ansible_test")
|
||||
- nm_add_site_again.current.id == nm_add_site.current.id
|
||||
- nm_add_site_again.current.name == 'ansible_test'
|
||||
- nm_add_site_again.current.name == msc_site|default("ansible_test")
|
||||
|
||||
|
||||
# CHANGE SITE
|
||||
|
@ -100,7 +107,7 @@
|
|||
msc_site:
|
||||
<<: *site_present
|
||||
site_id: '{{ nm_add_site.current.id }}'
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
check_mode: yes
|
||||
register: cm_change_site
|
||||
|
||||
|
@ -109,13 +116,13 @@
|
|||
that:
|
||||
- cm_change_site is changed
|
||||
- cm_change_site.current.id == nm_add_site.current.id
|
||||
- cm_change_site.current.name == 'ansible_test2'
|
||||
- cm_change_site.current.name == '{{ msc_site | default("ansible_test") }}_2'
|
||||
|
||||
- name: Change site (normal mode)
|
||||
msc_site:
|
||||
<<: *site_present
|
||||
site_id: '{{ nm_add_site.current.id }}'
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
output_level: debug
|
||||
register: nm_change_site
|
||||
|
||||
|
@ -124,13 +131,13 @@
|
|||
that:
|
||||
- nm_change_site is changed
|
||||
- nm_change_site.current.id == nm_add_site.current.id
|
||||
- nm_change_site.current.name == 'ansible_test2'
|
||||
- nm_change_site.current.name == '{{ msc_site | default("ansible_test") }}_2'
|
||||
|
||||
- name: Change site again (check_mode)
|
||||
msc_site:
|
||||
<<: *site_present
|
||||
site_id: '{{ nm_add_site.current.id }}'
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
check_mode: yes
|
||||
register: cm_change_site_again
|
||||
|
||||
|
@ -139,13 +146,13 @@
|
|||
that:
|
||||
- cm_change_site_again is not changed
|
||||
- cm_change_site_again.current.id == nm_add_site.current.id
|
||||
- cm_change_site_again.current.name == 'ansible_test2'
|
||||
- cm_change_site_again.current.name == '{{ msc_site | default("ansible_test") }}_2'
|
||||
|
||||
- name: Change site again (normal mode)
|
||||
msc_site:
|
||||
<<: *site_present
|
||||
site_id: '{{ nm_add_site.current.id }}'
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
register: nm_change_site_again
|
||||
|
||||
- name: Verify nm_change_site_again
|
||||
|
@ -153,7 +160,7 @@
|
|||
that:
|
||||
- nm_change_site_again is not changed
|
||||
- nm_change_site_again.current.id == nm_add_site.current.id
|
||||
- nm_change_site_again.current.name == 'ansible_test2'
|
||||
- nm_change_site_again.current.name == '{{ msc_site | default("ansible_test") }}_2'
|
||||
|
||||
|
||||
# QUERY ALL SITES
|
||||
|
@ -187,14 +194,14 @@
|
|||
- name: Query our site
|
||||
msc_site:
|
||||
<<: *site_query
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
check_mode: yes
|
||||
register: cm_query_site
|
||||
|
||||
- name: Query our site
|
||||
msc_site:
|
||||
<<: *site_query
|
||||
site: ansible_test2
|
||||
site: '{{ msc_site | default("ansible_test") }}_2'
|
||||
register: nm_query_site
|
||||
|
||||
- name: Verify query_site
|
||||
|
@ -202,10 +209,10 @@
|
|||
that:
|
||||
- cm_query_site is not changed
|
||||
- cm_query_site.current.id == nm_add_site.current.id
|
||||
- cm_query_site.current.name == 'ansible_test2'
|
||||
- cm_query_site.current.name == '{{ msc_site | default("ansible_test") }}_2'
|
||||
- nm_query_site is not changed
|
||||
- nm_query_site.current.id == nm_add_site.current.id
|
||||
- nm_query_site.current.name == 'ansible_test2'
|
||||
- nm_query_site.current.name == '{{ msc_site | default("ansible_test") }}_2'
|
||||
- cm_query_site == nm_query_site
|
||||
|
||||
|
||||
|
@ -257,14 +264,14 @@
|
|||
- name: Query non-existing site (check_mode)
|
||||
msc_site:
|
||||
<<: *site_query
|
||||
site: ansible_test
|
||||
site: '{{ msc_site | default("ansible_test") }}'
|
||||
check_mode: yes
|
||||
register: cm_query_non_site
|
||||
|
||||
- name: Query non-existing site (normal mode)
|
||||
msc_site:
|
||||
<<: *site_query
|
||||
site: ansible_test
|
||||
site: '{{ msc_site | default("ansible_test") }}'
|
||||
register: nm_query_non_site
|
||||
|
||||
# TODO: Implement more tests
|
||||
|
|
|
@ -48,7 +48,8 @@
|
|||
phone: +32 478 436 299
|
||||
account_status: active
|
||||
roles:
|
||||
- 0000ffff0000000000000031
|
||||
- powerUser
|
||||
domain: Local
|
||||
|
||||
state: present
|
||||
check_mode: yes
|
||||
|
|
Loading…
Reference in a new issue