From 5326d7ca8c1444f3cfd12195a4a195e7e6e3040a Mon Sep 17 00:00:00 2001 From: Nadav Goldin Date: Sun, 19 Mar 2017 22:51:16 +0200 Subject: [PATCH] ovirtlago: adding waiters.py This new class creates a common infrastructure for tasks that require polling results periodically from the SDK. The methods in the 'waiters' class are generated from the data/waiters.yaml file. Each method defined in the YAML is in charge of polling the Engine for a status of a request. In this commit only 3 methods were configured in the YAML: waiters.vm_up(api, id) waiters.vm_down(api, id) waiters.host_up(api, id) Invoking each of these methods will check if the VM with the given id reached an 'accepting' state as defined in the YAML 'acceptors' section. It will run for 'max_attempts', with a 'delay' between and block until. The polling might end earlier, if the VM reached any of the 'rejectors' states as defined in the YAML, and then a 'WaiterError' exception will be thrown. Adding more 'waiters' requires only updating the YAML. Signed-off-by: Nadav Goldin --- ovirtlago/data/waiters.yaml | 45 ++++++++++++++++ ovirtlago/waiters.py | 100 ++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 ovirtlago/data/waiters.yaml create mode 100644 ovirtlago/waiters.py diff --git a/ovirtlago/data/waiters.yaml b/ovirtlago/data/waiters.yaml new file mode 100644 index 00000000..6987c4ea --- /dev/null +++ b/ovirtlago/data/waiters.yaml @@ -0,0 +1,45 @@ +common: &common + max_attempts: 20 + delay: 10 + +waiters: + - waiter_name: 'host_up' + <<: *common + service: hosts_service + target: + name: 'host_service' + path: 'get' + acceptors: + - name: 'HostStatus' + type: status + paths: ['UP'] + rejectors: + - name: 'HostStatus' + type: status + paths: ["NON_OPERATIONAL", "INSTALL_FAILED", "NON_RESPONSIVE"] + + - waiter_name: 'vm_up' + <<: *common + service: vms_service + target: + name: 'vm_service' + path: 'get' + acceptors: + - name: 'VmStatus' + type: status + paths: ["UP"] + rejectors: + - name: 'VmStatus' + type: status + paths: ["NOT_RESPONDING", "SUSPENDED"] + + - waiter_name: 'vm_down' + <<: *common + service: vms_service + target: + name: 'vm_service' + path: 'get' + acceptors: + - name: 'VmStatus' + type: status + paths: ["DOWN"] diff --git a/ovirtlago/waiters.py b/ovirtlago/waiters.py new file mode 100644 index 00000000..17883a3e --- /dev/null +++ b/ovirtlago/waiters.py @@ -0,0 +1,100 @@ +import time + +import pkg_resources +import yaml + +from ovirtsdk4 import types as ovirt_types + + +class WaiterError(BaseException): + pass + + +class Waiter(object): + def __init__(self, config): + self.config = config + self.state = 'unknown' + self.name = config['waiter_name'] + self.rejectors = self._build_matchers(config.get('rejectors', [])) + self.acceptors = self._build_matchers(config['acceptors']) + self.target = self.config['target']['name'] + self.target_path = self.config['target']['path'] + self.operation = self._build_operation() + + # TO-DO: create a matcher class which holds more details about + # the result. + + def _build_matchers(self, matchers): + callables = [] + for matcher in matchers: + if matcher['type'] == 'status': + callables.append(self._build_status_matcher(matcher)) + return callables + + def _build_status_matcher(self, matcher): + def exact_match(result): + base = ( + getattr(getattr(ovirt_types, matcher['name']), path) + for path in matcher['paths'] + ) + return result.status in base + + return exact_match + + def _build_operation(self): + def func(api, *args, **kwargs): + system_service = getattr(api, 'system_service')() + service = getattr(system_service, self.config['service'])() + target = getattr(service, self.target)(*args, **kwargs) + return getattr(target, self.target_path)() + + return func + + def wait(self, api, *args, **kwargs): + max_attempts = kwargs.pop('max_attempts', + False) or self.config['max_attempts'] + delay = kwargs.pop('delay', False) or self.config['delay'] + attempts = 0 + max_attempts = int(max_attempts) + delay = int(delay) + + while True: + res = self.operation(api, *args, **kwargs) + attempts = attempts + 1 + for acceptor in self.acceptors: + if acceptor(res): + self.state = 'success' + break + + for rejector in self.rejectors: + if rejector(res): + self.state = 'failed' + raise WaiterError('rejected state') + + if self.state == 'success': + return + + if attempts >= max_attempts: + raise WaiterError('Maximum number of attempts exceeded') + time.sleep(delay) + + +class _waiters_meta(type): + def __init__(self, name, bases, d): + type.__init__(self, name, bases, d) + config = pkg_resources.resource_filename( + __name__, '/'.join(['data', 'waiters.yaml']) + ) + with open(config, 'r') as waiters_fd: + waiters_cfg = yaml.load(waiters_fd)['waiters'] + + for waiter_cfg in waiters_cfg: + waiter = Waiter(config=waiter_cfg) + # TO-DO: attach docstring for the wait method + # according to the waiter name + setattr(self, waiter.name, waiter.wait) + + +class waiters: + __metaclass__ = _waiters_meta + pass