234 lines
8.5 KiB
Python
Raw Normal View History

# Copyright 2013 Rackspace, Inc.
#
# 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.
2013-12-17 15:13:30 -08:00
import functools
import threading
2014-01-02 13:17:15 -08:00
import uuid
2013-12-17 15:13:30 -08:00
import six
2013-12-17 15:13:30 -08:00
from ironic_python_agent import encoding
from ironic_python_agent import errors
from ironic_python_agent.openstack.common import log
from ironic_python_agent import utils
2013-12-17 15:13:30 -08:00
2014-01-02 13:17:15 -08:00
class AgentCommandStatus(object):
2014-03-12 09:50:32 -07:00
RUNNING = u'RUNNING'
SUCCEEDED = u'SUCCEEDED'
FAILED = u'FAILED'
2014-01-02 13:17:15 -08:00
class BaseCommandResult(encoding.Serializable):
def __init__(self, command_name, command_params):
self.id = six.text_type(uuid.uuid4())
2014-01-02 13:17:15 -08:00
self.command_name = command_name
self.command_params = command_params
self.command_status = AgentCommandStatus.RUNNING
self.command_error = None
self.command_result = None
def serialize(self):
2014-03-12 09:50:32 -07:00
return dict((
(u'id', self.id),
(u'command_name', self.command_name),
(u'command_params', self.command_params),
(u'command_status', self.command_status),
(u'command_error', self.command_error),
(u'command_result', self.command_result),
))
2014-01-02 13:17:15 -08:00
def is_done(self):
return self.command_status != AgentCommandStatus.RUNNING
2014-01-09 15:35:35 -08:00
def join(self):
return self
2014-01-02 13:17:15 -08:00
class SyncCommandResult(BaseCommandResult):
def __init__(self, command_name, command_params, success, result_or_error):
super(SyncCommandResult, self).__init__(command_name,
command_params)
if success:
self.command_status = AgentCommandStatus.SUCCEEDED
self.command_result = result_or_error
else:
self.command_status = AgentCommandStatus.FAILED
self.command_error = result_or_error
class AsyncCommandResult(BaseCommandResult):
"""A command that executes asynchronously in the background.
:param execute_method: a callable to be executed asynchronously
"""
def __init__(self, command_name, command_params, execute_method):
super(AsyncCommandResult, self).__init__(command_name, command_params)
self.execute_method = execute_method
self.command_state_lock = threading.Lock()
thread_name = 'agent-command-{0}'.format(self.id)
self.execution_thread = threading.Thread(target=self.run,
name=thread_name)
def serialize(self):
with self.command_state_lock:
return super(AsyncCommandResult, self).serialize()
def start(self):
self.execution_thread.start()
return self
def join(self, timeout=None):
self.execution_thread.join(timeout)
return self
def is_done(self):
with self.command_state_lock:
return super(AsyncCommandResult, self).is_done()
def run(self):
try:
result = self.execute_method(**self.command_params)
with self.command_state_lock:
self.command_result = result
self.command_status = AgentCommandStatus.SUCCEEDED
except Exception as e:
if not isinstance(e, errors.RESTError):
e = errors.CommandExecutionError(str(e))
with self.command_state_lock:
self.command_error = e
self.command_status = AgentCommandStatus.FAILED
class BaseAgentExtension(object):
def __init__(self):
super(BaseAgentExtension, self).__init__()
2014-03-17 15:17:27 -07:00
self.log = log.getLogger(__name__)
2014-01-21 10:42:43 -08:00
self.command_map = {}
def execute(self, command_name, **kwargs):
if command_name not in self.command_map:
raise errors.InvalidCommandError(
'Unknown command: {0}'.format(command_name))
2014-01-21 10:42:43 -08:00
return self.command_map[command_name](command_name, **kwargs)
def check_cmd_presence(self, ext_obj, ext, cmd):
if not (hasattr(ext_obj, 'execute') and hasattr(ext_obj, 'command_map')
and cmd in ext_obj.command_map):
raise errors.InvalidCommandParamsError(
"Extension {0} doesn't provide {1} method".format(ext, cmd))
class ExecuteCommandMixin(object):
def __init__(self):
self.command_lock = threading.Lock()
self.command_results = utils.get_ordereddict()
self.ext_mgr = self.get_extension_manager()
def get_extension_manager(self):
raise NotImplementedError(
'get_extension_manager should be implemented in successor class')
def split_command(self, command_name):
command_parts = command_name.split('.', 1)
if len(command_parts) != 2:
raise errors.InvalidCommandError(
'Command name must be of the form <extension>.<name>')
return (command_parts[0], command_parts[1])
def execute_command(self, command_name, **kwargs):
"""Execute an agent command."""
with self.command_lock:
extension_part, command_part = self.split_command(command_name)
if len(self.command_results) > 0:
last_command = list(self.command_results.values())[-1]
if not last_command.is_done():
raise errors.CommandExecutionError('agent is busy')
try:
ext = self.ext_mgr[extension_part].obj
result = ext.execute(command_part, **kwargs)
except KeyError:
# Extension Not found
raise errors.RequestedObjectNotFoundError('Extension',
extension_part)
except errors.InvalidContentError as e:
# Any command may raise a InvalidContentError which will be
# returned to the caller directly.
raise e
except Exception as e:
# Other errors are considered command execution errors, and are
# recorded as an
result = SyncCommandResult(command_name,
kwargs,
False,
six.text_type(e))
self.command_results[result.id] = result
return result
def async_command(validator=None):
"""Will run the command in an AsyncCommandResult in its own thread.
command_name is set based on the func name and command_params will
be whatever args/kwargs you pass into the decorated command.
"""
def async_decorator(func):
@functools.wraps(func)
def wrapper(self, command_name, **command_params):
# Run a validator before passing everything off to async.
# validators should raise exceptions or return silently.
if validator:
validator(self, **command_params)
# bind self to func so that AsyncCommandResult doesn't need to
# know about the mode
bound_func = functools.partial(func, self)
return AsyncCommandResult(command_name,
command_params,
bound_func).start()
return wrapper
return async_decorator
def sync_command(validator=None):
"""Decorate a method in order to wrap up its return value in a
SyncCommandResult. For consistency with @async_command() can also accept a
validator which will be used to validate input, although a synchronous
command can also choose to implement validation inline.
"""
def sync_decorator(func):
@functools.wraps(func)
def wrapper(self, command_name, **command_params):
# Run a validator before passing everything off to async.
# validators should raise exceptions or return silently.
if validator:
validator(self, **command_params)
result = func(self, **command_params)
return SyncCommandResult(command_name,
command_params,
True,
result)
return wrapper
return sync_decorator