Source code for smc.core.waiters

#  Licensed under the Apache License, Version 2.0 (the "License"); you may
#  not use this file except in compliance with the License. You may obtain
#  a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#  License for the specific language governing permissions and limitations
#  under the License.

"""
Waiters are convenience classes that use blocking or non-blocking threads to
monitor for a particular state of an engine node.

A waiter can have a callback added that will be executed after either
the state has matched, a number of iterations exceeded or an exception is
caught while monitoring. The callback should be a callable that takes a single
argument.

They provide the ability to perform logical actions such as "wait for the engine to
have status 'Configured', then fire a policy upload task".

Example of waiting for an engine to be ready, then send policy::

    class ContainerPolicyCallback(object):
        def __init__(self, container):
            self.engine = engine

        def __call__(self, status):
            if status == 'Configured':
                self.engine.upload(policy='MyPolicy')

    engine = Engine('myengine')
    callback = ContainerPolicyCallback(engine)

    waiter = ConfigurationStatusWaiter(engine.nodes[0], 'Configured')
    waiter.add_done_callback(callback)

Waiters can also be blocking while waiting for status. Example of using a waiter
to block input while waiting for the engine to reach a specific status::

    waiter = ConfigurationStatusWaiter(node, 'Initial', max_wait=5)
    while not waiter.done():
        print("Status after 5 sec wait: %s" % waiter.result(5))

"""
import time
import threading
from smc.compat import PYTHON_v3_9

#: Configuration status constant values

CFG_STATUS = frozenset(["Initial", "Declared", "Configured", "Installed"])

#: Node status constant values
STATUS = frozenset(
    [
        "Not Monitored",
        "Unknown",
        "Online",
        "Going Online",
        "Locked Online",
        "Going Locked Online",
        "Offline",
        "Going Offline",
        "Locked Offline",
        "Going Locked Offline",
        "Standby",
        "Going Standby",
        "No Policy Installed",
        "Policy Out Of Date",
    ]
)

#: Node state constant values
STATE = frozenset(
    ["INITIAL", "READY", "ERROR", "SERVER_ERROR", "NO_STATUS", "TIMEOUT", "DELETED", "DUMMY"]
)


[docs] class NodeWaiter(threading.Thread): """ Node Waiter provides a common threaded interface to monitoring a nodes status and wait for a specific response. """ def __init__(self, resource, status, timeout=5, max_wait=36, **kw): threading.Thread.__init__(self) self._desired_status = status self._resource = resource # node resource self._status = None self._max_wait = max_wait self._timeout = timeout self.callbacks = [] self._done = threading.Event() self.daemon = True self.start()
[docs] def run(self): while not self.finished(): time.sleep(self._timeout) try: self._status = self._get_status() self._max_wait -= 1 except Exception as e: self._status = e break self._done.set() for call in self.callbacks: call(self._status)
def _get_status(self): # Raises NodeCommandFailed # Modified in 0.6.2 to support SMC 6.5 where the attribute name changed status = self._resource.status() latest = [getattr(status, attr) for attr in self.value if getattr(status, attr)] if self._desired_status in latest: return self._desired_status else: return latest[0] or None def finished(self): return self._done.is_set() or self._status == self._desired_status or self._max_wait == 0
[docs] def add_done_callback(self, callback): """ Add a callback to run after the task completes. The callable must take 1 argument which will be the completed Task. :param callable callback """ if self._done.is_set(): raise ValueError("Thread has already terminated, cannot add callback.") if callable(callback): self.callbacks.append(callback)
[docs] def done(self): """ Is the task still running or considered complete :rtype: bool """ # isAlive() is removed since python3.9 return self._done.is_set() or \ (PYTHON_v3_9 and not self.is_alive()) or \ (not PYTHON_v3_9 and not self.isAlive())
[docs] def result(self, timeout=None): """ Get current status result after waiting timeout Result does a join on the thread to get a status update. It is possible the first couple of statuses are None if an update has not yet been joined. """ self.wait(timeout) return self._status
[docs] def wait(self, timeout=None): """ Blocking method to wait for thread """ self.join(timeout)
[docs] def stop(self): """ Stop thread if it's still running """ if (not PYTHON_v3_9 and self.isAlive()) or (PYTHON_v3_9 and self.is_alive()): self._done.set()
[docs] class ConfigurationStatusWaiter(NodeWaiter): """ Configuration status waiter provides a current engine status with respects to having a configuration. :param Node resource: Engine node to check for status :param str status: used defined status to wait for. :raises NodeCommandFailed: Failure to obtain a status back from the engine. This can be thrown when getting initial status. If thrown after the thread has started, it is caught and returned in the ``result`` after ending the thread. """ value = ("configuration_status",) def __init__(self, resource, status, **kw): if status not in CFG_STATUS: raise ValueError("Status is invalid. Valid options are: %s" % CFG_STATUS) super(ConfigurationStatusWaiter, self).__init__(resource, status, **kw)
[docs] class NodeStatusWaiter(NodeWaiter): """ Node Status specifies the current state of the engine such as offline, online, locked offline, no policy installed, etc. :param Node resource: Engine node to check for status :param str status: used defined status to wait for. :raises NodeCommandFailed: Failure to obtain a status back from the engine. This can be thrown when getting initial status. If thrown after the thread has started, it is caught and returned in the ``result`` after ending the thread. """ # Node status changed in SMC 6.5 from status to monitoring_status so # value was changed to an iterable to check for a status other than # None for both attributes value = ("status", "monitoring_status", "engine_node_status") def __init__(self, resource, status, **kw): if status not in STATUS: raise ValueError("Status is invalid. Valid options are: %s" % STATUS) super(NodeStatusWaiter, self).__init__(resource, status, **kw)
[docs] class NodeStateWaiter(NodeWaiter): """ Node State specifies where the engine is within it's lifecycle, such as initial state, ready state, error, timeout, etc. :param Node resource: Engine node to check for status :param str status: used defined status to wait for. :raises NodeCommandFailed: Failure to obtain a status back from the engine. This can be thrown when getting initial status. If thrown after the thread has started, it is caught and returned in the ``result`` after ending the thread. """ value = ("state", "monitoring_state") def __init__(self, resource, status, **kw): if status not in STATE: raise ValueError("Status is invalid. Valid options are: %s" % STATE) super(NodeStateWaiter, self).__init__(resource, status, **kw)