From c00554735fdc4ba68d60d11955542f4c35936c2c Mon Sep 17 00:00:00 2001 From: Philippe Dellaert Date: Tue, 25 Jul 2017 13:35:03 +0200 Subject: [PATCH] New module: management of the Nuage Networks VSP SDN solution (network/nuage/nuage_vspk) (#24895) * Nuage module and unit tests with requested changes * Cleanup of imports * Adding check on python version * Adding import try and catch wrappers * Cleanup of requirements and adding integration tests * Using pypi package for simulator * Cleanup of requirements and adding integration tests * Adding aliases for integration tests * Adding module to import sanity test skip list * Revert "Adding module to import sanity test skip list" This reverts commit eab23af8c5ca7c503af63c05610b5db66d31fae4. * Adding check for importlib and cleanup of requirements --- lib/ansible/modules/network/nuage/__init__.py | 0 .../modules/network/nuage/nuage_vspk.py | 1046 +++++++++++++ test/integration/network-all.yaml | 1 + test/integration/nuage.yaml | 11 + test/integration/targets/nuage_vspk/aliases | 1 + .../targets/nuage_vspk/defaults/main.yaml | 9 + .../targets/nuage_vspk/meta/main.yaml | 2 + .../targets/nuage_vspk/tasks/main.yaml | 17 + .../targets/nuage_vspk/tests/basic.yaml | 226 +++ .../prepare_nuage_tests/tasks/main.yml | 9 + test/units/modules/network/nuage/__init__.py | 0 .../modules/network/nuage/nuage_module.py | 100 ++ .../modules/network/nuage/test_nuage_vspk.py | 1382 +++++++++++++++++ 13 files changed, 2804 insertions(+) create mode 100644 lib/ansible/modules/network/nuage/__init__.py create mode 100644 lib/ansible/modules/network/nuage/nuage_vspk.py create mode 100644 test/integration/nuage.yaml create mode 100644 test/integration/targets/nuage_vspk/aliases create mode 100644 test/integration/targets/nuage_vspk/defaults/main.yaml create mode 100644 test/integration/targets/nuage_vspk/meta/main.yaml create mode 100644 test/integration/targets/nuage_vspk/tasks/main.yaml create mode 100644 test/integration/targets/nuage_vspk/tests/basic.yaml create mode 100644 test/integration/targets/prepare_nuage_tests/tasks/main.yml create mode 100644 test/units/modules/network/nuage/__init__.py create mode 100644 test/units/modules/network/nuage/nuage_module.py create mode 100644 test/units/modules/network/nuage/test_nuage_vspk.py diff --git a/lib/ansible/modules/network/nuage/__init__.py b/lib/ansible/modules/network/nuage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/network/nuage/nuage_vspk.py b/lib/ansible/modules/network/nuage/nuage_vspk.py new file mode 100644 index 00000000000..efe3e219110 --- /dev/null +++ b/lib/ansible/modules/network/nuage/nuage_vspk.py @@ -0,0 +1,1046 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Nokia +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0'} + +DOCUMENTATION = ''' +--- +module: nuage_vspk +short_description: Manage Nuage VSP environments +description: + - Manage or find Nuage VSP entities, this includes create, update, delete, assign, unassign and find, with all supported properties. +version_added: "2.4" +author: Philippe Dellaert (@pdellaert) +options: + auth: + description: + - Dict with the authentication information required to connect to a Nuage VSP environment. + - Requires a I(api_username) parameter (example csproot). + - Requires either a I(api_password) parameter (example csproot) or a I(api_certificate) and I(api_key) parameters, + which point to the certificate and key files for certificate based authentication. + - Requires a I(api_enterprise) parameter (example csp). + - Requires a I(api_url) parameter (example https://10.0.0.10:8443). + - Requires a I(api_version) parameter (example v4_0). + required: true + default: null + type: + description: + - The type of entity you want to work on (example Enterprise). + - This should match the objects CamelCase class name in VSPK-Python. + - This Class name can be found on U(https://nuagenetworks.github.io/vspkdoc/html/index.html). + required: true + default: null + id: + description: + - The ID of the entity you want to work on. + - In combination with I(command=find), it will only return the single entity. + - In combination with I(state), it will either update or delete this entity. + - Will take precedence over I(match_filter) and I(properties) whenever an entity needs to be found. + required: false + default: null + parent_id: + description: + - The ID of the parent of the entity you want to work on. + - When I(state) is specified, the entity will be gathered from this parent, if it exists, unless an I(id) is specified. + - When I(command=find) is specified, the entity will be searched for in this parent, unless an I(id) is specified. + - If specified, I(parent_type) also needs to be specified. + required: false + default: null + parent_type: + description: + - The type of parent the ID is specified for (example Enterprise). + - This should match the objects CamelCase class name in VSPK-Python. + - This Class name can be found on U(https://nuagenetworks.github.io/vspkdoc/html/index.html). + - If specified, I(parent_id) also needs to be specified. + required: false + default: null + state: + description: + - Specifies the desired state of the entity. + - If I(state=present), in case the entity already exists, will update the entity if it is needed. + - If I(state=present), in case the relationship with the parent is a member relationship, will assign the entity as a member of the parent. + - If I(state=absent), in case the relationship with the parent is a member relationship, will unassign the entity as a member of the parent. + - Either I(state) or I(command) needs to be defined, both can not be defined at the same time. + required: false + default: null + choices: + - present + - absent + command: + description: + - Specifies a command to be executed. + - With I(command=find), if I(parent_id) and I(parent_type) are defined, it will only search within the parent. Otherwise, if allowed, + will search in the root object. + - With I(command=find), if I(id) is specified, it will only return the single entity matching the id. + - With I(command=find), otherwise, if I(match_filter) is define, it will use that filter to search. + - With I(command=find), otherwise, if I(properties) are defined, it will do an AND search using all properties. + - With I(command=change_password), a password of a user can be changed. Warning - In case the password is the same as the existing, + it will throw an error. + - With I(command=wait_for_job), the module will wait for a job to either have a status of SUCCESS or ERROR. In case an ERROR status is found, + the module will exit with an error. + - With I(command=wait_for_job), the job will always be returned, even if the state is ERROR situation. + - Either I(state) or I(command) needs to be defined, both can not be defined at the same time. + required: false + default: null + choices: + - find + - change_password + - wait_for_job + - get_csp_enterprise + match_filter: + description: + - A filter used when looking (both in I(command) and I(state) for entities, in the format the Nuage VSP API expects. + - If I(match_filter) is defined, it will take precedence over the I(properties), but not on the I(id) + required: false + default: null + properties: + description: + - Properties are the key, value pairs of the different properties an entity has. + - If no I(id) and no I(match_filter) is specified, these are used to find or determine if the entity exists. + required: false + default: null + children: + description: + - Can be used to specify a set of child entities. + - A mandatory property of each child is the I(type). + - Supported optional properties of each child are I(id), I(properties) and I(match_filter). + - The function of each of these properties is the same as in the general task definition. + - This can be used recursively + - Only useable in case I(state=present). + required: false + default: null +notes: + - Check mode is supported, but with some caveats. It will not do any changes, and if possible try to determine if it is able do what is requested. + - In case a parent id is provided from a previous task, it might be empty and if a search is possible on root, it will do so, which can impact performance. +requirements: + - Python 2.7 + - Supports Nuage VSP 4.0Rx & 5.x.y + - Proper VSPK-Python installed for your Nuage version + - Tested with NuageX U(https://nuagex.io) +''' + +EXAMPLES = ''' +# This can be executed as a single role, with the following vars +# vars: +# auth: +# api_username: csproot +# api_password: csproot +# api_enterprise: csp +# api_url: https://10.0.0.10:8443 +# api_version: v5_0 +# enterprise_name: Ansible-Enterprise +# enterprise_new_name: Ansible-Updated-Enterprise +# +# or, for certificate based authentication +# vars: +# auth: +# api_username: csproot +# api_certificate: /path/to/user-certificate.pem +# api_key: /path/to/user-Key.pem +# api_enterprise: csp +# api_url: https://10.0.0.10:8443 +# api_version: v5_0 +# enterprise_name: Ansible-Enterprise +# enterprise_new_name: Ansible-Updated-Enterprise + +# Creating a new enterprise +- name: Create Enterprise + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + state: present + properties: + name: "{{ enterprise_name }}-basic" + register: nuage_enterprise + +# Checking if an Enterprise with the new name already exists +- name: Check if an Enterprise exists with the new name + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + command: find + properties: + name: "{{ enterprise_new_name }}-basic" + ignore_errors: yes + register: nuage_check_enterprise + +# Updating an enterprise's name +- name: Update Enterprise name + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + id: "{{ nuage_enterprise.id }}" + state: present + properties: + name: "{{ enterprise_new_name }}-basic" + when: nuage_check_enterprise | failed + +# Creating a User in an Enterprise +- name: Create admin user + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + parent_id: "{{ nuage_enterprise.id }}" + parent_type: Enterprise + state: present + match_filter: "userName == 'ansible-admin'" + properties: + email: "ansible@localhost.local" + first_name: "Ansible" + last_name: "Admin" + password: "ansible-password" + user_name: "ansible-admin" + register: nuage_user + +# Updating password for User +- name: Update admin password + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + command: change_password + properties: + password: "ansible-new-password" + ignore_errors: yes + +# Finding a group in an enterprise +- name: Find Administrators group in Enterprise + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Group + parent_id: "{{ nuage_enterprise.id }}" + parent_type: Enterprise + command: find + properties: + name: "Administrators" + register: nuage_group + +# Assign the user to the group +- name: Assign admin user to administrators + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + parent_id: "{{ nuage_group.id }}" + parent_type: Group + state: present + +# Creating multiple DomainTemplates +- name: Create multiple DomainTemplates + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: DomainTemplate + parent_id: "{{ nuage_enterprise.id }}" + parent_type: Enterprise + state: present + properties: + name: "{{ item }}" + description: "Created by Ansible" + with_items: + - "Template-1" + - "Template-2" + +# Finding all DomainTemplates +- name: Fetching all DomainTemplates + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: DomainTemplate + parent_id: "{{ nuage_enterprise.id }}" + parent_type: Enterprise + command: find + register: nuage_domain_templates + +# Deleting all DomainTemplates +- name: Deleting all found DomainTemplates + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: DomainTemplate + state: absent + id: "{{ item.ID }}" + with_items: "{{ nuage_domain_templates.entities }}" + when: nuage_domain_templates.entities is defined + +# Unassign user from group +- name: Unassign admin user to administrators + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + parent_id: "{{ nuage_group.id }}" + parent_type: Group + state: absent + +# Deleting an enterprise +- name: Delete Enterprise + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + id: "{{ nuage_enterprise.id }}" + state: absent + +# Setup an enterprise with Children +- name: Setup Enterprise and domain structure + connection: local + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + state: present + properties: + name: "Child-based-Enterprise" + children: + - type: L2DomainTemplate + properties: + name: "Unmanaged-Template" + children: + - type: EgressACLTemplate + match_filter: "name == 'Allow All'" + properties: + name: "Allow All" + active: true + default_allow_ip: true + default_allow_non_ip: true + default_install_acl_implicit_rules: true + description: "Created by Ansible" + priority_type: "TOP" + - type: IngressACLTemplate + match_filter: "name == 'Allow All'" + properties: + name: "Allow All" + active: true + default_allow_ip: true + default_allow_non_ip: true + description: "Created by Ansible" + priority_type: "TOP" +''' + +RETURN = ''' +id: + description: The id of the entity that was found, created, updated or assigned. + returned: On state=present and command=find in case one entity was found. + type: string + sample: bae07d8d-d29c-4e2b-b6ba-621b4807a333 +entities: + description: A list of entities handled. Each element is the to_dict() of the entity. + returned: On state=present and find, with only one element in case of state=present or find=one. + type: list + sample: [{ + "ID": acabc435-3946-4117-a719-b8895a335830", + "assocEntityType": "DOMAIN", + "command": "BEGIN_POLICY_CHANGES", + "creationDate": 1487515656000, + "entityScope": "ENTERPRISE", + "externalID": null, + "lastUpdatedBy": "8a6f0e20-a4db-4878-ad84-9cc61756cd5e", + "lastUpdatedDate": 1487515656000, + "owner": "8a6f0e20-a4db-4878-ad84-9cc61756cd5e", + "parameters": null, + "parentID": "a22fddb9-3da4-4945-bd2e-9d27fe3d62e0", + "parentType": "domain", + "progress": 0.0, + "result": null, + "status": "RUNNING" + }] +''' + +import time +from ansible.module_utils.basic import AnsibleModule + +try: + import importlib + HAS_IMPORTLIB = True +except ImportError: + HAS_IMPORTLIB = False + +try: + from bambou.exceptions import BambouHTTPError + HAS_BAMBOU = True +except ImportError: + HAS_BAMBOU = False + +SUPPORTED_COMMANDS = ['find', 'change_password', 'wait_for_job', 'get_csp_enterprise'] +VSPK = None + + +class NuageEntityManager(object): + """ + This module is meant to manage an entity in a Nuage VSP Platform + """ + + def __init__(self, module): + self.module = module + self.auth = module.params['auth'] + self.api_username = None + self.api_password = None + self.api_enterprise = None + self.api_url = None + self.api_version = None + self.api_certificate = None + self.api_key = None + self.type = module.params['type'] + + self.state = module.params['state'] + self.command = module.params['command'] + self.match_filter = module.params['match_filter'] + self.entity_id = module.params['id'] + self.parent_id = module.params['parent_id'] + self.parent_type = module.params['parent_type'] + self.properties = module.params['properties'] + self.children = module.params['children'] + + self.entity = None + self.entity_class = None + self.parent = None + self.parent_class = None + self.entity_fetcher = None + + self.result = { + 'state': self.state, + 'id': self.entity_id, + 'entities': [] + } + self.nuage_connection = None + + self._verify_api() + self._verify_input() + self._connect_vspk() + self._find_parent() + + def _connect_vspk(self): + """ + Connects to a Nuage API endpoint + """ + try: + # Connecting to Nuage + if self.api_certificate and self.api_key: + self.nuage_connection = VSPK.NUVSDSession(username=self.api_username, enterprise=self.api_enterprise, api_url=self.api_url, + certificate=(self.api_certificate, self.api_key)) + else: + self.nuage_connection = VSPK.NUVSDSession(username=self.api_username, password=self.api_password, enterprise=self.api_enterprise, + api_url=self.api_url) + self.nuage_connection.start() + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to connect to the API URL with given username, password and enterprise: {0}'.format(error)) + + def _verify_api(self): + """ + Verifies the API and loads the proper VSPK version + """ + # Checking auth parameters + if ('api_password' not in list(self.auth.keys()) or not self.auth['api_password']) and ('api_certificate' not in list(self.auth.keys()) or + 'api_key' not in list(self.auth.keys()) or + not self.auth['api_certificate'] or not self.auth['api_key']): + self.module.fail_json(msg='Missing api_password or api_certificate and api_key parameter in auth') + + self.api_username = self.auth['api_username'] + if 'api_password' in list(self.auth.keys()) and self.auth['api_password']: + self.api_password = self.auth['api_password'] + if 'api_certificate' in list(self.auth.keys()) and 'api_key' in list(self.auth.keys()) and self.auth['api_certificate'] and self.auth['api_key']: + self.api_certificate = self.auth['api_certificate'] + self.api_key = self.auth['api_key'] + self.api_enterprise = self.auth['api_enterprise'] + self.api_url = self.auth['api_url'] + self.api_version = self.auth['api_version'] + + try: + global VSPK + VSPK = importlib.import_module('vspk.{0:s}'.format(self.api_version)) + except ImportError: + self.module.fail_json(msg='vspk is required for this module, or the API version specified does not exist.') + + def _verify_input(self): + """ + Verifies the parameter input for types and parent correctness and necessary parameters + """ + + # Checking if type exists + try: + self.entity_class = getattr(VSPK, 'NU{0:s}'.format(self.type)) + except AttributeError: + self.module.fail_json(msg='Unrecognised type specified') + + if self.module.check_mode: + return + + if self.parent_type: + # Checking if parent type exists + try: + self.parent_class = getattr(VSPK, 'NU{0:s}'.format(self.parent_type)) + except AttributeError: + # The parent type does not exist, fail + self.module.fail_json(msg='Unrecognised parent type specified') + + fetcher = self.parent_class().fetcher_for_rest_name(self.entity_class.rest_name) + if fetcher is None: + # The parent has no fetcher, fail + self.module.fail_json(msg='Specified parent is not a valid parent for the specified type') + elif not self.entity_id: + # If there is an id, we do not need a parent because we'll interact directly with the entity + # If an assign needs to happen, a parent will have to be provided + # Root object is the parent + self.parent_class = VSPK.NUMe + fetcher = self.parent_class().fetcher_for_rest_name(self.entity_class.rest_name) + if fetcher is None: + self.module.fail_json(msg='No parent specified and root object is not a parent for the type') + + # Verifying if a password is provided in case of the change_password command: + if self.command and self.command == 'change_password' and 'password' not in self.properties.keys(): + self.module.fail_json(msg='command is change_password but the following are missing: password property') + + def _find_parent(self): + """ + Fetches the parent if needed, otherwise configures the root object as parent. Also configures the entity fetcher + Important notes: + - If the parent is not set, the parent is automatically set to the root object + - It the root object does not hold a fetcher for the entity, you have to provide an ID + - If you want to assign/unassign, you have to provide a valid parent + """ + self.parent = self.nuage_connection.user + + if self.parent_id: + self.parent = self.parent_class(id=self.parent_id) + try: + self.parent.fetch() + except BambouHTTPError as error: + self.module.fail_json(msg='Failed to fetch the specified parent: {0}'.format(error)) + + self.entity_fetcher = self.parent.fetcher_for_rest_name(self.entity_class.rest_name) + + def _find_entities(self, entity_id=None, entity_class=None, match_filter=None, properties=None, entity_fetcher=None): + """ + Will return a set of entities matching a filter or set of properties if the match_filter is unset. If the + entity_id is set, it will return only the entity matching that ID as the single element of the list. + :param entity_id: Optional ID of the entity which should be returned + :param entity_class: Optional class of the entity which needs to be found + :param match_filter: Optional search filter + :param properties: Optional set of properties the entities should contain + :param entity_fetcher: The fetcher for the entity type + :return: List of matching entities + """ + search_filter = '' + + if entity_id: + found_entity = entity_class(id=entity_id) + try: + found_entity.fetch() + except BambouHTTPError as error: + self.module.fail_json(msg='Failed to fetch the specified entity by ID: {0}'.format(error)) + + return [found_entity] + + elif match_filter: + search_filter = match_filter + elif properties: + # Building filter + for num, property_name in enumerate(properties): + if num > 0: + search_filter += ' and ' + search_filter += '{0:s} == "{1}"'.format(property_name, properties[property_name]) + + if entity_fetcher is not None: + try: + return entity_fetcher.get(filter=search_filter) + except BambouHTTPError: + pass + return [] + + def _find_entity(self, entity_id=None, entity_class=None, match_filter=None, properties=None, entity_fetcher=None): + """ + Finds a single matching entity that matches all the provided properties, unless an ID is specified, in which + case it just fetches the one item + :param entity_id: Optional ID of the entity which should be returned + :param entity_class: Optional class of the entity which needs to be found + :param match_filter: Optional search filter + :param properties: Optional set of properties the entities should contain + :param entity_fetcher: The fetcher for the entity type + :return: The first entity matching the criteria, or None if none was found + """ + search_filter = '' + if entity_id: + found_entity = entity_class(id=entity_id) + try: + found_entity.fetch() + except BambouHTTPError as error: + self.module.fail_json(msg='Failed to fetch the specified entity by ID: {0}'.format(error)) + + return found_entity + + elif match_filter: + search_filter = match_filter + elif properties: + # Building filter + for num, property_name in enumerate(properties): + if num > 0: + search_filter += ' and ' + search_filter += '{0:s} == "{1}"'.format(property_name, properties[property_name]) + + if entity_fetcher is not None: + try: + return entity_fetcher.get_first(filter=search_filter) + except BambouHTTPError: + pass + return None + + def handle_main_entity(self): + """ + Handles the Ansible task + """ + if self.command and self.command == 'find': + self._handle_find() + elif self.command and self.command == 'change_password': + self._handle_change_password() + elif self.command and self.command == 'wait_for_job': + self._handle_wait_for_job() + elif self.command and self.command == 'get_csp_enterprise': + self._handle_get_csp_enterprise() + elif self.state == 'present': + self._handle_present() + elif self.state == 'absent': + self._handle_absent() + self.module.exit_json(**self.result) + + def _handle_absent(self): + """ + Handles the Ansible task when the state is set to absent + """ + # Absent state + self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties, + entity_fetcher=self.entity_fetcher) + if self.entity and (self.entity_fetcher is None or self.entity_fetcher.relationship in ['child', 'root']): + # Entity is present, deleting + if self.module.check_mode: + self.result['changed'] = True + else: + self._delete_entity(self.entity) + self.result['id'] = None + elif self.entity and self.entity_fetcher.relationship == 'member': + # Entity is a member, need to check if already present + if self._is_member(entity_fetcher=self.entity_fetcher, entity=self.entity): + # Entity is not a member yet + if self.module.check_mode: + self.result['changed'] = True + else: + self._unassign_member(entity_fetcher=self.entity_fetcher, entity=self.entity, entity_class=self.entity_class, parent=self.parent, + set_output=True) + + def _handle_present(self): + """ + Handles the Ansible task when the state is set to present + """ + # Present state + self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties, + entity_fetcher=self.entity_fetcher) + # Determining action to take + if self.entity_fetcher is not None and self.entity_fetcher.relationship == 'member' and not self.entity: + self.module.fail_json(msg='Trying to assign an entity that does not exist') + elif self.entity_fetcher is not None and self.entity_fetcher.relationship == 'member' and self.entity: + # Entity is a member, need to check if already present + if not self._is_member(entity_fetcher=self.entity_fetcher, entity=self.entity): + # Entity is not a member yet + if self.module.check_mode: + self.result['changed'] = True + else: + self._assign_member(entity_fetcher=self.entity_fetcher, entity=self.entity, entity_class=self.entity_class, parent=self.parent, + set_output=True) + elif self.entity_fetcher is not None and self.entity_fetcher.relationship in ['child', 'root'] and not self.entity: + # Entity is not present as a child, creating + if self.module.check_mode: + self.result['changed'] = True + else: + self.entity = self._create_entity(entity_class=self.entity_class, parent=self.parent, properties=self.properties) + self.result['id'] = self.entity.id + self.result['entities'].append(self.entity.to_dict()) + + # Checking children + if self.children: + for child in self.children: + self._handle_child(child=child, parent=self.entity) + elif self.entity: + # Need to compare properties in entity and found entity + changed = self._has_changed(entity=self.entity, properties=self.properties) + + if self.module.check_mode: + self.result['changed'] = changed + elif changed: + self.entity = self._save_entity(entity=self.entity) + self.result['id'] = self.entity.id + self.result['entities'].append(self.entity.to_dict()) + else: + self.result['id'] = self.entity.id + self.result['entities'].append(self.entity.to_dict()) + + # Checking children + if self.children: + for child in self.children: + self._handle_child(child=child, parent=self.entity) + elif not self.module.check_mode: + self.module.fail_json(msg='Invalid situation, verify parameters') + + def _handle_get_csp_enterprise(self): + """ + Handles the Ansible task when the command is to get the csp enterprise + """ + self.entity_id = self.parent.enterprise_id + self.entity = VSPK.NUEnterprise(id=self.entity_id) + try: + self.entity.fetch() + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to fetch CSP enterprise: {0}'.format(error)) + self.result['id'] = self.entity_id + self.result['entities'].append(self.entity.to_dict()) + + def _handle_wait_for_job(self): + """ + Handles the Ansible task when the command is to wait for a job + """ + # Command wait_for_job + self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties, + entity_fetcher=self.entity_fetcher) + if self.module.check_mode: + self.result['changed'] = True + else: + self._wait_for_job(self.entity) + + def _handle_change_password(self): + """ + Handles the Ansible task when the command is to change a password + """ + # Command change_password + self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties, + entity_fetcher=self.entity_fetcher) + if self.module.check_mode: + self.result['changed'] = True + else: + try: + getattr(self.entity, 'password') + except AttributeError: + self.module.fail_json(msg='Entity does not have a password property') + + try: + setattr(self.entity, 'password', self.properties['password']) + except AttributeError: + self.module.fail_json(msg='Password can not be changed for entity') + + self.entity = self._save_entity(entity=self.entity) + self.result['id'] = self.entity.id + self.result['entities'].append(self.entity.to_dict()) + + def _handle_find(self): + """ + Handles the Ansible task when the command is to find an entity + """ + # Command find + entities = self._find_entities(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties, + entity_fetcher=self.entity_fetcher) + self.result['changed'] = False + if entities: + if len(entities) == 1: + self.result['id'] = entities[0].id + for entity in entities: + self.result['entities'].append(entity.to_dict()) + elif not self.module.check_mode: + self.module.fail_json(msg='Unable to find matching entries') + + def _handle_child(self, child, parent): + """ + Handles children of a main entity. Fields are similar to the normal fields + Currently only supported state: present + """ + if 'type' not in list(child.keys()): + self.module.fail_json(msg='Child type unspecified') + elif 'id' not in list(child.keys()) and 'properties' not in list(child.keys()): + self.module.fail_json(msg='Child ID or properties unspecified') + + # Setting intern variables + child_id = None + if 'id' in list(child.keys()): + child_id = child['id'] + child_properties = None + if 'properties' in list(child.keys()): + child_properties = child['properties'] + child_filter = None + if 'match_filter' in list(child.keys()): + child_filter = child['match_filter'] + + # Checking if type exists + entity_class = None + try: + entity_class = getattr(VSPK, 'NU{0:s}'.format(child['type'])) + except AttributeError: + self.module.fail_json(msg='Unrecognised child type specified') + + entity_fetcher = parent.fetcher_for_rest_name(entity_class.rest_name) + if entity_fetcher is None and not child_id and not self.module.check_mode: + self.module.fail_json(msg='Unable to find a fetcher for child, and no ID specified.') + + # Try and find the child + entity = self._find_entity(entity_id=child_id, entity_class=entity_class, match_filter=child_filter, properties=child_properties, + entity_fetcher=entity_fetcher) + + # Determining action to take + if entity_fetcher.relationship == 'member' and not entity: + self.module.fail_json(msg='Trying to assign a child that does not exist') + elif entity_fetcher.relationship == 'member' and entity: + # Entity is a member, need to check if already present + if not self._is_member(entity_fetcher=entity_fetcher, entity=entity): + # Entity is not a member yet + if self.module.check_mode: + self.result['changed'] = True + else: + self._assign_member(entity_fetcher=entity_fetcher, entity=entity, entity_class=entity_class, parent=parent, set_output=False) + elif entity_fetcher.relationship in ['child', 'root'] and not entity: + # Entity is not present as a child, creating + if self.module.check_mode: + self.result['changed'] = True + else: + entity = self._create_entity(entity_class=entity_class, parent=parent, properties=child_properties) + elif entity_fetcher.relationship in ['child', 'root'] and entity: + changed = self._has_changed(entity=entity, properties=child_properties) + + if self.module.check_mode: + self.result['changed'] = changed + elif changed: + entity = self._save_entity(entity=entity) + + if entity: + self.result['entities'].append(entity.to_dict()) + + # Checking children + if 'children' in list(child.keys()) and not self.module.check_mode: + for subchild in child['children']: + self._handle_child(child=subchild, parent=entity) + + def _has_changed(self, entity, properties): + """ + Compares a set of properties with a given entity, returns True in case the properties are different from the + values in the entity + :param entity: The entity to check + :param properties: The properties to check + :return: boolean + """ + # Need to compare properties in entity and found entity + changed = False + if properties: + for property_name in list(properties.keys()): + if property_name == 'password': + continue + entity_value = '' + try: + entity_value = getattr(entity, property_name) + except AttributeError: + self.module.fail_json(msg='Property {0:s} is not valid for this type of entity'.format(property_name)) + + if entity_value != properties[property_name]: + # Difference in values changing property + changed = True + try: + setattr(entity, property_name, properties[property_name]) + except AttributeError: + self.module.fail_json(msg='Property {0:s} can not be changed for this type of entity'.format(property_name)) + return changed + + def _is_member(self, entity_fetcher, entity): + """ + Verifies if the entity is a member of the parent in the fetcher + :param entity_fetcher: The fetcher for the entity type + :param entity: The entity to look for as a member in the entity fetcher + :return: boolean + """ + members = entity_fetcher.get() + for member in members: + if member.id == entity.id: + return True + return False + + def _assign_member(self, entity_fetcher, entity, entity_class, parent, set_output): + """ + Adds the entity as a member to a parent + :param entity_fetcher: The fetcher of the entity type + :param entity: The entity to add as a member + :param entity_class: The class of the entity + :param parent: The parent on which to add the entity as a member + :param set_output: If set to True, sets the Ansible result variables + """ + members = entity_fetcher.get() + members.append(entity) + try: + parent.assign(members, entity_class) + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to assign entity as a member: {0}'.format(error)) + self.result['changed'] = True + if set_output: + self.result['id'] = entity.id + self.result['entities'].append(entity.to_dict()) + + def _unassign_member(self, entity_fetcher, entity, entity_class, parent, set_output): + """ + Removes the entity as a member of a parent + :param entity_fetcher: The fetcher of the entity type + :param entity: The entity to remove as a member + :param entity_class: The class of the entity + :param parent: The parent on which to add the entity as a member + :param set_output: If set to True, sets the Ansible result variables + """ + members = [] + for member in entity_fetcher.get(): + if member.id != entity.id: + members.append(member) + try: + parent.assign(members, entity_class) + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to remove entity as a member: {0}'.format(error)) + self.result['changed'] = True + if set_output: + self.result['id'] = entity.id + self.result['entities'].append(entity.to_dict()) + + def _create_entity(self, entity_class, parent, properties): + """ + Creates a new entity in the parent, with all properties configured as in the file + :param entity_class: The class of the entity + :param parent: The parent of the entity + :param properties: The set of properties of the entity + :return: The entity + """ + entity = entity_class(**properties) + try: + parent.create_child(entity) + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to create entity: {0}'.format(error)) + self.result['changed'] = True + return entity + + def _save_entity(self, entity): + """ + Updates an existing entity + :param entity: The entity to save + :return: The updated entity + """ + try: + entity.save() + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to update entity: {0}'.format(error)) + self.result['changed'] = True + return entity + + def _delete_entity(self, entity): + """ + Deletes an entity + :param entity: The entity to delete + """ + try: + entity.delete() + except BambouHTTPError as error: + self.module.fail_json(msg='Unable to delete entity: {0}'.format(error)) + self.result['changed'] = True + + def _wait_for_job(self, entity): + """ + Waits for a job to finish + :param entity: The job to wait for + """ + running = False + if entity.status == 'RUNNING': + self.result['changed'] = True + running = True + + while running: + time.sleep(1) + entity.fetch() + + if entity.status != 'RUNNING': + running = False + + self.result['entities'].append(entity.to_dict()) + if entity.status == 'ERROR': + self.module.fail_json(msg='Job ended in an error') + + +def main(): + """ + Main method + """ + module = AnsibleModule( + argument_spec=dict( + auth=dict( + required=True, + type='dict', + options=dict( + api_username=dict(required=True, type='str'), + api_enterprise=dict(required=True, type='str'), + api_url=dict(required=True, type='str'), + api_version=dict(required=True, type='str'), + api_password=dict(default=None, required=False, type='str', no_log=True), + api_certificate=dict(default=None, required=False, type='str', no_log=True), + api_key=dict(default=None, required=False, type='str', no_log=True) + ) + ), + type=dict(required=True, type='str'), + id=dict(default=None, required=False, type='str'), + parent_id=dict(default=None, required=False, type='str'), + parent_type=dict(default=None, required=False, type='str'), + state=dict(default=None, choices=['present', 'absent'], type='str'), + command=dict(default=None, choices=SUPPORTED_COMMANDS, type='str'), + match_filter=dict(default=None, required=False, type='str'), + properties=dict(default=None, required=False, type='dict'), + children=dict(default=None, required=False, type='list') + ), + mutually_exclusive=[ + ['command', 'state'] + ], + required_together=[ + ['parent_id', 'parent_type'] + ], + required_one_of=[ + ['command', 'state'] + ], + required_if=[ + ['state', 'present', ['id', 'properties', 'match_filter'], True], + ['state', 'absent', ['id', 'properties', 'match_filter'], True], + ['command', 'change_password', ['id', 'properties']], + ['command', 'wait_for_job', ['id']] + ], + supports_check_mode=True + ) + + if not HAS_BAMBOU: + module.fail_json(msg='bambou is required for this module') + + if not HAS_IMPORTLIB: + module.fail_json(msg='importlib (python 2.7) is required for this module') + + entity_manager = NuageEntityManager(module) + entity_manager.handle_main_entity() + + +if __name__ == '__main__': + main() diff --git a/test/integration/network-all.yaml b/test/integration/network-all.yaml index ebdac4ed9ee..354cdf7d0be 100644 --- a/test/integration/network-all.yaml +++ b/test/integration/network-all.yaml @@ -11,3 +11,4 @@ - { include: dellos10.yaml } - { include: dellos9.yaml } - { include: dellos6.yaml } +- { include: nuage.yaml } diff --git a/test/integration/nuage.yaml b/test/integration/nuage.yaml new file mode 100644 index 00000000000..b59efbdcd98 --- /dev/null +++ b/test/integration/nuage.yaml @@ -0,0 +1,11 @@ +--- +- hosts: nuage + gather_facts: no + connection: local + + vars: + limit_to: "*" + debug: false + + roles: + - { role: nuage_vspk, when: "limit_to in ['*', 'nuage_vspk']" } \ No newline at end of file diff --git a/test/integration/targets/nuage_vspk/aliases b/test/integration/targets/nuage_vspk/aliases new file mode 100644 index 00000000000..a678773f18b --- /dev/null +++ b/test/integration/targets/nuage_vspk/aliases @@ -0,0 +1 @@ +skip/python3 \ No newline at end of file diff --git a/test/integration/targets/nuage_vspk/defaults/main.yaml b/test/integration/targets/nuage_vspk/defaults/main.yaml new file mode 100644 index 00000000000..78266b65b92 --- /dev/null +++ b/test/integration/targets/nuage_vspk/defaults/main.yaml @@ -0,0 +1,9 @@ +--- +testcase: "*" +test_items: [] +nuage_auth: + api_username: csproot + api_password: csproot + api_enterprise: csp + api_url: http://localhost:5000 + api_version: v5_0 \ No newline at end of file diff --git a/test/integration/targets/nuage_vspk/meta/main.yaml b/test/integration/targets/nuage_vspk/meta/main.yaml new file mode 100644 index 00000000000..64ceb78f40c --- /dev/null +++ b/test/integration/targets/nuage_vspk/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_nuage_tests diff --git a/test/integration/targets/nuage_vspk/tasks/main.yaml b/test/integration/targets/nuage_vspk/tasks/main.yaml new file mode 100644 index 00000000000..8e4e36a51a1 --- /dev/null +++ b/test/integration/targets/nuage_vspk/tasks/main.yaml @@ -0,0 +1,17 @@ +--- + +- name: collect all test cases + find: + paths: "{{ role_path }}/tests" + patterns: "{{ testcase }}.yaml" + delegate_to: localhost + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nuage_vspk/tests/basic.yaml b/test/integration/targets/nuage_vspk/tests/basic.yaml new file mode 100644 index 00000000000..744c0dec4e4 --- /dev/null +++ b/test/integration/targets/nuage_vspk/tests/basic.yaml @@ -0,0 +1,226 @@ +--- +# Getting the CSP enterprise +- name: Get CSP Enterprise + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + command: get_csp_enterprise + register: nuage_csp_enterprise + +- name: Check if CSP enterprise was found + assert: + that: + - nuage_csp_enterprise.id is defined + - nuage_csp_enterprise.entities is defined + - nuage_csp_enterprise.entities[0].name == "CSP" + +- name: Create Enterprise + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + state: present + properties: + name: "Ansible-Enterprise" + register: nuage_enterprise + +- name: Check Enterprise was created + assert: + that: + - nuage_enterprise.changed + - nuage_enterprise.id is defined + - nuage_enterprise.entities is defined + - nuage_enterprise.entities[0].name == "Ansible-Enterprise" + +- name: Finding Enterprise + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + command: find + properties: + name: "Ansible-Enterprise" + register: nuage_enterprise + +- name: Check Enterprise was found + assert: + that: + - not nuage_enterprise.changed + - nuage_enterprise.id is defined + - nuage_enterprise.entities is defined + - nuage_enterprise.entities[0].name == "Ansible-Enterprise" + +- name: Create Enterprise again to confirm idempoteny + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + state: present + properties: + name: "Ansible-Enterprise" + register: nuage_enterprise + +- name: Check Enterprise was not created again + assert: + that: + - not nuage_enterprise.changed + - nuage_enterprise.id is defined + - nuage_enterprise.entities is defined + - nuage_enterprise.entities[0].name == "Ansible-Enterprise" + +- name: Create admin user + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + parent_id: "{{ nuage_enterprise.id }}" + parent_type: Enterprise + state: present + match_filter: "userName == 'ansible-admin'" + properties: + email: "ansible@localhost.local" + first_name: "Ansible" + last_name: "Admin" + password: "ansible-password" + user_name: "ansible-admin" + register: nuage_user + +- name: Check the user was created + assert: + that: + - nuage_user.changed + - nuage_user.id is defined + - nuage_user.entities is defined + - nuage_user.entities[0].userName == "ansible-admin" + +- name: Update admin password + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + command: change_password + properties: + password: "ansible-new-password" + ignore_errors: yes + +- name: Check the user was created + assert: + that: + - nuage_user.changed + - nuage_user.id is defined + - nuage_user.entities is defined + - nuage_user.entities[0].userName == "ansible-admin" + +- name: Create group in Enterprise + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Group + parent_id: "{{ nuage_enterprise.id }}" + parent_type: Enterprise + state: present + properties: + name: "Ansible-Group" + register: nuage_group + +- name: Check the group was created + assert: + that: + - nuage_group.changed + - nuage_group.id is defined + - nuage_group.entities is defined + - nuage_group.entities[0].name == "Ansible-Group" + +- name: Assign admin user to group + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + parent_id: "{{ nuage_group.id }}" + parent_type: Group + state: present + register: nuage_assign + +- name: Check the admin was added to the group + assert: + that: + - nuage_assign.changed + +- name: Assign admin user to administrators again to test idempotency + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + parent_id: "{{ nuage_group.id }}" + parent_type: Group + state: present + register: nuage_assign + +- name: Check the group was not changed + assert: + that: + - not nuage_assign.changed + +- name: Unassign admin user to administrators + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + parent_id: "{{ nuage_group.id }}" + parent_type: Group + state: absent + register: nuage_unassign + +- name: Check the admin was removed from the group + assert: + that: + - nuage_unassign.changed + +- name: Unassign admin user to administrators again to test idempotency + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + parent_id: "{{ nuage_group.id }}" + parent_type: Group + state: absent + register: nuage_unassign + +- name: Check the group was not changed + assert: + that: + - not nuage_unassign.changed + +- name: Delete User + nuage_vspk: + auth: "{{ nuage_auth }}" + type: User + id: "{{ nuage_user.id }}" + state: absent + register: nuage_user + +- name: Check the user was deleted + assert: + that: + - nuage_user.changed + +- name: Delete Enterprise + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + id: "{{ nuage_enterprise.id }}" + state: absent + register: nuage_enterprise + +- name: Check the enterprise was deleted + assert: + that: + - nuage_enterprise.changed + +- name: Delete Enterprise again to test idempotency + nuage_vspk: + auth: "{{ nuage_auth }}" + type: Enterprise + match_filter: 'name == "Ansible-Enterprise"' + state: absent + register: nuage_enterprise + +- name: Check the delete idempotency + assert: + that: + - not nuage_enterprise.changed \ No newline at end of file diff --git a/test/integration/targets/prepare_nuage_tests/tasks/main.yml b/test/integration/targets/prepare_nuage_tests/tasks/main.yml new file mode 100644 index 00000000000..bfeab9bf72a --- /dev/null +++ b/test/integration/targets/prepare_nuage_tests/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: Install Nuage VSD API Simulator + pip: + name: nuage-vsd-sim + +- name: Start Nuage VSD API Simulator + shell: "(cd /; nuage-vsd-sim >/dev/null 2>&1 &)" + async: 10 + poll: 0 diff --git a/test/units/modules/network/nuage/__init__.py b/test/units/modules/network/nuage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/network/nuage/nuage_module.py b/test/units/modules/network/nuage/nuage_module.py new file mode 100644 index 00000000000..2a38ca39814 --- /dev/null +++ b/test/units/modules/network/nuage/nuage_module.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# (c) 2017, Nokia +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import json +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + +from nose.plugins.skip import SkipTest +try: + from vspk import v5_0 as vsdk + from bambou import nurest_session +except ImportError: + raise SkipTest('Nuage Ansible modules requires the vspk and bambou python libraries') + + +def set_module_args(args): + set_module_args_custom_auth(args=args, auth={ + 'api_username': 'csproot', + 'api_password': 'csproot', + 'api_enterprise': 'csp', + 'api_url': 'https://localhost:8443', + 'api_version': 'v5_0' + }) + + +def set_module_args_custom_auth(args, auth): + args['auth'] = auth + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +class MockNuageResponse(object): + def __init__(self, status_code, reason, errors): + self.status_code = status_code + self.reason = reason + self.errors = errors + + +class MockNuageConnection(object): + def __init__(self, status_code, reason, errors): + self.response = MockNuageResponse(status_code, reason, errors) + + +class TestNuageModule(unittest.TestCase): + + def setUp(self): + + def session_start(self): + self._root_object = vsdk.NUMe() + self._root_object.enterprise_id = 'enterprise-id' + nurest_session._NURESTSessionCurrentContext.session = self + return self + + self.session_mock = patch('vspk.v5_0.NUVSDSession.start', new=session_start) + self.session_mock.start() + + def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + self.fail_json_mock = patch('ansible.module_utils.basic.AnsibleModule.fail_json', new=fail_json) + self.fail_json_mock.start() + + def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + self.exit_json_mock = patch('ansible.module_utils.basic.AnsibleModule.exit_json', new=exit_json) + self.exit_json_mock.start() + + def tearDown(self): + self.session_mock.stop() + self.fail_json_mock.stop() + self.exit_json_mock.stop() diff --git a/test/units/modules/network/nuage/test_nuage_vspk.py b/test/units/modules/network/nuage/test_nuage_vspk.py new file mode 100644 index 00000000000..28e2d685d78 --- /dev/null +++ b/test/units/modules/network/nuage/test_nuage_vspk.py @@ -0,0 +1,1382 @@ +# -*- coding: utf-8 -*- + +# (c) 2017, Nokia +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import sys + +from nose.plugins.skip import SkipTest +if not(sys.version_info[0] == 2 and sys.version_info[1] >= 7): + raise SkipTest('Nuage Ansible modules requires Python 2.7') + +try: + from vspk import v5_0 as vsdk + from bambou.exceptions import BambouHTTPError + from ansible.modules.network.nuage import nuage_vspk +except ImportError: + raise SkipTest('Nuage Ansible modules requires the vspk and bambou python libraries') + +from ansible.compat.tests.mock import patch +from .nuage_module import AnsibleExitJson, AnsibleFailJson, MockNuageConnection, TestNuageModule, set_module_args, set_module_args_custom_auth + +_LOOP_COUNTER = 0 + + +class TestNuageVSPKModule(TestNuageModule): + + def setUp(self): + super(TestNuageVSPKModule, self).setUp() + + self.patches = [] + + def enterprises_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, + callback=None): + if 'unknown' in filter: + return [] + + result = [vsdk.NUEnterprise(id='enterprise-id', name='test-enterprise')] + if filter == '' or filter == 'name == "test%"': + result.append(vsdk.NUEnterprise(id='enterprise-id-2', name='test-enterprise-2')) + return result + + self.enterprises_get_mock = patch('vspk.v5_0.fetchers.NUEnterprisesFetcher.get', new=enterprises_get) + self.enterprises_get_mock.start() + self.patches.append(self.enterprises_get_mock) + + def enterprises_get_first(self, filter=None, order_by=None, group_by=[], query_parameters=None, commit=False, async=False, callback=None): + if filter == 'name == "test-enterprise-create"' or 'unknown' in filter: + return None + return vsdk.NUEnterprise(id='enterprise-id', name='test-enterprise') + + self.enterprises_get_first_mock = patch('vspk.v5_0.fetchers.NUEnterprisesFetcher.get_first', new=enterprises_get_first) + self.enterprises_get_first_mock.start() + self.patches.append(self.enterprises_get_first_mock) + + def enterprise_delete(self, response_choice=1, async=False, callback=None): + pass + + self.enterprise_delete_mock = patch('vspk.v5_0.NUEnterprise.delete', new=enterprise_delete) + self.enterprise_delete_mock.start() + self.patches.append(self.enterprise_delete_mock) + + def enterprise_fetch(self, async=False, callback=None): + self.id = 'enterprise-id' + self.name = 'test-enterprise' + + self.enterprise_fetch_mock = patch('vspk.v5_0.NUEnterprise.fetch', new=enterprise_fetch) + self.enterprise_fetch_mock.start() + self.patches.append(self.enterprise_fetch_mock) + + def enterprise_save(self, response_choice=None, async=False, callback=None): + self.id = 'enterprise-id' + self.name = 'test-enterprise-update' + + self.enterprise_save_mock = patch('vspk.v5_0.NUEnterprise.save', new=enterprise_save) + self.enterprise_save_mock.start() + self.patches.append(self.enterprise_save_mock) + + def enterprise_create_child(self, nurest_object, response_choice=None, async=False, callback=None, commit=True): + nurest_object.id = 'user-id-create' + return nurest_object + + self.enterprise_create_child_mock = patch('vspk.v5_0.NUEnterprise.create_child', new=enterprise_create_child) + self.enterprise_create_child_mock.start() + self.patches.append(self.enterprise_create_child_mock) + + def me_create_child(self, nurest_object, response_choice=None, async=False, callback=None, commit=True): + nurest_object.id = 'enterprise-id-create' + return nurest_object + + self.me_create_child_mock = patch('vspk.v5_0.NUMe.create_child', new=me_create_child) + self.me_create_child_mock.start() + self.patches.append(self.me_create_child_mock) + + def user_fetch(self, async=False, callback=None): + self.id = 'user-id' + self.first_name = 'John' + self.last_name = 'Doe' + self.email = 'john.doe@localhost' + self.user_name = 'johndoe' + self.password = '' + + self.user_fetch_mock = patch('vspk.v5_0.NUUser.fetch', new=user_fetch) + self.user_fetch_mock.start() + self.patches.append(self.user_fetch_mock) + + def user_save(self, response_choice=None, async=False, callback=None): + self.id = 'user-id' + self.first_name = 'John' + self.last_name = 'Doe' + self.email = 'john.doe@localhost' + self.user_name = 'johndoe' + self.password = '' + + self.user_save_mock = patch('vspk.v5_0.NUUser.save', new=user_save) + self.user_save_mock.start() + self.patches.append(self.user_save_mock) + + def groups_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, + callback=None): + return [] + + self.groups_get_mock = patch('vspk.v5_0.fetchers.NUGroupsFetcher.get', new=groups_get) + self.groups_get_mock.start() + self.patches.append(self.groups_get_mock) + + def group_fetch(self, async=False, callback=None): + self.id = 'group-id' + self.name = 'group' + + self.group_fetch_mock = patch('vspk.v5_0.NUGroup.fetch', new=group_fetch) + self.group_fetch_mock.start() + self.patches.append(self.group_fetch_mock) + + def group_assign(self, objects, nurest_object_type, async=False, callback=None, commit=True): + self.id = 'group-id' + self.name = 'group' + + self.group_assign_mock = patch('vspk.v5_0.NUGroup.assign', new=group_assign) + self.group_assign_mock.start() + self.patches.append(self.group_assign_mock) + + def job_fetch(self, async=False, callback=None): + global _LOOP_COUNTER + self.id = 'job-id' + self.command = 'EXPORT' + self.status = 'RUNNING' + if _LOOP_COUNTER > 1: + self.status = 'SUCCESS' + _LOOP_COUNTER += 1 + + self.job_fetch_mock = patch('vspk.v5_0.NUJob.fetch', new=job_fetch) + self.job_fetch_mock.start() + self.patches.append(self.job_fetch_mock) + + def tearDown(self): + super(TestNuageVSPKModule, self).tearDown() + for patch in self.patches: + patch.stop() + + def test_certificate_auth(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise' + } + }, + auth={ + 'api_username': 'csproot', + 'api_certificate': '/dummy/location/certificate.pem', + 'api_key': '/dummy/location/key.pem', + 'api_enterprise': 'csp', + 'api_url': 'https://localhost:8443', + 'api_version': 'v5_0' + } + ) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + + def test_command_find_by_property(self): + set_module_args(args={ + 'type': 'Enterprise', + 'command': 'find', + 'properties': { + 'name': 'test-enterprise' + } + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + + def test_command_find_by_filter(self): + set_module_args(args={ + 'type': 'Enterprise', + 'command': 'find', + 'match_filter': 'name == "test%"' + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 2) + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + self.assertEqual(result['entities'][1]['name'], 'test-enterprise-2') + + def test_command_find_by_id(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'command': 'find' + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + + def test_command_find_all(self): + set_module_args(args={ + 'type': 'Enterprise', + 'command': 'find' + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 2) + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + self.assertEqual(result['entities'][1]['name'], 'test-enterprise-2') + + def test_command_change_password(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'parent_id': 'enterprise-id', + 'parent_type': 'Enterprise', + 'command': 'change_password', + 'properties': { + 'password': 'test' + } + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertEqual(result['changed'], True) + self.assertEqual(result['id'], 'user-id') + self.assertEqual(result['entities'][0]['firstName'], 'John') + self.assertEqual(result['entities'][0]['lastName'], 'Doe') + self.assertEqual(result['entities'][0]['email'], 'john.doe@localhost') + self.assertEqual(result['entities'][0]['userName'], 'johndoe') + self.assertEqual(result['entities'][0]['password'], '') + + def test_command_wait_for_job(self): + set_module_args(args={ + 'id': 'job-id', + 'type': 'Job', + 'command': 'wait_for_job', + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertEqual(result['changed'], True) + self.assertEqual(result['id'], 'job-id') + self.assertEqual(result['entities'][0]['command'], 'EXPORT') + self.assertEqual(result['entities'][0]['status'], 'SUCCESS') + + def test_command_get_csp_enterprise(self): + set_module_args(args={ + 'type': 'Enterprise', + 'command': 'get_csp_enterprise' + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + + def test_state_present_existing(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'id': 'enterprise-id', + 'name': 'test-enterprise' + } + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + + def test_state_present_existing_filter(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'match_filter': 'name == "test-enterprise"' + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise') + + def test_state_present_create(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-create' + } + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertEqual(result['changed'], True) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id-create') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise-create') + + def test_state_present_update(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-update' + } + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertEqual(result['changed'], True) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'enterprise-id') + self.assertEqual(result['entities'][0]['name'], 'test-enterprise-update') + + def test_state_present_member_existing(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'state': 'present' + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [vsdk.NUUser(id='user-id'), vsdk.NUUser(id='user-id-2')] + + with self.assertRaises(AnsibleExitJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + + def test_state_present_member_missing(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'state': 'present' + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [] + + with self.assertRaises(AnsibleExitJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertEqual(result['changed'], True) + self.assertEqual(len(result['entities']), 1) + self.assertEqual(result['id'], 'user-id') + + def test_state_present_children_update(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise' + }, + 'children': [ + { + 'id': 'user-id', + 'type': 'User', + 'match_filter': 'userName == "johndoe"', + 'properties': { + 'user_name': 'johndoe-changed' + } + } + ] + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertEqual(result['changed'], True) + self.assertEqual(len(result['entities']), 2) + + def test_state_present_children_create(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-create' + }, + 'children': [ + { + 'type': 'User', + 'properties': { + 'user_name': 'johndoe-new' + } + } + ] + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [] + + with self.assertRaises(AnsibleExitJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['changed']) + self.assertEqual(len(result['entities']), 2) + + def test_state_present_children_member_missing(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'unkown-test-enterprise' + }, + 'children': [ + { + 'type': 'Group', + 'properties': { + 'name': 'unknown-group' + }, + 'children': [ + { + 'id': 'user-id', + 'type': 'User' + } + ] + } + ] + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [] + + with self.assertRaises(AnsibleExitJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['changed']) + self.assertEqual(len(result['entities']), 3) + + def test_state_absent(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'absent', + 'properties': { + 'name': 'test-enterprise' + } + }) + + with self.assertRaises(AnsibleExitJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['changed']) + + def test_state_absent_member(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'state': 'absent' + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [vsdk.NUUser(id='user-id')] + + with self.assertRaises(AnsibleExitJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['changed']) + + def test_exception_session(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'command': 'find' + }) + + def failed_session_start(self): + raise BambouHTTPError(MockNuageConnection(status_code='401', reason='Unauthorized', errors={})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUVSDSession.start', new=failed_session_start): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unable to connect to the API URL with given username, password and enterprise: [HTTP 401(Unauthorized)] {}') + + def test_exception_find_parent(self): + set_module_args(args={ + 'type': 'User', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'command': 'find' + }) + + def group_failed_fetch(self, async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='404', reason='Not Found', errors={'description': 'Entity not found'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUGroup.fetch', group_failed_fetch): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Failed to fetch the specified parent: [HTTP 404(Not Found)] {'description': 'Entity not found'}") + + def test_exception_find_entities_id(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'command': 'find' + }) + + def enterprise_failed_fetch(self, async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='404', reason='Not Found', errors={'description': 'Entity not found'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUEnterprise.fetch', enterprise_failed_fetch): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Failed to fetch the specified entity by ID: [HTTP 404(Not Found)] {'description': 'Entity not found'}") + + def test_excption_find_entities_property(self): + set_module_args(args={ + 'type': 'Enterprise', + 'match_filter': 'name == "enterprise-id"', + 'command': 'find' + }) + + def enterprises_failed_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, + callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='404', reason='Not Found', errors={'description': 'Entity not found'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.fetchers.NUEnterprisesFetcher.get', enterprises_failed_get): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unable to find matching entries') + + def test_exception_find_entity_id(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'state': 'present' + }) + + def enterprise_failed_fetch(self, async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='404', reason='Not Found', errors={'description': 'Entity not found'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUEnterprise.fetch', enterprise_failed_fetch): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Failed to fetch the specified entity by ID: [HTTP 404(Not Found)] {'description': 'Entity not found'}") + + def test_exception_find_entity_property(self): + set_module_args(args={ + 'type': 'Enterprise', + 'match_filter': 'name == "enterprise-id"', + 'state': 'absent' + }) + + def enterprises_failed_get_first(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, + async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='404', reason='Not Found', errors={'description': 'Entity not found'})) + + with self.assertRaises(AnsibleExitJson) as exc: + with patch('vspk.v5_0.fetchers.NUEnterprisesFetcher.get_first', enterprises_failed_get_first): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertFalse(result['changed']) + + def test_exception_get_csp_enterprise(self): + set_module_args(args={ + 'type': 'Enterprise', + 'command': 'get_csp_enterprise' + }) + + def enterprise_failed_fetch(self, async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='404', reason='Not Found', errors={'description': 'Entity not found'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUEnterprise.fetch', enterprise_failed_fetch): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Unable to fetch CSP enterprise: [HTTP 404(Not Found)] {'description': 'Entity not found'}") + + def test_exception_assign_member(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'state': 'present' + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [] + + def group_assign(self, objects, nurest_object_type, async=False, callback=None, commit=True): + raise BambouHTTPError(MockNuageConnection(status_code='500', reason='Server exception', errors={'description': 'Unable to assign member'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + with patch('vspk.v5_0.NUGroup.assign', new=group_assign): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Unable to assign entity as a member: [HTTP 500(Server exception)] {'description': 'Unable to assign member'}") + + def test_exception_unassign_member(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'state': 'absent' + }) + + def users_get(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, callback=None): + return [vsdk.NUUser(id='user-id'), vsdk.NUUser(id='user-id-2')] + + def group_assign(self, objects, nurest_object_type, async=False, callback=None, commit=True): + raise BambouHTTPError(MockNuageConnection(status_code='500', reason='Server exception', errors={'description': 'Unable to remove member'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get', users_get): + with patch('vspk.v5_0.NUGroup.assign', new=group_assign): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Unable to remove entity as a member: [HTTP 500(Server exception)] {'description': 'Unable to remove member'}") + + def test_exception_create_entity(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-create' + } + }) + + def me_create_child(self, nurest_object, response_choice=None, async=False, callback=None, commit=True): + raise BambouHTTPError(MockNuageConnection(status_code='500', reason='Server exception', errors={'description': 'Unable to create entity'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUMe.create_child', me_create_child): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Unable to create entity: [HTTP 500(Server exception)] {'description': 'Unable to create entity'}") + + def test_exception_save_entity(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'new-enterprise-name' + } + }) + + def enterprise_save(self, response_choice=None, async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='500', reason='Server exception', errors={'description': 'Unable to save entity'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUEnterprise.save', enterprise_save): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Unable to update entity: [HTTP 500(Server exception)] {'description': 'Unable to save entity'}") + + def test_exception_delete_entity(self): + set_module_args(args={ + 'id': 'enterprise-id', + 'type': 'Enterprise', + 'state': 'absent' + }) + + def enterprise_delete(self, response_choice=1, async=False, callback=None): + raise BambouHTTPError(MockNuageConnection(status_code='500', reason='Server exception', errors={'description': 'Unable to delete entity'})) + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUEnterprise.delete', enterprise_delete): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Unable to delete entity: [HTTP 500(Server exception)] {'description': 'Unable to delete entity'}") + + def test_exception_wait_for_job(self): + set_module_args(args={ + 'id': 'job-id', + 'type': 'Job', + 'command': 'wait_for_job' + }) + + def job_fetch(self, async=False, callback=None): + global _LOOP_COUNTER + self.id = 'job-id' + self.command = 'EXPORT' + self.status = 'RUNNING' + if _LOOP_COUNTER > 1: + self.status = 'ERROR' + _LOOP_COUNTER += 1 + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.NUJob.fetch', new=job_fetch): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "Job ended in an error") + + def test_fail_auth(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'command': 'find' + }, + auth={ + 'api_username': 'csproot', + 'api_enterprise': 'csp', + 'api_url': 'https://localhost:8443', + 'api_version': 'v5_0' + } + ) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Missing api_password or api_certificate and api_key parameter in auth') + + def test_fail_version(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'command': 'find' + }, + auth={ + 'api_username': 'csproot', + 'api_password': 'csproot', + 'api_enterprise': 'csp', + 'api_url': 'https://localhost:8443', + 'api_version': 'v1_0' + } + ) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'vspk is required for this module, or the API version specified does not exist.') + + def test_fail_type(self): + set_module_args(args={ + 'type': 'Unknown', + 'command': 'find' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unrecognised type specified') + + def test_fail_parent_type(self): + set_module_args(args={ + 'type': 'User', + 'parent_id': 'unkown-id', + 'parent_type': 'Unknown', + 'command': 'find' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unrecognised parent type specified') + + def test_fail_parent_child(self): + set_module_args(args={ + 'type': 'Enterprise', + 'parent_id': 'user-id', + 'parent_type': 'User', + 'command': 'find' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Specified parent is not a valid parent for the specified type') + + def test_fail_no_parent(self): + set_module_args(args={ + 'type': 'Group', + 'command': 'find' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'No parent specified and root object is not a parent for the type') + + def test_fail_present_member(self): + set_module_args(args={ + 'type': 'User', + 'match_filter': 'name == "test-user"', + 'parent_id': 'group-id', + 'parent_type': 'Group', + 'state': 'present' + }) + + def users_get_first(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, + callback=None): + return None + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get_first', users_get_first): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Trying to assign an entity that does not exist', result) + + def test_fail_change_password(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'command': 'change_password', + 'properties': {} + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'command is change_password but the following are missing: password property') + + def test_fail_change_password_non_user(self): + set_module_args(args={ + 'id': 'group-id', + 'type': 'Group', + 'command': 'change_password', + 'properties': { + 'password': 'new-password' + } + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Entity does not have a password property') + + def test_fail_command_find(self): + set_module_args(args={ + 'type': 'Enterprise', + 'command': 'find', + 'properties': { + 'id': 'unknown-enterprise-id', + 'name': 'unkown-enterprise' + } + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unable to find matching entries') + + def test_fail_children_type(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-create' + }, + 'children': [ + { + 'properties': { + 'user_name': 'johndoe-new' + } + } + ] + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Child type unspecified') + + def test_fail_children_mandatory(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-create' + }, + 'children': [ + { + 'type': 'User' + } + ] + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Child ID or properties unspecified') + + def test_fail_children_unknown(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'properties': { + 'name': 'test-enterprise-create' + }, + 'children': [ + { + 'id': 'unkown-id', + 'type': 'Unkown' + } + ] + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unrecognised child type specified') + + def test_fail_children_parent(self): + set_module_args(args={ + 'id': 'group-id', + 'type': 'Group', + 'state': 'present', + 'children': [ + { + 'type': 'User', + 'properties': { + 'name': 'test-user' + } + } + ] + }) + + def users_get_first(self, filter=None, order_by=None, group_by=[], page=None, page_size=None, query_parameters=None, commit=True, async=False, + callback=None): + return None + + with self.assertRaises(AnsibleFailJson) as exc: + with patch('vspk.v5_0.fetchers.NUUsersFetcher.get_first', users_get_first): + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Trying to assign a child that does not exist') + + def test_fail_children_fetcher(self): + set_module_args(args={ + 'id': 'group-id', + 'type': 'Group', + 'state': 'present', + 'children': [ + { + 'type': 'Enterprise', + 'properties': { + 'name': 'test-enterprise' + } + } + ] + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Unable to find a fetcher for child, and no ID specified.') + + def test_fail_has_changed(self): + set_module_args(args={ + 'id': 'user-id', + 'type': 'User', + 'state': 'present', + 'properties': { + 'user_name': 'changed-user', + 'fake': 'invalid-property', + 'password': 'hidden-property' + } + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'Property fake is not valid for this type of entity') + + def test_input_auth_username(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'command': 'find' + }, + auth={ + 'api_password': 'csproot', + 'api_enterprise': 'csp', + 'api_url': 'https://localhost:8443', + 'api_version': 'v5_0' + } + ) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'missing required arguments: api_username') + + def test_input_auth_enterprise(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'command': 'find' + }, + auth={ + 'api_username': 'csproot', + 'api_password': 'csproot', + 'api_url': 'https://localhost:8443', + 'api_version': 'v5_0' + } + ) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'missing required arguments: api_enterprise') + + def test_input_auth_url(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'command': 'find' + }, + auth={ + 'api_username': 'csproot', + 'api_password': 'csproot', + 'api_enterprise': 'csp', + 'api_version': 'v5_0' + } + ) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'missing required arguments: api_url') + + def test_input_auth_version(self): + set_module_args_custom_auth( + args={ + 'type': 'Enterprise', + 'command': 'find' + }, + auth={ + 'api_username': 'csproot', + 'api_password': 'csproot', + 'api_enterprise': 'csp', + 'api_url': 'https://localhost:8443', + } + ) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], 'missing required arguments: api_version') + + def test_input_exclusive(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + 'command': 'find' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "parameters are mutually exclusive: ['command', 'state']") + + def test_input_require_both_parent_id(self): + set_module_args(args={ + 'type': 'User', + 'command': 'find', + 'parent_type': 'Enterprise' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "parameters are required together: ['parent_id', 'parent_type']") + + def test_input_require_both_parent_type(self): + set_module_args(args={ + 'type': 'User', + 'command': 'find', + 'parent_id': 'enterprise-id' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "parameters are required together: ['parent_id', 'parent_type']") + + def test_input_require_on_off(self): + set_module_args(args={ + 'type': 'Enterprise' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "one of the following is required: command,state") + + def test_input_require_if_present(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'present', + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "state is present but the following are missing: id,properties,match_filter") + + def test_input_require_if_absent(self): + set_module_args(args={ + 'type': 'Enterprise', + 'state': 'absent', + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "state is absent but the following are missing: id,properties,match_filter") + + def test_input_require_if_change_password_id(self): + set_module_args(args={ + 'type': 'User', + 'command': 'change_password', + 'properties': { + 'password': 'dummy-password' + } + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "command is change_password but the following are missing: id") + + def test_input_require_if_change_password_properties(self): + set_module_args(args={ + 'type': 'User', + 'command': 'change_password', + 'id': 'user-id' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "command is change_password but the following are missing: properties") + + def test_input_require_if_wait_for_job_id(self): + set_module_args(args={ + 'type': 'Job', + 'command': 'wait_for_job' + }) + + with self.assertRaises(AnsibleFailJson) as exc: + nuage_vspk.main() + + result = exc.exception.args[0] + + self.assertTrue(result['failed']) + self.assertEqual(result['msg'], "command is wait_for_job but the following are missing: id")