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