1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# (c) 2017, Nokia
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = '''
12---
13module: nuage_vspk
14short_description: Manage Nuage VSP environments
15description:
16    - Manage or find Nuage VSP entities, this includes create, update, delete, assign, unassign and find, with all supported properties.
17author: Philippe Dellaert (@pdellaert)
18options:
19    auth:
20        description:
21            - Dict with the authentication information required to connect to a Nuage VSP environment.
22            - Requires a I(api_username) parameter (example csproot).
23            - Requires either a I(api_password) parameter (example csproot) or a I(api_certificate) and I(api_key) parameters,
24              which point to the certificate and key files for certificate based authentication.
25            - Requires a I(api_enterprise) parameter (example csp).
26            - Requires a I(api_url) parameter (example https://10.0.0.10:8443).
27            - Requires a I(api_version) parameter (example v4_0).
28        required: true
29    type:
30        description:
31            - The type of entity you want to work on (example Enterprise).
32            - This should match the objects CamelCase class name in VSPK-Python.
33            - This Class name can be found on U(https://nuagenetworks.github.io/vspkdoc/index.html).
34        required: true
35    id:
36        description:
37            - The ID of the entity you want to work on.
38            - In combination with I(command=find), it will only return the single entity.
39            - In combination with I(state), it will either update or delete this entity.
40            - Will take precedence over I(match_filter) and I(properties) whenever an entity needs to be found.
41    parent_id:
42        description:
43            - The ID of the parent of the entity you want to work on.
44            - When I(state) is specified, the entity will be gathered from this parent, if it exists, unless an I(id) is specified.
45            - When I(command=find) is specified, the entity will be searched for in this parent, unless an I(id) is specified.
46            - If specified, I(parent_type) also needs to be specified.
47    parent_type:
48        description:
49            - The type of parent the ID is specified for (example Enterprise).
50            - This should match the objects CamelCase class name in VSPK-Python.
51            - This Class name can be found on U(https://nuagenetworks.github.io/vspkdoc/index.html).
52            - If specified, I(parent_id) also needs to be specified.
53    state:
54        description:
55            - Specifies the desired state of the entity.
56            - If I(state=present), in case the entity already exists, will update the entity if it is needed.
57            - 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.
58            - 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.
59            - Either I(state) or I(command) needs to be defined, both can not be defined at the same time.
60        choices:
61            - present
62            - absent
63    command:
64        description:
65            - Specifies a command to be executed.
66            - With I(command=find), if I(parent_id) and I(parent_type) are defined, it will only search within the parent. Otherwise, if allowed,
67              will search in the root object.
68            - With I(command=find), if I(id) is specified, it will only return the single entity matching the id.
69            - With I(command=find), otherwise, if I(match_filter) is define, it will use that filter to search.
70            - With I(command=find), otherwise, if I(properties) are defined, it will do an AND search using all properties.
71            - With I(command=change_password), a password of a user can be changed. Warning - In case the password is the same as the existing,
72              it will throw an error.
73            - 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,
74              the module will exit with an error.
75            - With I(command=wait_for_job), the job will always be returned, even if the state is ERROR situation.
76            - Either I(state) or I(command) needs to be defined, both can not be defined at the same time.
77        choices:
78            - find
79            - change_password
80            - wait_for_job
81            - get_csp_enterprise
82    match_filter:
83        description:
84            - A filter used when looking (both in I(command) and I(state) for entities, in the format the Nuage VSP API expects.
85            - If I(match_filter) is defined, it will take precedence over the I(properties), but not on the I(id)
86    properties:
87        description:
88            - Properties are the key, value pairs of the different properties an entity has.
89            - If no I(id) and no I(match_filter) is specified, these are used to find or determine if the entity exists.
90    children:
91        description:
92            - Can be used to specify a set of child entities.
93            - A mandatory property of each child is the I(type).
94            - Supported optional properties of each child are I(id), I(properties) and I(match_filter).
95            - The function of each of these properties is the same as in the general task definition.
96            - This can be used recursively
97            - Only useable in case I(state=present).
98notes:
99    - 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.
100    - 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.
101requirements:
102    - Python 2.7
103    - Supports Nuage VSP 4.0Rx & 5.x.y
104    - Proper VSPK-Python installed for your Nuage version
105    - Tested with NuageX U(https://nuagex.io)
106'''
107
108EXAMPLES = '''
109# This can be executed as a single role, with the following vars
110# vars:
111#   auth:
112#     api_username: csproot
113#     api_password: csproot
114#     api_enterprise: csp
115#     api_url: https://10.0.0.10:8443
116#     api_version: v5_0
117#   enterprise_name: Ansible-Enterprise
118#   enterprise_new_name: Ansible-Updated-Enterprise
119#
120# or, for certificate based authentication
121# vars:
122#   auth:
123#     api_username: csproot
124#     api_certificate: /path/to/user-certificate.pem
125#     api_key: /path/to/user-Key.pem
126#     api_enterprise: csp
127#     api_url: https://10.0.0.10:8443
128#     api_version: v5_0
129#   enterprise_name: Ansible-Enterprise
130#   enterprise_new_name: Ansible-Updated-Enterprise
131
132# Creating a new enterprise
133- name: Create Enterprise
134  connection: local
135  community.network.nuage_vspk:
136    auth: "{{ nuage_auth }}"
137    type: Enterprise
138    state: present
139    properties:
140      name: "{{ enterprise_name }}-basic"
141  register: nuage_enterprise
142
143# Checking if an Enterprise with the new name already exists
144- name: Check if an Enterprise exists with the new name
145  connection: local
146  community.network.nuage_vspk:
147    auth: "{{ nuage_auth }}"
148    type: Enterprise
149    command: find
150    properties:
151      name: "{{ enterprise_new_name }}-basic"
152  ignore_errors: yes
153  register: nuage_check_enterprise
154
155# Updating an enterprise's name
156- name: Update Enterprise name
157  connection: local
158  community.network.nuage_vspk:
159    auth: "{{ nuage_auth }}"
160    type: Enterprise
161    id: "{{ nuage_enterprise.id }}"
162    state: present
163    properties:
164      name: "{{ enterprise_new_name }}-basic"
165  when: nuage_check_enterprise is failed
166
167# Creating a User in an Enterprise
168- name: Create admin user
169  connection: local
170  community.network.nuage_vspk:
171    auth: "{{ nuage_auth }}"
172    type: User
173    parent_id: "{{ nuage_enterprise.id }}"
174    parent_type: Enterprise
175    state: present
176    match_filter: "userName == 'ansible-admin'"
177    properties:
178      email: "ansible@localhost.local"
179      first_name: "Ansible"
180      last_name: "Admin"
181      password: "ansible-password"
182      user_name: "ansible-admin"
183  register: nuage_user
184
185# Updating password for User
186- name: Update admin password
187  connection: local
188  community.network.nuage_vspk:
189    auth: "{{ nuage_auth }}"
190    type: User
191    id: "{{ nuage_user.id }}"
192    command: change_password
193    properties:
194      password: "ansible-new-password"
195  ignore_errors: yes
196
197# Finding a group in an enterprise
198- name: Find Administrators group in Enterprise
199  connection: local
200  community.network.nuage_vspk:
201    auth: "{{ nuage_auth }}"
202    type: Group
203    parent_id: "{{ nuage_enterprise.id }}"
204    parent_type: Enterprise
205    command: find
206    properties:
207      name: "Administrators"
208  register: nuage_group
209
210# Assign the user to the group
211- name: Assign admin user to administrators
212  connection: local
213  community.network.nuage_vspk:
214    auth: "{{ nuage_auth }}"
215    type: User
216    id: "{{ nuage_user.id }}"
217    parent_id: "{{ nuage_group.id }}"
218    parent_type: Group
219    state: present
220
221# Creating multiple DomainTemplates
222- name: Create multiple DomainTemplates
223  connection: local
224  community.network.nuage_vspk:
225    auth: "{{ nuage_auth }}"
226    type: DomainTemplate
227    parent_id: "{{ nuage_enterprise.id }}"
228    parent_type: Enterprise
229    state: present
230    properties:
231      name: "{{ item }}"
232      description: "Created by Ansible"
233  with_items:
234    - "Template-1"
235    - "Template-2"
236
237# Finding all DomainTemplates
238- name: Fetching all DomainTemplates
239  connection: local
240  community.network.nuage_vspk:
241    auth: "{{ nuage_auth }}"
242    type: DomainTemplate
243    parent_id: "{{ nuage_enterprise.id }}"
244    parent_type: Enterprise
245    command: find
246  register: nuage_domain_templates
247
248# Deleting all DomainTemplates
249- name: Deleting all found DomainTemplates
250  connection: local
251  community.network.nuage_vspk:
252    auth: "{{ nuage_auth }}"
253    type: DomainTemplate
254    state: absent
255    id: "{{ item.ID }}"
256  with_items: "{{ nuage_domain_templates.entities }}"
257  when: nuage_domain_templates.entities is defined
258
259# Unassign user from group
260- name: Unassign admin user to administrators
261  connection: local
262  community.network.nuage_vspk:
263    auth: "{{ nuage_auth }}"
264    type: User
265    id: "{{ nuage_user.id }}"
266    parent_id: "{{ nuage_group.id }}"
267    parent_type: Group
268    state: absent
269
270# Deleting an enterprise
271- name: Delete Enterprise
272  connection: local
273  community.network.nuage_vspk:
274    auth: "{{ nuage_auth }}"
275    type: Enterprise
276    id: "{{ nuage_enterprise.id }}"
277    state: absent
278
279# Setup an enterprise with Children
280- name: Setup Enterprise and domain structure
281  connection: local
282  community.network.nuage_vspk:
283    auth: "{{ nuage_auth }}"
284    type: Enterprise
285    state: present
286    properties:
287      name: "Child-based-Enterprise"
288    children:
289    - type: L2DomainTemplate
290      properties:
291        name: "Unmanaged-Template"
292      children:
293      - type: EgressACLTemplate
294        match_filter: "name == 'Allow All'"
295        properties:
296          name: "Allow All"
297          active: true
298          default_allow_ip: true
299          default_allow_non_ip: true
300          default_install_acl_implicit_rules: true
301          description: "Created by Ansible"
302          priority_type: "TOP"
303      - type: IngressACLTemplate
304        match_filter: "name == 'Allow All'"
305        properties:
306          name: "Allow All"
307          active: true
308          default_allow_ip: true
309          default_allow_non_ip: true
310          description: "Created by Ansible"
311          priority_type: "TOP"
312'''
313
314RETURN = '''
315id:
316    description: The id of the entity that was found, created, updated or assigned.
317    returned: On state=present and command=find in case one entity was found.
318    type: str
319    sample: bae07d8d-d29c-4e2b-b6ba-621b4807a333
320entities:
321    description: A list of entities handled. Each element is the to_dict() of the entity.
322    returned: On state=present and find, with only one element in case of state=present or find=one.
323    type: list
324    sample: [{
325        "ID": acabc435-3946-4117-a719-b8895a335830",
326        "assocEntityType": "DOMAIN",
327        "command": "BEGIN_POLICY_CHANGES",
328        "creationDate": 1487515656000,
329        "entityScope": "ENTERPRISE",
330        "externalID": null,
331        "lastUpdatedBy": "8a6f0e20-a4db-4878-ad84-9cc61756cd5e",
332        "lastUpdatedDate": 1487515656000,
333        "owner": "8a6f0e20-a4db-4878-ad84-9cc61756cd5e",
334        "parameters": null,
335        "parentID": "a22fddb9-3da4-4945-bd2e-9d27fe3d62e0",
336        "parentType": "domain",
337        "progress": 0.0,
338        "result": null,
339        "status": "RUNNING"
340        }]
341'''
342
343import time
344
345try:
346    import importlib
347    HAS_IMPORTLIB = True
348except ImportError:
349    HAS_IMPORTLIB = False
350
351try:
352    from bambou.exceptions import BambouHTTPError
353    HAS_BAMBOU = True
354except ImportError:
355    HAS_BAMBOU = False
356
357from ansible.module_utils.basic import AnsibleModule
358
359
360SUPPORTED_COMMANDS = ['find', 'change_password', 'wait_for_job', 'get_csp_enterprise']
361VSPK = None
362
363
364class NuageEntityManager(object):
365    """
366    This module is meant to manage an entity in a Nuage VSP Platform
367    """
368
369    def __init__(self, module):
370        self.module = module
371        self.auth = module.params['auth']
372        self.api_username = None
373        self.api_password = None
374        self.api_enterprise = None
375        self.api_url = None
376        self.api_version = None
377        self.api_certificate = None
378        self.api_key = None
379        self.type = module.params['type']
380
381        self.state = module.params['state']
382        self.command = module.params['command']
383        self.match_filter = module.params['match_filter']
384        self.entity_id = module.params['id']
385        self.parent_id = module.params['parent_id']
386        self.parent_type = module.params['parent_type']
387        self.properties = module.params['properties']
388        self.children = module.params['children']
389
390        self.entity = None
391        self.entity_class = None
392        self.parent = None
393        self.parent_class = None
394        self.entity_fetcher = None
395
396        self.result = {
397            'state': self.state,
398            'id': self.entity_id,
399            'entities': []
400        }
401        self.nuage_connection = None
402
403        self._verify_api()
404        self._verify_input()
405        self._connect_vspk()
406        self._find_parent()
407
408    def _connect_vspk(self):
409        """
410        Connects to a Nuage API endpoint
411        """
412        try:
413            # Connecting to Nuage
414            if self.api_certificate and self.api_key:
415                self.nuage_connection = VSPK.NUVSDSession(username=self.api_username, enterprise=self.api_enterprise, api_url=self.api_url,
416                                                          certificate=(self.api_certificate, self.api_key))
417            else:
418                self.nuage_connection = VSPK.NUVSDSession(username=self.api_username, password=self.api_password, enterprise=self.api_enterprise,
419                                                          api_url=self.api_url)
420            self.nuage_connection.start()
421        except BambouHTTPError as error:
422            self.module.fail_json(msg='Unable to connect to the API URL with given username, password and enterprise: {0}'.format(error))
423
424    def _verify_api(self):
425        """
426        Verifies the API and loads the proper VSPK version
427        """
428        # Checking auth parameters
429        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
430                                                                                                'api_key' not in list(self.auth.keys()) or
431                                                                                                not self.auth['api_certificate'] or not self.auth['api_key']):
432            self.module.fail_json(msg='Missing api_password or api_certificate and api_key parameter in auth')
433
434        self.api_username = self.auth['api_username']
435        if 'api_password' in list(self.auth.keys()) and self.auth['api_password']:
436            self.api_password = self.auth['api_password']
437        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']:
438            self.api_certificate = self.auth['api_certificate']
439            self.api_key = self.auth['api_key']
440        self.api_enterprise = self.auth['api_enterprise']
441        self.api_url = self.auth['api_url']
442        self.api_version = self.auth['api_version']
443
444        try:
445            global VSPK
446            VSPK = importlib.import_module('vspk.{0:s}'.format(self.api_version))
447        except ImportError:
448            self.module.fail_json(msg='vspk is required for this module, or the API version specified does not exist.')
449
450    def _verify_input(self):
451        """
452        Verifies the parameter input for types and parent correctness and necessary parameters
453        """
454
455        # Checking if type exists
456        try:
457            self.entity_class = getattr(VSPK, 'NU{0:s}'.format(self.type))
458        except AttributeError:
459            self.module.fail_json(msg='Unrecognised type specified')
460
461        if self.module.check_mode:
462            return
463
464        if self.parent_type:
465            # Checking if parent type exists
466            try:
467                self.parent_class = getattr(VSPK, 'NU{0:s}'.format(self.parent_type))
468            except AttributeError:
469                # The parent type does not exist, fail
470                self.module.fail_json(msg='Unrecognised parent type specified')
471
472            fetcher = self.parent_class().fetcher_for_rest_name(self.entity_class.rest_name)
473            if fetcher is None:
474                # The parent has no fetcher, fail
475                self.module.fail_json(msg='Specified parent is not a valid parent for the specified type')
476        elif not self.entity_id:
477            # If there is an id, we do not need a parent because we'll interact directly with the entity
478            # If an assign needs to happen, a parent will have to be provided
479            # Root object is the parent
480            self.parent_class = VSPK.NUMe
481            fetcher = self.parent_class().fetcher_for_rest_name(self.entity_class.rest_name)
482            if fetcher is None:
483                self.module.fail_json(msg='No parent specified and root object is not a parent for the type')
484
485        # Verifying if a password is provided in case of the change_password command:
486        if self.command and self.command == 'change_password' and 'password' not in self.properties.keys():
487            self.module.fail_json(msg='command is change_password but the following are missing: password property')
488
489    def _find_parent(self):
490        """
491        Fetches the parent if needed, otherwise configures the root object as parent. Also configures the entity fetcher
492        Important notes:
493        - If the parent is not set, the parent is automatically set to the root object
494        - It the root object does not hold a fetcher for the entity, you have to provide an ID
495        - If you want to assign/unassign, you have to provide a valid parent
496        """
497        self.parent = self.nuage_connection.user
498
499        if self.parent_id:
500            self.parent = self.parent_class(id=self.parent_id)
501            try:
502                self.parent.fetch()
503            except BambouHTTPError as error:
504                self.module.fail_json(msg='Failed to fetch the specified parent: {0}'.format(error))
505
506        self.entity_fetcher = self.parent.fetcher_for_rest_name(self.entity_class.rest_name)
507
508    def _find_entities(self, entity_id=None, entity_class=None, match_filter=None, properties=None, entity_fetcher=None):
509        """
510        Will return a set of entities matching a filter or set of properties if the match_filter is unset. If the
511        entity_id is set, it will return only the entity matching that ID as the single element of the list.
512        :param entity_id: Optional ID of the entity which should be returned
513        :param entity_class: Optional class of the entity which needs to be found
514        :param match_filter: Optional search filter
515        :param properties: Optional set of properties the entities should contain
516        :param entity_fetcher: The fetcher for the entity type
517        :return: List of matching entities
518        """
519        search_filter = ''
520
521        if entity_id:
522            found_entity = entity_class(id=entity_id)
523            try:
524                found_entity.fetch()
525            except BambouHTTPError as error:
526                self.module.fail_json(msg='Failed to fetch the specified entity by ID: {0}'.format(error))
527
528            return [found_entity]
529
530        elif match_filter:
531            search_filter = match_filter
532        elif properties:
533            # Building filter
534            for num, property_name in enumerate(properties):
535                if num > 0:
536                    search_filter += ' and '
537                search_filter += '{0:s} == "{1}"'.format(property_name, properties[property_name])
538
539        if entity_fetcher is not None:
540            try:
541                return entity_fetcher.get(filter=search_filter)
542            except BambouHTTPError:
543                pass
544        return []
545
546    def _find_entity(self, entity_id=None, entity_class=None, match_filter=None, properties=None, entity_fetcher=None):
547        """
548        Finds a single matching entity that matches all the provided properties, unless an ID is specified, in which
549        case it just fetches the one item
550        :param entity_id: Optional ID of the entity which should be returned
551        :param entity_class: Optional class of the entity which needs to be found
552        :param match_filter: Optional search filter
553        :param properties: Optional set of properties the entities should contain
554        :param entity_fetcher: The fetcher for the entity type
555        :return: The first entity matching the criteria, or None if none was found
556        """
557        search_filter = ''
558        if entity_id:
559            found_entity = entity_class(id=entity_id)
560            try:
561                found_entity.fetch()
562            except BambouHTTPError as error:
563                self.module.fail_json(msg='Failed to fetch the specified entity by ID: {0}'.format(error))
564
565            return found_entity
566
567        elif match_filter:
568            search_filter = match_filter
569        elif properties:
570            # Building filter
571            for num, property_name in enumerate(properties):
572                if num > 0:
573                    search_filter += ' and '
574                search_filter += '{0:s} == "{1}"'.format(property_name, properties[property_name])
575
576        if entity_fetcher is not None:
577            try:
578                return entity_fetcher.get_first(filter=search_filter)
579            except BambouHTTPError:
580                pass
581        return None
582
583    def handle_main_entity(self):
584        """
585        Handles the Ansible task
586        """
587        if self.command and self.command == 'find':
588            self._handle_find()
589        elif self.command and self.command == 'change_password':
590            self._handle_change_password()
591        elif self.command and self.command == 'wait_for_job':
592            self._handle_wait_for_job()
593        elif self.command and self.command == 'get_csp_enterprise':
594            self._handle_get_csp_enterprise()
595        elif self.state == 'present':
596            self._handle_present()
597        elif self.state == 'absent':
598            self._handle_absent()
599        self.module.exit_json(**self.result)
600
601    def _handle_absent(self):
602        """
603        Handles the Ansible task when the state is set to absent
604        """
605        # Absent state
606        self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties,
607                                        entity_fetcher=self.entity_fetcher)
608        if self.entity and (self.entity_fetcher is None or self.entity_fetcher.relationship in ['child', 'root']):
609            # Entity is present, deleting
610            if self.module.check_mode:
611                self.result['changed'] = True
612            else:
613                self._delete_entity(self.entity)
614                self.result['id'] = None
615        elif self.entity and self.entity_fetcher.relationship == 'member':
616            # Entity is a member, need to check if already present
617            if self._is_member(entity_fetcher=self.entity_fetcher, entity=self.entity):
618                # Entity is not a member yet
619                if self.module.check_mode:
620                    self.result['changed'] = True
621                else:
622                    self._unassign_member(entity_fetcher=self.entity_fetcher, entity=self.entity, entity_class=self.entity_class, parent=self.parent,
623                                          set_output=True)
624
625    def _handle_present(self):
626        """
627        Handles the Ansible task when the state is set to present
628        """
629        # Present state
630        self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties,
631                                        entity_fetcher=self.entity_fetcher)
632        # Determining action to take
633        if self.entity_fetcher is not None and self.entity_fetcher.relationship == 'member' and not self.entity:
634            self.module.fail_json(msg='Trying to assign an entity that does not exist')
635        elif self.entity_fetcher is not None and self.entity_fetcher.relationship == 'member' and self.entity:
636            # Entity is a member, need to check if already present
637            if not self._is_member(entity_fetcher=self.entity_fetcher, entity=self.entity):
638                # Entity is not a member yet
639                if self.module.check_mode:
640                    self.result['changed'] = True
641                else:
642                    self._assign_member(entity_fetcher=self.entity_fetcher, entity=self.entity, entity_class=self.entity_class, parent=self.parent,
643                                        set_output=True)
644        elif self.entity_fetcher is not None and self.entity_fetcher.relationship in ['child', 'root'] and not self.entity:
645            # Entity is not present as a child, creating
646            if self.module.check_mode:
647                self.result['changed'] = True
648            else:
649                self.entity = self._create_entity(entity_class=self.entity_class, parent=self.parent, properties=self.properties)
650                self.result['id'] = self.entity.id
651                self.result['entities'].append(self.entity.to_dict())
652
653            # Checking children
654            if self.children:
655                for child in self.children:
656                    self._handle_child(child=child, parent=self.entity)
657        elif self.entity:
658            # Need to compare properties in entity and found entity
659            changed = self._has_changed(entity=self.entity, properties=self.properties)
660
661            if self.module.check_mode:
662                self.result['changed'] = changed
663            elif changed:
664                self.entity = self._save_entity(entity=self.entity)
665                self.result['id'] = self.entity.id
666                self.result['entities'].append(self.entity.to_dict())
667            else:
668                self.result['id'] = self.entity.id
669                self.result['entities'].append(self.entity.to_dict())
670
671            # Checking children
672            if self.children:
673                for child in self.children:
674                    self._handle_child(child=child, parent=self.entity)
675        elif not self.module.check_mode:
676            self.module.fail_json(msg='Invalid situation, verify parameters')
677
678    def _handle_get_csp_enterprise(self):
679        """
680        Handles the Ansible task when the command is to get the csp enterprise
681        """
682        self.entity_id = self.parent.enterprise_id
683        self.entity = VSPK.NUEnterprise(id=self.entity_id)
684        try:
685            self.entity.fetch()
686        except BambouHTTPError as error:
687            self.module.fail_json(msg='Unable to fetch CSP enterprise: {0}'.format(error))
688        self.result['id'] = self.entity_id
689        self.result['entities'].append(self.entity.to_dict())
690
691    def _handle_wait_for_job(self):
692        """
693        Handles the Ansible task when the command is to wait for a job
694        """
695        # Command wait_for_job
696        self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties,
697                                        entity_fetcher=self.entity_fetcher)
698        if self.module.check_mode:
699            self.result['changed'] = True
700        else:
701            self._wait_for_job(self.entity)
702
703    def _handle_change_password(self):
704        """
705        Handles the Ansible task when the command is to change a password
706        """
707        # Command change_password
708        self.entity = self._find_entity(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties,
709                                        entity_fetcher=self.entity_fetcher)
710        if self.module.check_mode:
711            self.result['changed'] = True
712        else:
713            try:
714                getattr(self.entity, 'password')
715            except AttributeError:
716                self.module.fail_json(msg='Entity does not have a password property')
717
718            try:
719                setattr(self.entity, 'password', self.properties['password'])
720            except AttributeError:
721                self.module.fail_json(msg='Password can not be changed for entity')
722
723            self.entity = self._save_entity(entity=self.entity)
724            self.result['id'] = self.entity.id
725            self.result['entities'].append(self.entity.to_dict())
726
727    def _handle_find(self):
728        """
729        Handles the Ansible task when the command is to find an entity
730        """
731        # Command find
732        entities = self._find_entities(entity_id=self.entity_id, entity_class=self.entity_class, match_filter=self.match_filter, properties=self.properties,
733                                       entity_fetcher=self.entity_fetcher)
734        self.result['changed'] = False
735        if entities:
736            if len(entities) == 1:
737                self.result['id'] = entities[0].id
738            for entity in entities:
739                self.result['entities'].append(entity.to_dict())
740        elif not self.module.check_mode:
741            self.module.fail_json(msg='Unable to find matching entries')
742
743    def _handle_child(self, child, parent):
744        """
745        Handles children of a main entity. Fields are similar to the normal fields
746        Currently only supported state: present
747        """
748        if 'type' not in list(child.keys()):
749            self.module.fail_json(msg='Child type unspecified')
750        elif 'id' not in list(child.keys()) and 'properties' not in list(child.keys()):
751            self.module.fail_json(msg='Child ID or properties unspecified')
752
753        # Setting intern variables
754        child_id = None
755        if 'id' in list(child.keys()):
756            child_id = child['id']
757        child_properties = None
758        if 'properties' in list(child.keys()):
759            child_properties = child['properties']
760        child_filter = None
761        if 'match_filter' in list(child.keys()):
762            child_filter = child['match_filter']
763
764        # Checking if type exists
765        entity_class = None
766        try:
767            entity_class = getattr(VSPK, 'NU{0:s}'.format(child['type']))
768        except AttributeError:
769            self.module.fail_json(msg='Unrecognised child type specified')
770
771        entity_fetcher = parent.fetcher_for_rest_name(entity_class.rest_name)
772        if entity_fetcher is None and not child_id and not self.module.check_mode:
773            self.module.fail_json(msg='Unable to find a fetcher for child, and no ID specified.')
774
775        # Try and find the child
776        entity = self._find_entity(entity_id=child_id, entity_class=entity_class, match_filter=child_filter, properties=child_properties,
777                                   entity_fetcher=entity_fetcher)
778
779        # Determining action to take
780        if entity_fetcher.relationship == 'member' and not entity:
781            self.module.fail_json(msg='Trying to assign a child that does not exist')
782        elif entity_fetcher.relationship == 'member' and entity:
783            # Entity is a member, need to check if already present
784            if not self._is_member(entity_fetcher=entity_fetcher, entity=entity):
785                # Entity is not a member yet
786                if self.module.check_mode:
787                    self.result['changed'] = True
788                else:
789                    self._assign_member(entity_fetcher=entity_fetcher, entity=entity, entity_class=entity_class, parent=parent, set_output=False)
790        elif entity_fetcher.relationship in ['child', 'root'] and not entity:
791            # Entity is not present as a child, creating
792            if self.module.check_mode:
793                self.result['changed'] = True
794            else:
795                entity = self._create_entity(entity_class=entity_class, parent=parent, properties=child_properties)
796        elif entity_fetcher.relationship in ['child', 'root'] and entity:
797            changed = self._has_changed(entity=entity, properties=child_properties)
798
799            if self.module.check_mode:
800                self.result['changed'] = changed
801            elif changed:
802                entity = self._save_entity(entity=entity)
803
804        if entity:
805            self.result['entities'].append(entity.to_dict())
806
807        # Checking children
808        if 'children' in list(child.keys()) and not self.module.check_mode:
809            for subchild in child['children']:
810                self._handle_child(child=subchild, parent=entity)
811
812    def _has_changed(self, entity, properties):
813        """
814        Compares a set of properties with a given entity, returns True in case the properties are different from the
815        values in the entity
816        :param entity: The entity to check
817        :param properties: The properties to check
818        :return: boolean
819        """
820        # Need to compare properties in entity and found entity
821        changed = False
822        if properties:
823            for property_name in list(properties.keys()):
824                if property_name == 'password':
825                    continue
826                entity_value = ''
827                try:
828                    entity_value = getattr(entity, property_name)
829                except AttributeError:
830                    self.module.fail_json(msg='Property {0:s} is not valid for this type of entity'.format(property_name))
831
832                if entity_value != properties[property_name]:
833                    # Difference in values changing property
834                    changed = True
835                    try:
836                        setattr(entity, property_name, properties[property_name])
837                    except AttributeError:
838                        self.module.fail_json(msg='Property {0:s} can not be changed for this type of entity'.format(property_name))
839        return changed
840
841    def _is_member(self, entity_fetcher, entity):
842        """
843        Verifies if the entity is a member of the parent in the fetcher
844        :param entity_fetcher: The fetcher for the entity type
845        :param entity: The entity to look for as a member in the entity fetcher
846        :return: boolean
847        """
848        members = entity_fetcher.get()
849        for member in members:
850            if member.id == entity.id:
851                return True
852        return False
853
854    def _assign_member(self, entity_fetcher, entity, entity_class, parent, set_output):
855        """
856        Adds the entity as a member to a parent
857        :param entity_fetcher: The fetcher of the entity type
858        :param entity: The entity to add as a member
859        :param entity_class: The class of the entity
860        :param parent: The parent on which to add the entity as a member
861        :param set_output: If set to True, sets the Ansible result variables
862        """
863        members = entity_fetcher.get()
864        members.append(entity)
865        try:
866            parent.assign(members, entity_class)
867        except BambouHTTPError as error:
868            self.module.fail_json(msg='Unable to assign entity as a member: {0}'.format(error))
869        self.result['changed'] = True
870        if set_output:
871            self.result['id'] = entity.id
872            self.result['entities'].append(entity.to_dict())
873
874    def _unassign_member(self, entity_fetcher, entity, entity_class, parent, set_output):
875        """
876        Removes the entity as a member of a parent
877        :param entity_fetcher: The fetcher of the entity type
878        :param entity: The entity to remove as a member
879        :param entity_class: The class of the entity
880        :param parent: The parent on which to add the entity as a member
881        :param set_output: If set to True, sets the Ansible result variables
882        """
883        members = []
884        for member in entity_fetcher.get():
885            if member.id != entity.id:
886                members.append(member)
887        try:
888            parent.assign(members, entity_class)
889        except BambouHTTPError as error:
890            self.module.fail_json(msg='Unable to remove entity as a member: {0}'.format(error))
891        self.result['changed'] = True
892        if set_output:
893            self.result['id'] = entity.id
894            self.result['entities'].append(entity.to_dict())
895
896    def _create_entity(self, entity_class, parent, properties):
897        """
898        Creates a new entity in the parent, with all properties configured as in the file
899        :param entity_class: The class of the entity
900        :param parent: The parent of the entity
901        :param properties: The set of properties of the entity
902        :return: The entity
903        """
904        entity = entity_class(**properties)
905        try:
906            parent.create_child(entity)
907        except BambouHTTPError as error:
908            self.module.fail_json(msg='Unable to create entity: {0}'.format(error))
909        self.result['changed'] = True
910        return entity
911
912    def _save_entity(self, entity):
913        """
914        Updates an existing entity
915        :param entity: The entity to save
916        :return: The updated entity
917        """
918        try:
919            entity.save()
920        except BambouHTTPError as error:
921            self.module.fail_json(msg='Unable to update entity: {0}'.format(error))
922        self.result['changed'] = True
923        return entity
924
925    def _delete_entity(self, entity):
926        """
927        Deletes an entity
928        :param entity: The entity to delete
929        """
930        try:
931            entity.delete()
932        except BambouHTTPError as error:
933            self.module.fail_json(msg='Unable to delete entity: {0}'.format(error))
934        self.result['changed'] = True
935
936    def _wait_for_job(self, entity):
937        """
938        Waits for a job to finish
939        :param entity: The job to wait for
940        """
941        running = False
942        if entity.status == 'RUNNING':
943            self.result['changed'] = True
944            running = True
945
946        while running:
947            time.sleep(1)
948            entity.fetch()
949
950            if entity.status != 'RUNNING':
951                running = False
952
953        self.result['entities'].append(entity.to_dict())
954        if entity.status == 'ERROR':
955            self.module.fail_json(msg='Job ended in an error')
956
957
958def main():
959    """
960    Main method
961    """
962    module = AnsibleModule(
963        argument_spec=dict(
964            auth=dict(
965                required=True,
966                type='dict',
967                options=dict(
968                    api_username=dict(required=True, type='str'),
969                    api_enterprise=dict(required=True, type='str'),
970                    api_url=dict(required=True, type='str'),
971                    api_version=dict(required=True, type='str'),
972                    api_password=dict(default=None, required=False, type='str', no_log=True),
973                    api_certificate=dict(default=None, required=False, type='str', no_log=True),
974                    api_key=dict(default=None, required=False, type='str', no_log=True)
975                )
976            ),
977            type=dict(required=True, type='str'),
978            id=dict(default=None, required=False, type='str'),
979            parent_id=dict(default=None, required=False, type='str'),
980            parent_type=dict(default=None, required=False, type='str'),
981            state=dict(default=None, choices=['present', 'absent'], type='str'),
982            command=dict(default=None, choices=SUPPORTED_COMMANDS, type='str'),
983            match_filter=dict(default=None, required=False, type='str'),
984            properties=dict(default=None, required=False, type='dict'),
985            children=dict(default=None, required=False, type='list')
986        ),
987        mutually_exclusive=[
988            ['command', 'state']
989        ],
990        required_together=[
991            ['parent_id', 'parent_type']
992        ],
993        required_one_of=[
994            ['command', 'state']
995        ],
996        required_if=[
997            ['state', 'present', ['id', 'properties', 'match_filter'], True],
998            ['state', 'absent', ['id', 'properties', 'match_filter'], True],
999            ['command', 'change_password', ['id', 'properties']],
1000            ['command', 'wait_for_job', ['id']]
1001        ],
1002        supports_check_mode=True
1003    )
1004
1005    if not HAS_BAMBOU:
1006        module.fail_json(msg='bambou is required for this module')
1007
1008    if not HAS_IMPORTLIB:
1009        module.fail_json(msg='importlib (python 2.7) is required for this module')
1010
1011    entity_manager = NuageEntityManager(module)
1012    entity_manager.handle_main_entity()
1013
1014
1015if __name__ == '__main__':
1016    main()
1017