# -*- coding: utf-8 -*- # # Copyright (c) 2016 Red Hat, Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # import collections import inspect import os import time from abc import ABCMeta, abstractmethod from datetime import datetime from distutils.version import LooseVersion try: from enum import Enum # enum is a ovirtsdk4 requirement import ovirtsdk4 as sdk import ovirtsdk4.version as sdk_version HAS_SDK = LooseVersion(sdk_version.VERSION) >= LooseVersion('4.2.4') except ImportError: HAS_SDK = False BYTES_MAP = { 'kib': 2**10, 'mib': 2**20, 'gib': 2**30, 'tib': 2**40, 'pib': 2**50, } def check_sdk(module): if not HAS_SDK: module.fail_json( msg='ovirtsdk4 version 4.2.4 or higher is required for this module' ) def get_dict_of_struct(struct, connection=None, fetch_nested=False, attributes=None): """ Convert SDK Struct type into dictionary. """ res = {} def remove_underscore(val): if val.startswith('_'): val = val[1:] remove_underscore(val) return val def convert_value(value): nested = False if isinstance(value, sdk.Struct): return get_dict_of_struct(value) elif isinstance(value, Enum) or isinstance(value, datetime): return str(value) elif isinstance(value, list) or isinstance(value, sdk.List): if isinstance(value, sdk.List) and fetch_nested and value.href: try: value = connection.follow_link(value) nested = True except sdk.Error: value = [] ret = [] for i in value: if isinstance(i, sdk.Struct): if not nested: ret.append(get_dict_of_struct(i)) else: nested_obj = dict( (attr, convert_value(getattr(i, attr))) for attr in attributes if getattr(i, attr, None) ) nested_obj['id'] = getattr(i, 'id', None) ret.append(nested_obj) elif isinstance(i, Enum): ret.append(str(i)) else: ret.append(i) return ret else: return value if struct is not None: for key, value in struct.__dict__.items(): if value is None: continue key = remove_underscore(key) res[key] = convert_value(value) return res def engine_version(connection): """ Return string representation of oVirt engine version. """ engine_api = connection.system_service().get() engine_version = engine_api.product_info.version return '%s.%s' % (engine_version.major, engine_version.minor) def create_connection(auth): """ Create a connection to Python SDK, from task `auth` parameter. If user doesnt't have SSO token the `auth` dictionary has following parameters mandatory: url, username, password If user has SSO token the `auth` dictionary has following parameters mandatory: url, token The `ca_file` parameter is mandatory in case user want to use secure connection, in case user want to use insecure connection, it's mandatory to send insecure=True. :param auth: dictionary which contains needed values for connection creation :return: Python SDK connection """ return sdk.Connection( url=auth.get('url'), username=auth.get('username'), password=auth.get('password'), ca_file=auth.get('ca_file', None), insecure=auth.get('insecure', False), token=auth.get('token', None), kerberos=auth.get('kerberos', None), headers=auth.get('headers', None), ) def convert_to_bytes(param): """ This method convert units to bytes, which follow IEC standard. :param param: value to be converted """ if param is None: return None # Get rid of whitespaces: param = ''.join(param.split()) # Convert to bytes: if param[-3].lower() in ['k', 'm', 'g', 't', 'p']: return int(param[:-3]) * BYTES_MAP.get(param[-3:].lower(), 1) elif param.isdigit(): return int(param) * 2**10 else: raise ValueError( "Unsupported value(IEC supported): '{value}'".format(value=param) ) def follow_link(connection, link): """ This method returns the entity of the element which link points to. :param connection: connection to the Python SDK :param link: link of the entity :return: entity which link points to """ if link: return connection.follow_link(link) else: return None def get_link_name(connection, link): """ This method returns the name of the element which link points to. :param connection: connection to the Python SDK :param link: link of the entity :return: name of the entity, which link points to """ if link: return connection.follow_link(link).name else: return None def equal(param1, param2, ignore_case=False): """ Compare two parameters and return if they are equal. This parameter doesn't run equal operation if first parameter is None. With this approach we don't run equal operation in case user don't specify parameter in their task. :param param1: user inputted parameter :param param2: value of entity parameter :return: True if parameters are equal or first parameter is None, otherwise False """ if param1 is not None: if ignore_case: return param1.lower() == param2.lower() return param1 == param2 return True def search_by_attributes(service, list_params=None, **kwargs): """ Search for the entity by attributes. Nested entities don't support search via REST, so in case using search for nested entity we return all entities and filter them by specified attributes. """ list_params = list_params or {} # Check if 'list' method support search(look for search parameter): if 'search' in inspect.getargspec(service.list)[0]: res = service.list( search=' and '.join('{0}={1}'.format(k, v) for k, v in kwargs.items()), **list_params ) else: res = [ e for e in service.list(**list_params) if len([ k for k, v in kwargs.items() if getattr(e, k, None) == v ]) == len(kwargs) ] res = res or [None] return res[0] def search_by_name(service, name, **kwargs): """ Search for the entity by its name. Nested entities don't support search via REST, so in case using search for nested entity we return all entities and filter them by name. :param service: service of the entity :param name: name of the entity :return: Entity object returned by Python SDK """ # Check if 'list' method support search(look for search parameter): if 'search' in inspect.getargspec(service.list)[0]: res = service.list( search="name={name}".format(name=name) ) else: res = [e for e in service.list() if e.name == name] if kwargs: res = [ e for e in service.list() if len([ k for k, v in kwargs.items() if getattr(e, k, None) == v ]) == len(kwargs) ] res = res or [None] return res[0] def get_entity(service, get_params=None): """ Ignore SDK Error in case of getting an entity from service. """ entity = None try: if get_params is not None: entity = service.get(**get_params) else: entity = service.get() except sdk.Error: # We can get here 404, we should ignore it, in case # of removing entity for example. pass return entity def get_id_by_name(service, name, raise_error=True, ignore_case=False): """ Search an entity ID by it's name. """ entity = search_by_name(service, name) if entity is not None: return entity.id if raise_error: raise Exception("Entity '%s' was not found." % name) def wait( service, condition, fail_condition=lambda e: False, timeout=180, wait=True, poll_interval=3, ): """ Wait until entity fulfill expected condition. :param service: service of the entity :param condition: condition to be fulfilled :param fail_condition: if this condition is true, raise Exception :param timeout: max time to wait in seconds :param wait: if True wait for condition, if False don't wait :param poll_interval: Number of seconds we should wait until next condition check """ # Wait until the desired state of the entity: if wait: start = time.time() while time.time() < start + timeout: # Exit if the condition of entity is valid: entity = get_entity(service) if condition(entity): return elif fail_condition(entity): raise Exception("Error while waiting on result state of the entity.") # Sleep for `poll_interval` seconds if none of the conditions apply: time.sleep(float(poll_interval)) raise Exception("Timeout exceed while waiting on result state of the entity.") def __get_auth_dict(): OVIRT_URL = os.environ.get('OVIRT_URL') OVIRT_USERNAME = os.environ.get('OVIRT_USERNAME') OVIRT_PASSWORD = os.environ.get('OVIRT_PASSWORD') OVIRT_TOKEN = os.environ.get('OVIRT_TOKEN') OVIRT_CAFILE = os.environ.get('OVIRT_CAFILE') OVIRT_INSECURE = OVIRT_CAFILE is None env_vars = None if OVIRT_URL and ((OVIRT_USERNAME and OVIRT_PASSWORD) or OVIRT_TOKEN): env_vars = { 'url': OVIRT_URL, 'username': OVIRT_USERNAME, 'password': OVIRT_PASSWORD, 'insecure': OVIRT_INSECURE, 'token': OVIRT_TOKEN, 'ca_file': OVIRT_CAFILE, } if env_vars is not None: auth = dict(default=env_vars, type='dict') else: auth = dict(required=True, type='dict') return auth def ovirt_facts_full_argument_spec(**kwargs): """ Extend parameters of facts module with parameters which are common to all oVirt facts modules. :param kwargs: kwargs to be extended :return: extended dictionary with common parameters """ spec = dict( auth=__get_auth_dict(), fetch_nested=dict(default=False, type='bool'), nested_attributes=dict(type='list', default=list()), ) spec.update(kwargs) return spec def ovirt_full_argument_spec(**kwargs): """ Extend parameters of module with parameters which are common to all oVirt modules. :param kwargs: kwargs to be extended :return: extended dictionary with common parameters """ spec = dict( auth=__get_auth_dict(), timeout=dict(default=180, type='int'), wait=dict(default=True, type='bool'), poll_interval=dict(default=3, type='int'), fetch_nested=dict(default=False, type='bool'), nested_attributes=dict(type='list', default=list()), ) spec.update(kwargs) return spec def check_params(module): """ Most modules must have either `name` or `id` specified. """ if module.params.get('name') is None and module.params.get('id') is None: module.fail_json(msg='"name" or "id" is required') def engine_supported(connection, version): return LooseVersion(engine_version(connection)) >= LooseVersion(version) def check_support(version, connection, module, params): """ Check if parameters used by user are supported by oVirt Python SDK and oVirt engine. """ api_version = LooseVersion(engine_version(connection)) version = LooseVersion(version) for param in params: if module.params.get(param) is not None: return LooseVersion(sdk_version.VERSION) >= version and api_version >= version return True class BaseModule(object): """ This is base class for oVirt modules. oVirt modules should inherit this class and override method to customize specific needs of the module. The only abstract method of this class is `build_entity`, which must to be implemented in child class. """ __metaclass__ = ABCMeta def __init__(self, connection, module, service, changed=False): self._connection = connection self._module = module self._service = service self._changed = changed self._diff = {'after': dict(), 'before': dict()} @property def changed(self): return self._changed @changed.setter def changed(self, changed): if not self._changed: self._changed = changed @abstractmethod def build_entity(self): """ This method should return oVirt Python SDK type, which we want to create or update, initialized by values passed by Ansible module. For example if we want to create VM, we will return following: types.Vm(name=self._module.params['vm_name']) :return: Specific instance of sdk.Struct. """ pass def param(self, name, default=None): """ Return a module parameter specified by it's name. """ return self._module.params.get(name, default) def update_check(self, entity): """ This method handle checks whether the entity values are same as values passed to ansible module. By default we don't compare any values. :param entity: Entity we want to compare with Ansible module values. :return: True if values are same, so we don't need to update the entity. """ return True def pre_create(self, entity): """ This method is called right before entity is created. :param entity: Entity to be created or updated. """ pass def post_create(self, entity): """ This method is called right after entity is created. :param entity: Entity which was created. """ pass def post_update(self, entity): """ This method is called right after entity is updated. :param entity: Entity which was updated. """ pass def diff_update(self, after, update): for k, v in update.items(): if isinstance(v, collections.Mapping): after[k] = self.diff_update(after.get(k, dict()), v) else: after[k] = update[k] return after def create( self, entity=None, result_state=None, fail_condition=lambda e: False, search_params=None, update_params=None, **kwargs ): """ Method which is called when state of the entity is 'present'. If user don't provide `entity` parameter the entity is searched using `search_params` parameter. If entity is found it's updated, whether the entity should be updated is checked by `update_check` method. The corresponding updated entity is build by `build_entity` method. Function executed after entity is created can optionally be specified in `post_create` parameter. Function executed after entity is updated can optionally be specified in `post_update` parameter. :param entity: Entity we want to update, if exists. :param result_state: State which should entity has in order to finish task. :param fail_condition: Function which checks incorrect state of entity, if it returns `True` Exception is raised. :param search_params: Dictionary of parameters to be used for search. :param update_params: The params which should be passed to update method. :param kwargs: Additional parameters passed when creating entity. :return: Dictionary with values returned by Ansible module. """ if entity is None: entity = self.search_entity(search_params) self.pre_create(entity) if entity: # Entity exists, so update it: entity_service = self._service.service(entity.id) if not self.update_check(entity): new_entity = self.build_entity() if not self._module.check_mode: update_params = update_params or {} updated_entity = entity_service.update( new_entity, **update_params ) self.post_update(entity) # Update diffs only if user specified --diff parameter, # so we don't useless overload API: if self._module._diff: before = get_dict_of_struct( entity, self._connection, fetch_nested=True, attributes=['name'], ) after = before.copy() self.diff_update(after, get_dict_of_struct(new_entity)) self._diff['before'] = before self._diff['after'] = after self.changed = True else: # Entity don't exists, so create it: if not self._module.check_mode: entity = self._service.add( self.build_entity(), **kwargs ) self.post_create(entity) self.changed = True if not self._module.check_mode: # Wait for the entity to be created and to be in the defined state: entity_service = self._service.service(entity.id) def state_condition(entity): return entity if result_state: def state_condition(entity): return entity and entity.status == result_state wait( service=entity_service, condition=state_condition, fail_condition=fail_condition, wait=self._module.params['wait'], timeout=self._module.params['timeout'], poll_interval=self._module.params['poll_interval'], ) return { 'changed': self.changed, 'id': getattr(entity, 'id', None), type(entity).__name__.lower(): get_dict_of_struct( struct=entity, connection=self._connection, fetch_nested=self._module.params.get('fetch_nested'), attributes=self._module.params.get('nested_attributes'), ), 'diff': self._diff, } def pre_remove(self, entity): """ This method is called right before entity is removed. :param entity: Entity which we want to remove. """ pass def entity_name(self, entity): return "{e_type} '{e_name}'".format( e_type=type(entity).__name__.lower(), e_name=getattr(entity, 'name', None), ) def remove(self, entity=None, search_params=None, **kwargs): """ Method which is called when state of the entity is 'absent'. If user don't provide `entity` parameter the entity is searched using `search_params` parameter. If entity is found it's removed. Function executed before remove is executed can optionally be specified in `pre_remove` parameter. :param entity: Entity we want to remove. :param search_params: Dictionary of parameters to be used for search. :param kwargs: Additional parameters passed when removing entity. :return: Dictionary with values returned by Ansible module. """ if entity is None: entity = self.search_entity(search_params) if entity is None: return { 'changed': self.changed, 'msg': "Entity wasn't found." } self.pre_remove(entity) entity_service = self._service.service(entity.id) if not self._module.check_mode: entity_service.remove(**kwargs) wait( service=entity_service, condition=lambda entity: not entity, wait=self._module.params['wait'], timeout=self._module.params['timeout'], poll_interval=self._module.params['poll_interval'], ) self.changed = True return { 'changed': self.changed, 'id': entity.id, type(entity).__name__.lower(): get_dict_of_struct( struct=entity, connection=self._connection, fetch_nested=self._module.params.get('fetch_nested'), attributes=self._module.params.get('nested_attributes'), ), } def action( self, action, entity=None, action_condition=lambda e: e, wait_condition=lambda e: e, fail_condition=lambda e: False, pre_action=lambda e: e, post_action=lambda e: None, search_params=None, **kwargs ): """ This method is executed when we want to change the state of some oVirt entity. The action to be executed on oVirt service is specified by `action` parameter. Whether the action should be executed can be specified by passing `action_condition` parameter. State which the entity should be in after execution of the action can be specified by `wait_condition` parameter. Function executed before an action on entity can optionally be specified in `pre_action` parameter. Function executed after an action on entity can optionally be specified in `post_action` parameter. :param action: Action which should be executed by service on entity. :param entity: Entity we want to run action on. :param action_condition: Function which is executed when checking if action should be executed. :param fail_condition: Function which checks incorrect state of entity, if it returns `True` Exception is raised. :param wait_condition: Function which is executed when waiting on result state. :param pre_action: Function which is executed before running the action. :param post_action: Function which is executed after running the action. :param search_params: Dictionary of parameters to be used for search. :param kwargs: Additional parameters passed to action. :return: Dictionary with values returned by Ansible module. """ if entity is None: entity = self.search_entity(search_params) entity = pre_action(entity) if entity is None: self._module.fail_json( msg="Entity not found, can't run action '{0}'.".format( action ) ) entity_service = self._service.service(entity.id) entity = entity_service.get() if action_condition(entity): if not self._module.check_mode: getattr(entity_service, action)(**kwargs) self.changed = True post_action(entity) wait( service=self._service.service(entity.id), condition=wait_condition, fail_condition=fail_condition, wait=self._module.params['wait'], timeout=self._module.params['timeout'], poll_interval=self._module.params['poll_interval'], ) return { 'changed': self.changed, 'id': entity.id, type(entity).__name__.lower(): get_dict_of_struct( struct=entity, connection=self._connection, fetch_nested=self._module.params.get('fetch_nested'), attributes=self._module.params.get('nested_attributes'), ), 'diff': self._diff, } def wait_for_import(self, condition=lambda e: True): if self._module.params['wait']: start = time.time() timeout = self._module.params['timeout'] poll_interval = self._module.params['poll_interval'] while time.time() < start + timeout: entity = self.search_entity() if entity and condition(entity): return entity time.sleep(poll_interval) def search_entity(self, search_params=None, list_params=None): """ Always first try to search by `ID`, if ID isn't specified, check if user constructed special search in `search_params`, if not search by `name`. """ entity = None if 'id' in self._module.params and self._module.params['id'] is not None: entity = get_entity(self._service.service(self._module.params['id']), get_params=list_params) elif search_params is not None: entity = search_by_attributes(self._service, list_params=list_params, **search_params) elif self._module.params.get('name') is not None: entity = search_by_attributes(self._service, list_params=list_params, name=self._module.params['name']) return entity