# 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)