diff --git a/lib/ansible/modules/network/nuage/__init__.py b/lib/ansible/modules/network/nuage/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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 0000000000..efe3e21911
--- /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 ebdac4ed9e..354cdf7d0b 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 0000000000..b59efbdcd9
--- /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 0000000000..a678773f18
--- /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 0000000000..78266b65b9
--- /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 0000000000..64ceb78f40
--- /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 0000000000..8e4e36a51a
--- /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 0000000000..744c0dec4e
--- /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 0000000000..bfeab9bf72
--- /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 0000000000..e69de29bb2
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 0000000000..2a38ca3981
--- /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 0000000000..28e2d685d7
--- /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")