1# -*- coding: utf-8 -*- 2# 3# Copyright (c) 2016 Red Hat, Inc. 4# 5# This file is part of Ansible 6# 7# Ansible is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# Ansible is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 19# 20 21from __future__ import (absolute_import, division, print_function) 22__metaclass__ = type 23 24import inspect 25import os 26import time 27 28from abc import ABCMeta, abstractmethod 29from datetime import datetime 30from distutils.version import LooseVersion 31 32from ansible_collections.ovirt.ovirt.plugins.module_utils.cloud import CloudRetry 33from ansible.module_utils.basic import env_fallback 34from ansible.module_utils.common._collections_compat import Mapping 35 36try: 37 from enum import Enum # enum is a ovirtsdk4 requirement 38 import ovirtsdk4 as sdk 39 import ovirtsdk4.version as sdk_version 40 import ovirtsdk4.types as otypes 41 HAS_SDK = LooseVersion(sdk_version.VERSION) >= LooseVersion('4.4.0') 42except ImportError: 43 HAS_SDK = False 44 45 46BYTES_MAP = { 47 'kib': 2**10, 48 'mib': 2**20, 49 'gib': 2**30, 50 'tib': 2**40, 51 'pib': 2**50, 52} 53 54 55def check_sdk(module): 56 if not HAS_SDK: 57 module.fail_json( 58 msg='ovirtsdk4 version 4.4.0 or higher is required for this module' 59 ) 60 61 62def remove_underscore(val): 63 if val.startswith('_'): 64 val = val[1:] 65 remove_underscore(val) 66 return val 67 68 69def get_dict_of_struct_follow(struct, filter_keys): 70 if isinstance(struct, sdk.Struct): 71 res = {} 72 for key, value in struct.__dict__.items(): 73 if value is None: 74 continue 75 key = remove_underscore(key) 76 if filter_keys is None or key in filter_keys: 77 res[key] = get_dict_of_struct_follow(value, filter_keys) 78 return res 79 elif isinstance(struct, Enum) or isinstance(struct, datetime): 80 return str(struct) 81 elif isinstance(struct, list) or isinstance(struct, sdk.List): 82 return [get_dict_of_struct_follow(i, filter_keys) for i in struct] 83 return struct 84 85 86def get_dict_of_struct(struct, connection=None, fetch_nested=False, attributes=None, filter_keys=None, follows=None): 87 """ 88 Convert SDK Struct type into dictionary. 89 """ 90 if follows: 91 return get_dict_of_struct_follow(struct, filter_keys) 92 93 res = {} 94 95 def resolve_href(value): 96 # Fetch nested values of struct: 97 try: 98 value = connection.follow_link(value) 99 except sdk.Error: 100 value = None 101 nested_obj = dict( 102 (attr, convert_value(getattr(value, attr))) 103 for attr in attributes if getattr(value, attr, None) is not None 104 ) 105 nested_obj['id'] = getattr(value, 'id', None) 106 nested_obj['href'] = getattr(value, 'href', None) 107 return nested_obj 108 109 def convert_value(value): 110 nested = False 111 112 if isinstance(value, sdk.Struct): 113 if not fetch_nested or not value.href: 114 return get_dict_of_struct(value) 115 return resolve_href(value) 116 117 elif isinstance(value, Enum) or isinstance(value, datetime): 118 return str(value) 119 elif isinstance(value, list) or isinstance(value, sdk.List): 120 if isinstance(value, sdk.List) and fetch_nested and value.href: 121 try: 122 value = connection.follow_link(value) 123 nested = True 124 except sdk.Error: 125 value = [] 126 127 ret = [] 128 for i in value: 129 if isinstance(i, sdk.Struct): 130 if not nested and fetch_nested and i.href: 131 ret.append(resolve_href(i)) 132 elif not nested: 133 ret.append(get_dict_of_struct(i)) 134 else: 135 nested_obj = dict( 136 (attr, convert_value(getattr(i, attr))) 137 for attr in attributes if getattr(i, attr, None) 138 ) 139 nested_obj['id'] = getattr(i, 'id', None) 140 ret.append(nested_obj) 141 elif isinstance(i, Enum): 142 ret.append(str(i)) 143 else: 144 ret.append(i) 145 return ret 146 else: 147 return value 148 149 if struct is not None: 150 for key, value in struct.__dict__.items(): 151 if value is None: 152 continue 153 154 key = remove_underscore(key) 155 if filter_keys is None: 156 res[key] = convert_value(value) 157 elif key in filter_keys: 158 res[key] = convert_value(value) 159 160 return res 161 162 163def engine_version(connection): 164 """ 165 Return string representation of oVirt engine version. 166 """ 167 engine_api = connection.system_service().get() 168 engine_version = engine_api.product_info.version 169 return '%s.%s' % (engine_version.major, engine_version.minor) 170 171 172def create_connection(auth): 173 """ 174 Create a connection to Python SDK, from task `auth` parameter. 175 If user doesnt't have SSO token the `auth` dictionary has following parameters mandatory: 176 url, username, password 177 178 If user has SSO token the `auth` dictionary has following parameters mandatory: 179 url, token 180 181 The `ca_file` parameter is mandatory in case user want to use secure connection, 182 in case user want to use insecure connection, it's mandatory to send insecure=True. 183 184 :param auth: dictionary which contains needed values for connection creation 185 :return: Python SDK connection 186 """ 187 188 url = auth.get('url') 189 if url is None and auth.get('hostname') is not None: 190 url = 'https://{0}/ovirt-engine/api'.format(auth.get('hostname')) 191 192 return sdk.Connection( 193 url=url, 194 username=auth.get('username'), 195 password=auth.get('password'), 196 ca_file=auth.get('ca_file', None), 197 insecure=auth.get('insecure', False), 198 token=auth.get('token', None), 199 kerberos=auth.get('kerberos', None), 200 headers=auth.get('headers', None), 201 ) 202 203 204def convert_to_bytes(param): 205 """ 206 This method convert units to bytes, which follow IEC standard. 207 208 :param param: value to be converted 209 """ 210 if param is None: 211 return None 212 213 # Get rid of whitespaces: 214 param = ''.join(param.split()) 215 216 # Convert to bytes: 217 if len(param) > 3 and param[-3].lower() in ['k', 'm', 'g', 't', 'p']: 218 return int(param[:-3]) * BYTES_MAP.get(param[-3:].lower(), 1) 219 elif param.isdigit(): 220 return int(param) * 2**10 221 else: 222 raise ValueError( 223 "Unsupported value(IEC supported): '{value}'".format(value=param) 224 ) 225 226 227def follow_link(connection, link): 228 """ 229 This method returns the entity of the element which link points to. 230 231 :param connection: connection to the Python SDK 232 :param link: link of the entity 233 :return: entity which link points to 234 """ 235 236 if link: 237 return connection.follow_link(link) 238 else: 239 return None 240 241 242def get_link_name(connection, link): 243 """ 244 This method returns the name of the element which link points to. 245 246 :param connection: connection to the Python SDK 247 :param link: link of the entity 248 :return: name of the entity, which link points to 249 """ 250 251 if link: 252 return connection.follow_link(link).name 253 else: 254 return None 255 256 257def equal(param1, param2, ignore_case=False): 258 """ 259 Compare two parameters and return if they are equal. 260 This parameter doesn't run equal operation if first parameter is None. 261 With this approach we don't run equal operation in case user don't 262 specify parameter in their task. 263 264 :param param1: user inputted parameter 265 :param param2: value of entity parameter 266 :return: True if parameters are equal or first parameter is None, otherwise False 267 """ 268 if param1 is not None: 269 if ignore_case: 270 return param1.lower() == param2.lower() 271 return param1 == param2 272 return True 273 274 275def search_by_attributes(service, list_params=None, **kwargs): 276 """ 277 Search for the entity by attributes. Nested entities don't support search 278 via REST, so in case using search for nested entity we return all entities 279 and filter them by specified attributes. 280 """ 281 list_params = list_params or {} 282 # Check if 'list' method support search(look for search parameter): 283 if 'search' in inspect.getargspec(service.list)[0]: 284 res = service.list( 285 # There must be double quotes around name, because some oVirt resources it's possible to create then with space in name. 286 search=' and '.join('{0}="{1}"'.format(k, v) for k, v in kwargs.items()), 287 **list_params 288 ) 289 else: 290 res = [ 291 e for e in service.list(**list_params) if len([ 292 k for k, v in kwargs.items() if getattr(e, k, None) == v 293 ]) == len(kwargs) 294 ] 295 296 res = res or [None] 297 return res[0] 298 299 300def search_by_name(service, name, **kwargs): 301 """ 302 Search for the entity by its name. Nested entities don't support search 303 via REST, so in case using search for nested entity we return all entities 304 and filter them by name. 305 306 :param service: service of the entity 307 :param name: name of the entity 308 :return: Entity object returned by Python SDK 309 """ 310 # Check if 'list' method support search(look for search parameter): 311 if 'search' in inspect.getargspec(service.list)[0]: 312 res = service.list( 313 # There must be double quotes around name, because some oVirt resources it's possible to create then with space in name. 314 search='name="{name}"'.format(name=name) 315 ) 316 else: 317 res = [e for e in service.list() if e.name == name] 318 319 if kwargs: 320 res = [ 321 e for e in service.list() if len([ 322 k for k, v in kwargs.items() if getattr(e, k, None) == v 323 ]) == len(kwargs) 324 ] 325 326 res = res or [None] 327 return res[0] 328 329 330def get_entity(service, get_params=None): 331 """ 332 Ignore SDK Error in case of getting an entity from service. 333 """ 334 entity = None 335 try: 336 if get_params is not None: 337 entity = service.get(**get_params) 338 else: 339 entity = service.get() 340 except sdk.Error: 341 # We can get here 404, we should ignore it, in case 342 # of removing entity for example. 343 pass 344 return entity 345 346 347def get_id_by_name(service, name, raise_error=True, ignore_case=False): 348 """ 349 Search an entity ID by it's name. 350 """ 351 entity = search_by_name(service, name) 352 353 if entity is not None: 354 return entity.id 355 356 if raise_error: 357 raise Exception("Entity '%s' was not found." % name) 358 359 360def wait( 361 service, 362 condition, 363 fail_condition=lambda e: False, 364 timeout=180, 365 wait=True, 366 poll_interval=3, 367): 368 """ 369 Wait until entity fulfill expected condition. 370 371 :param service: service of the entity 372 :param condition: condition to be fulfilled 373 :param fail_condition: if this condition is true, raise Exception 374 :param timeout: max time to wait in seconds 375 :param wait: if True wait for condition, if False don't wait 376 :param poll_interval: Number of seconds we should wait until next condition check 377 """ 378 # Wait until the desired state of the entity: 379 if wait: 380 start = time.time() 381 while time.time() < start + timeout: 382 # Exit if the condition of entity is valid: 383 entity = get_entity(service) 384 if condition(entity): 385 return 386 elif fail_condition(entity): 387 raise Exception("Error while waiting on result state of the entity.") 388 389 # Sleep for `poll_interval` seconds if none of the conditions apply: 390 time.sleep(float(poll_interval)) 391 392 raise Exception("Timeout exceed while waiting on result state of the entity.") 393 394 395def __get_auth_dict(): 396 return dict( 397 type='dict', 398 apply_defaults=True, 399 required=True, 400 required_one_of=[['hostname', 'url']], 401 options=dict( 402 url=dict( 403 type='str', 404 fallback=(env_fallback, ['OVIRT_URL']), 405 ), 406 hostname=dict( 407 type='str', 408 fallback=(env_fallback, ['OVIRT_HOSTNAME']), 409 ), 410 username=dict( 411 type='str', 412 fallback=(env_fallback, ['OVIRT_USERNAME']), 413 ), 414 password=dict( 415 type='str', 416 fallback=(env_fallback, ['OVIRT_PASSWORD']), 417 no_log=True, 418 ), 419 insecure=dict( 420 type='bool', 421 default=False, 422 ), 423 token=dict( 424 type='str', 425 fallback=(env_fallback, ['OVIRT_TOKEN']), 426 no_log=False, 427 ), 428 ca_file=dict( 429 type='str', 430 fallback=(env_fallback, ['OVIRT_CAFILE']), 431 ), 432 compress=dict( 433 type='bool', 434 default=True 435 ), 436 timeout=dict( 437 type='int', 438 default=0 439 ), 440 kerberos=dict(type='bool'), 441 headers=dict(type='dict') 442 ) 443 ) 444 445 446def ovirt_info_full_argument_spec(**kwargs): 447 """ 448 Extend parameters of info module with parameters which are common to all 449 oVirt info modules. 450 451 :param kwargs: kwargs to be extended 452 :return: extended dictionary with common parameters 453 """ 454 spec = dict( 455 auth=__get_auth_dict(), 456 fetch_nested=dict(default=False, type='bool'), 457 nested_attributes=dict(type='list', default=list(), elements='str'), 458 follows=dict(default=list(), type='list', elements='str'), 459 ) 460 spec.update(kwargs) 461 return spec 462 463 464# Left for third-party module compatibility 465def ovirt_facts_full_argument_spec(**kwargs): 466 """ 467 This is deprecated. Please use ovirt_info_full_argument_spec instead! 468 469 :param kwargs: kwargs to be extended 470 :return: extended dictionary with common parameters 471 """ 472 return ovirt_info_full_argument_spec(**kwargs) 473 474 475def ovirt_full_argument_spec(**kwargs): 476 """ 477 Extend parameters of module with parameters which are common to all oVirt modules. 478 479 :param kwargs: kwargs to be extended 480 :return: extended dictionary with common parameters 481 """ 482 spec = dict( 483 auth=__get_auth_dict(), 484 timeout=dict(default=180, type='int'), 485 wait=dict(default=True, type='bool'), 486 poll_interval=dict(default=3, type='int'), 487 fetch_nested=dict(default=False, type='bool'), 488 nested_attributes=dict(type='list', default=list(), elements='str'), 489 ) 490 spec.update(kwargs) 491 return spec 492 493 494def check_params(module): 495 """ 496 Most modules must have either `name` or `id` specified. 497 """ 498 if module.params.get('name') is None and module.params.get('id') is None: 499 module.fail_json(msg='"name" or "id" is required') 500 501 502def engine_supported(connection, version): 503 return LooseVersion(engine_version(connection)) >= LooseVersion(version) 504 505 506def check_support(version, connection, module, params): 507 """ 508 Check if parameters used by user are supported by oVirt Python SDK 509 and oVirt engine. 510 """ 511 api_version = LooseVersion(engine_version(connection)) 512 version = LooseVersion(version) 513 for param in params: 514 if module.params.get(param) is not None: 515 return LooseVersion(sdk_version.VERSION) >= version and api_version >= version 516 517 return True 518 519 520class BaseModule(object): 521 """ 522 This is base class for oVirt modules. oVirt modules should inherit this 523 class and override method to customize specific needs of the module. 524 The only abstract method of this class is `build_entity`, which must 525 to be implemented in child class. 526 """ 527 __metaclass__ = ABCMeta 528 529 def __init__(self, connection, module, service, changed=False): 530 self._connection = connection 531 self._module = module 532 self._service = service 533 self._changed = changed 534 self._diff = {'after': dict(), 'before': dict()} 535 536 @property 537 def changed(self): 538 return self._changed 539 540 @changed.setter 541 def changed(self, changed): 542 if not self._changed: 543 self._changed = changed 544 545 @abstractmethod 546 def build_entity(self): 547 """ 548 This method should return oVirt Python SDK type, which we want to 549 create or update, initialized by values passed by Ansible module. 550 551 For example if we want to create VM, we will return following: 552 types.Vm(name=self._module.params['vm_name']) 553 554 :return: Specific instance of sdk.Struct. 555 """ 556 pass 557 558 def param(self, name, default=None): 559 """ 560 Return a module parameter specified by it's name. 561 """ 562 return self._module.params.get(name, default) 563 564 def update_check(self, entity): 565 """ 566 This method handle checks whether the entity values are same as values 567 passed to ansible module. By default we don't compare any values. 568 569 :param entity: Entity we want to compare with Ansible module values. 570 :return: True if values are same, so we don't need to update the entity. 571 """ 572 return True 573 574 def pre_create(self, entity): 575 """ 576 This method is called right before entity is created. 577 578 :param entity: Entity to be created or updated. 579 """ 580 pass 581 582 def post_create(self, entity): 583 """ 584 This method is called right after entity is created. 585 586 :param entity: Entity which was created. 587 """ 588 pass 589 590 def post_update(self, entity): 591 """ 592 This method is called right after entity is updated. 593 594 :param entity: Entity which was updated. 595 """ 596 pass 597 598 def diff_update(self, after, update): 599 for k, v in update.items(): 600 if isinstance(v, Mapping): 601 after[k] = self.diff_update(after.get(k, dict()), v) 602 else: 603 after[k] = update[k] 604 return after 605 606 def create( 607 self, 608 entity=None, 609 result_state=None, 610 fail_condition=lambda e: False, 611 search_params=None, 612 update_params=None, 613 _wait=None, 614 force_create=False, 615 **kwargs 616 ): 617 """ 618 Method which is called when state of the entity is 'present'. If user 619 don't provide `entity` parameter the entity is searched using 620 `search_params` parameter. If entity is found it's updated, whether 621 the entity should be updated is checked by `update_check` method. 622 The corresponding updated entity is build by `build_entity` method. 623 624 Function executed after entity is created can optionally be specified 625 in `post_create` parameter. Function executed after entity is updated 626 can optionally be specified in `post_update` parameter. 627 628 :param entity: Entity we want to update, if exists. 629 :param result_state: State which should entity has in order to finish task. 630 :param fail_condition: Function which checks incorrect state of entity, if it returns `True` Exception is raised. 631 :param search_params: Dictionary of parameters to be used for search. 632 :param update_params: The params which should be passed to update method. 633 :param kwargs: Additional parameters passed when creating entity. 634 :return: Dictionary with values returned by Ansible module. 635 """ 636 if entity is None and not force_create: 637 entity = self.search_entity(search_params) 638 639 self.pre_create(entity) 640 641 if entity: 642 # Entity exists, so update it: 643 entity_service = self._service.service(entity.id) 644 if not self.update_check(entity): 645 new_entity = self.build_entity() 646 if not self._module.check_mode: 647 update_params = update_params or {} 648 updated_entity = entity_service.update( 649 new_entity, 650 **update_params 651 ) 652 self.post_update(entity) 653 654 # Update diffs only if user specified --diff parameter, 655 # so we don't useless overload API: 656 if self._module._diff: 657 before = get_dict_of_struct( 658 entity, 659 self._connection, 660 fetch_nested=True, 661 attributes=['name'], 662 ) 663 after = before.copy() 664 self.diff_update(after, get_dict_of_struct(new_entity)) 665 self._diff['before'] = before 666 self._diff['after'] = after 667 668 self.changed = True 669 else: 670 # Entity don't exists, so create it: 671 if not self._module.check_mode: 672 entity = self._service.add( 673 self.build_entity(), 674 **kwargs 675 ) 676 self.post_create(entity) 677 self.changed = True 678 679 if not self._module.check_mode: 680 # Wait for the entity to be created and to be in the defined state: 681 entity_service = self._service.service(entity.id) 682 683 def state_condition(entity): 684 return entity 685 686 if result_state: 687 688 def state_condition(entity): 689 return entity and entity.status == result_state 690 691 wait( 692 service=entity_service, 693 condition=state_condition, 694 fail_condition=fail_condition, 695 wait=_wait if _wait is not None else self._module.params['wait'], 696 timeout=self._module.params['timeout'], 697 poll_interval=self._module.params['poll_interval'], 698 ) 699 700 return { 701 'changed': self.changed, 702 'id': getattr(entity, 'id', None), 703 type(entity).__name__.lower(): get_dict_of_struct( 704 struct=entity, 705 connection=self._connection, 706 fetch_nested=self._module.params.get('fetch_nested'), 707 attributes=self._module.params.get('nested_attributes'), 708 ), 709 'diff': self._diff, 710 } 711 712 def pre_remove(self, entity): 713 """ 714 This method is called right before entity is removed. 715 716 :param entity: Entity which we want to remove. 717 """ 718 pass 719 720 def entity_name(self, entity): 721 return "{e_type} '{e_name}'".format( 722 e_type=type(entity).__name__.lower(), 723 e_name=getattr(entity, 'name', None), 724 ) 725 726 def remove(self, entity=None, search_params=None, **kwargs): 727 """ 728 Method which is called when state of the entity is 'absent'. If user 729 don't provide `entity` parameter the entity is searched using 730 `search_params` parameter. If entity is found it's removed. 731 732 Function executed before remove is executed can optionally be specified 733 in `pre_remove` parameter. 734 735 :param entity: Entity we want to remove. 736 :param search_params: Dictionary of parameters to be used for search. 737 :param kwargs: Additional parameters passed when removing entity. 738 :return: Dictionary with values returned by Ansible module. 739 """ 740 if entity is None: 741 entity = self.search_entity(search_params) 742 743 if entity is None: 744 return { 745 'changed': self.changed, 746 'msg': "Entity wasn't found." 747 } 748 749 self.pre_remove(entity) 750 751 entity_service = self._service.service(entity.id) 752 if not self._module.check_mode: 753 entity_service.remove(**kwargs) 754 wait( 755 service=entity_service, 756 condition=lambda entity: not entity, 757 wait=self._module.params['wait'], 758 timeout=self._module.params['timeout'], 759 poll_interval=self._module.params['poll_interval'], 760 ) 761 self.changed = True 762 763 return { 764 'changed': self.changed, 765 'id': entity.id, 766 type(entity).__name__.lower(): get_dict_of_struct( 767 struct=entity, 768 connection=self._connection, 769 fetch_nested=self._module.params.get('fetch_nested'), 770 attributes=self._module.params.get('nested_attributes'), 771 ), 772 } 773 774 def action( 775 self, 776 action, 777 entity=None, 778 action_condition=lambda e: e, 779 wait_condition=lambda e: e, 780 fail_condition=lambda e: False, 781 pre_action=lambda e: e, 782 post_action=lambda e: None, 783 search_params=None, 784 **kwargs 785 ): 786 """ 787 This method is executed when we want to change the state of some oVirt 788 entity. The action to be executed on oVirt service is specified by 789 `action` parameter. Whether the action should be executed can be 790 specified by passing `action_condition` parameter. State which the 791 entity should be in after execution of the action can be specified 792 by `wait_condition` parameter. 793 794 Function executed before an action on entity can optionally be specified 795 in `pre_action` parameter. Function executed after an action on entity can 796 optionally be specified in `post_action` parameter. 797 798 :param action: Action which should be executed by service on entity. 799 :param entity: Entity we want to run action on. 800 :param action_condition: Function which is executed when checking if action should be executed. 801 :param fail_condition: Function which checks incorrect state of entity, if it returns `True` Exception is raised. 802 :param wait_condition: Function which is executed when waiting on result state. 803 :param pre_action: Function which is executed before running the action. 804 :param post_action: Function which is executed after running the action. 805 :param search_params: Dictionary of parameters to be used for search. 806 :param kwargs: Additional parameters passed to action. 807 :return: Dictionary with values returned by Ansible module. 808 """ 809 if entity is None: 810 entity = self.search_entity(search_params) 811 812 entity = pre_action(entity) 813 814 if entity is None: 815 self._module.fail_json( 816 msg="Entity not found, can't run action '{0}'.".format( 817 action 818 ) 819 ) 820 821 entity_service = self._service.service(entity.id) 822 entity = entity_service.get() 823 if action_condition(entity): 824 if not self._module.check_mode: 825 getattr(entity_service, action)(**kwargs) 826 self.changed = True 827 828 post_action(entity) 829 830 wait( 831 service=self._service.service(entity.id), 832 condition=wait_condition, 833 fail_condition=fail_condition, 834 wait=self._module.params['wait'], 835 timeout=self._module.params['timeout'], 836 poll_interval=self._module.params['poll_interval'], 837 ) 838 return { 839 'changed': self.changed, 840 'id': entity.id, 841 type(entity).__name__.lower(): get_dict_of_struct( 842 struct=entity, 843 connection=self._connection, 844 fetch_nested=self._module.params.get('fetch_nested'), 845 attributes=self._module.params.get('nested_attributes'), 846 ), 847 'diff': self._diff, 848 } 849 850 def wait_for_import(self, condition=lambda e: True): 851 if self._module.params['wait']: 852 start = time.time() 853 timeout = self._module.params['timeout'] 854 poll_interval = self._module.params['poll_interval'] 855 while time.time() < start + timeout: 856 entity = self.search_entity() 857 if entity and condition(entity): 858 return entity 859 time.sleep(poll_interval) 860 861 def search_entity(self, search_params=None, list_params=None): 862 """ 863 Always first try to search by `ID`, if ID isn't specified, 864 check if user constructed special search in `search_params`, 865 if not search by `name`. 866 """ 867 entity = None 868 869 if 'id' in self._module.params and self._module.params['id'] is not None: 870 entity = get_entity(self._service.service(self._module.params['id']), get_params=list_params) 871 elif search_params is not None: 872 entity = search_by_attributes(self._service, list_params=list_params, **search_params) 873 elif self._module.params.get('name') is not None: 874 entity = search_by_attributes(self._service, list_params=list_params, name=self._module.params['name']) 875 876 return entity 877 878 def _get_major(self, full_version): 879 if full_version is None or full_version == "": 880 return None 881 if isinstance(full_version, otypes.Version): 882 return int(full_version.major) 883 return int(full_version.split('.')[0]) 884 885 def _get_minor(self, full_version): 886 if full_version is None or full_version == "": 887 return None 888 if isinstance(full_version, otypes.Version): 889 return int(full_version.minor) 890 return int(full_version.split('.')[1]) 891 892 893def _sdk4_error_maybe(): 894 """ 895 Allow for ovirtsdk4 not being installed. 896 """ 897 if HAS_SDK: 898 return sdk.Error 899 return type(None) 900 901 902class OvirtRetry(CloudRetry): 903 base_class = _sdk4_error_maybe() 904 905 @staticmethod 906 def status_code_from_exception(error): 907 return error.code 908 909 @staticmethod 910 def found(response_code, catch_extra_error_codes=None): 911 # This is a list of error codes to retry. 912 retry_on = [ 913 # HTTP status: Conflict 914 409, 915 ] 916 if catch_extra_error_codes: 917 retry_on.extend(catch_extra_error_codes) 918 919 return response_code in retry_on 920