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