1 总入口
##################################
源码文件:st2/st2actions/bin/st2actionrunner
import sys
from st2actions.cmd import actionrunner
if __name__ == '__main__':
sys.exit(actionrunner.main())
2 调用
#########################################
源码文件:st2/st2actions/st2actions/cmd/actionrunner.py
from st2common.util.monkey_patch import monkey_patch
monkey_patch()
import os
import signal
import sys
from st2actions import config
from st2actions import scheduler, worker
from st2common import log as logging
from st2common.service_setup import setup as common_setup
from st2common.service_setup import teardown as common_teardown
__all__ = [
'main'
]
LOG = logging.getLogger(__name__)
def _setup_sigterm_handler():
def sigterm_handler(signum=None, frame=None):
# This will cause SystemExit to be throw and allow for component cleanup.
sys.exit(0)
# Register a SIGTERM signal handler which calls sys.exit which causes SystemExit to
# be thrown. We catch SystemExit and handle cleanup there.
signal.signal(signal.SIGTERM, sigterm_handler)
def _setup():
common_setup(service='actionrunner', config=config, setup_db=True, register_mq_exchanges=True,
register_signal_handlers=True)
_setup_sigterm_handler()
def _run_worker():
LOG.info('(PID=%s) Worker started.', os.getpid())
components = [
scheduler.get_scheduler(),
worker.get_worker()
]
try:
for component in components:
component.start()
for component in components:
component.wait()
except (KeyboardInterrupt, SystemExit):
LOG.info('(PID=%s) Worker stopped.', os.getpid())
errors = False
for component in components:
try:
component.shutdown()
except:
LOG.exception('Unable to shutdown %s.', component.__class__.__name__)
errors = True
if errors:
return 1
except:
LOG.exception('(PID=%s) Worker unexpectedly stopped.', os.getpid())
return 1
return 0
def _teardown():
common_teardown()
def main():
try:
_setup()
return _run_worker()
except SystemExit as exit_code:
sys.exit(exit_code)
except:
LOG.exception('(PID=%s) Worker quit due to exception.', os.getpid())
return 1
finally:
_teardown()
3 调用
#########################################
源码文件:st2/st2common/st2common/service_setup.py
from __future__ import absolute_import
import os
import traceback
from oslo_config import cfg
from st2common import log as logging
from st2common.constants.logging import DEFAULT_LOGGING_CONF_PATH
from st2common.transport.bootstrap_utils import register_exchanges_with_retry
from st2common.signal_handlers import register_common_signal_handlers
from st2common.util.debugging import enable_debugging
from st2common.models.utils.profiling import enable_profiling
from st2common import triggers
from st2common.rbac.migrations import run_all as run_all_rbac_migrations
# Note: This is here for backward compatibility.
# Function has been moved in a standalone module to avoid expensive in-direct
# import costs
from st2common.database_setup import db_setup
from st2common.database_setup import db_teardown
__all__ = [
'setup',
'teardown',
'db_setup',
'db_teardown'
]
LOG = logging.getLogger(__name__)
def setup(service, config, setup_db=True, register_mq_exchanges=True,
register_signal_handlers=True, register_internal_trigger_types=False,
run_migrations=True, config_args=None):
"""
Common setup function.
Currently it performs the following operations:
1. Parses config and CLI arguments
2. Establishes DB connection
3. Set log level for all the loggers to DEBUG if --debug flag is present or
if system.debug config option is set to True.
4. Registers RabbitMQ exchanges
5. Registers common signal handlers
6. Register internal trigger types
:param service: Name of the service.
:param config: Config object to use to parse args.
"""
# Set up logger which logs everything which happens during and before config
# parsing to sys.stdout
logging.setup(DEFAULT_LOGGING_CONF_PATH, excludes=None)
# Parse args to setup config.
if config_args:
config.parse_args(config_args)
else:
config.parse_args()
config_file_paths = cfg.CONF.config_file
config_file_paths = [os.path.abspath(path) for path in config_file_paths]
LOG.debug('Using config files: %s', ','.join(config_file_paths))
# Setup logging.
logging_config_path = config.get_logging_config_path()
logging_config_path = os.path.abspath(logging_config_path)
LOG.debug('Using logging config: %s', logging_config_path)
try:
logging.setup(logging_config_path, redirect_stderr=cfg.CONF.log.redirect_stderr,
excludes=cfg.CONF.log.excludes)
except KeyError as e:
tb_msg = traceback.format_exc()
if 'log.setLevel' in tb_msg:
msg = 'Invalid log level selected. Log level names need to be all uppercase.'
msg += '\n\n' + getattr(e, 'message', str(e))
raise KeyError(msg)
else:
raise e
if cfg.CONF.debug or cfg.CONF.system.debug:
enable_debugging()
if cfg.CONF.profile:
enable_profiling()
# All other setup which requires config to be parsed and logging to
# be correctly setup.
if setup_db:
db_setup()
if register_mq_exchanges:
register_exchanges_with_retry()
if register_signal_handlers:
register_common_signal_handlers()
if register_internal_trigger_types:
triggers.register_internal_trigger_types()
# TODO: This is a "not so nice" workaround until we have a proper migration system in place
if run_migrations:
run_all_rbac_migrations()
def teardown():
"""
Common teardown function.
"""
db_teardown()
4 调用
######################################
源码文件:st2/st2common/st2common/database_setup.py
from oslo_config import cfg
from st2common.models import db
from st2common.persistence import db_init
__all__ = [
'db_config',
'db_setup',
'db_teardown'
]
def db_config():
username = getattr(cfg.CONF.database, 'username', None)
password = getattr(cfg.CONF.database, 'password', None)
return {'db_name': cfg.CONF.database.db_name,
'db_host': cfg.CONF.database.host,
'db_port': cfg.CONF.database.port,
'username': username,
'password': password,
'ssl': cfg.CONF.database.ssl,
'ssl_keyfile': cfg.CONF.database.ssl_keyfile,
'ssl_certfile': cfg.CONF.database.ssl_certfile,
'ssl_cert_reqs': cfg.CONF.database.ssl_cert_reqs,
'ssl_ca_certs': cfg.CONF.database.ssl_ca_certs,
'ssl_match_hostname': cfg.CONF.database.ssl_match_hostname}
def db_setup(ensure_indexes=True):
"""
Creates the database and indexes (optional).
"""
db_cfg = db_config()
db_cfg['ensure_indexes'] = ensure_indexes
connection = db_init.db_setup_with_retry(**db_cfg)
return connection
def db_teardown():
"""
Disconnects from the database.
"""
return db.db_teardown()
5 调用
#########################################
源码文件:st2/st2actions/st2actions/scheduler.py
from kombu import Connection
from st2common import log as logging
from st2common.constants import action as action_constants
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.models.db.liveaction import LiveActionDB
from st2common.services import action as action_service
from st2common.persistence.liveaction import LiveAction
from st2common.persistence.policy import Policy
from st2common import policies
from st2common.transport import consumers
from st2common.transport import utils as transport_utils
from st2common.util import action_db as action_utils
from st2common.transport.queues import ACTIONSCHEDULER_REQUEST_QUEUE
__all__ = [
'ActionExecutionScheduler',
'get_scheduler'
]
LOG = logging.getLogger(__name__)
class ActionExecutionScheduler(consumers.MessageHandler):
message_type = LiveActionDB
def process(self, request):
"""Schedules the LiveAction and publishes the request
to the appropriate action runner(s).
LiveAction in statuses other than "requested" are ignored.
:param request: Action execution request.
:type request: ``st2common.models.db.liveaction.LiveActionDB``
"""
if request.status != action_constants.LIVEACTION_STATUS_REQUESTED:
LOG.info('%s is ignoring %s (id=%s) with "%s" status.',
self.__class__.__name__, type(request), request.id, request.status)
return
try:
liveaction_db = action_utils.get_liveaction_by_id(request.id)
except StackStormDBObjectNotFoundError:
LOG.exception('Failed to find liveaction %s in the database.', request.id)
raise
# Apply policies defined for the action.
liveaction_db = self._apply_pre_run_policies(liveaction_db=liveaction_db)
# Exit if the status of the request is no longer runnable.
# The status could have be changed by one of the policies.
if liveaction_db.status not in [action_constants.LIVEACTION_STATUS_REQUESTED,
action_constants.LIVEACTION_STATUS_SCHEDULED]:
LOG.info('%s is ignoring %s (id=%s) with "%s" status after policies are applied.',
self.__class__.__name__, type(request), request.id, liveaction_db.status)
return
# Update liveaction status to "scheduled".
if liveaction_db.status == action_constants.LIVEACTION_STATUS_REQUESTED:
liveaction_db = action_service.update_status(
liveaction_db, action_constants.LIVEACTION_STATUS_SCHEDULED, publish=False)
# Publish the "scheduled" status here manually. Otherwise, there could be a
# race condition with the update of the action_execution_db if the execution
# of the liveaction completes first.
LiveAction.publish_status(liveaction_db)
def _apply_pre_run_policies(self, liveaction_db):
# Apply policies defined for the action.
policy_dbs = Policy.query(resource_ref=liveaction_db.action, enabled=True)
LOG.debug('Applying %s pre_run policies' % (len(policy_dbs)))
for policy_db in policy_dbs:
driver = policies.get_driver(policy_db.ref,
policy_db.policy_type,
**policy_db.parameters)
try:
LOG.debug('Applying pre_run policy "%s" (%s) for liveaction %s' %
(policy_db.ref, policy_db.policy_type, str(liveaction_db.id)))
liveaction_db = driver.apply_before(liveaction_db)
except:
LOG.exception('An exception occurred while applying policy "%s".', policy_db.ref)
if liveaction_db.status == action_constants.LIVEACTION_STATUS_DELAYED:
break
return liveaction_db
def get_scheduler():
with Connection(transport_utils.get_messaging_urls()) as conn:
return ActionExecutionScheduler(conn, [ACTIONSCHEDULER_REQUEST_QUEUE])
6 调用
#######################################
源码文件:st2/st2actions/st2actions/worker.py
import sys
import traceback
from kombu import Connection
from st2actions.container.base import RunnerContainer
from st2common import log as logging
from st2common.constants import action as action_constants
from st2common.exceptions.actionrunner import ActionRunnerException
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.models.db.liveaction import LiveActionDB
from st2common.persistence.execution import ActionExecution
from st2common.services import executions
from st2common.transport.consumers import MessageHandler
from st2common.transport.consumers import ActionsQueueConsumer
from st2common.transport import utils as transport_utils
from st2common.util import action_db as action_utils
from st2common.util import system_info
from st2common.transport import queues
__all__ = [
'ActionExecutionDispatcher',
'get_worker'
]
LOG = logging.getLogger(__name__)
ACTIONRUNNER_QUEUES = [
queues.ACTIONRUNNER_WORK_QUEUE,
queues.ACTIONRUNNER_CANCEL_QUEUE,
queues.ACTIONRUNNER_PAUSE_QUEUE,
queues.ACTIONRUNNER_RESUME_QUEUE
]
ACTIONRUNNER_DISPATCHABLE_STATES = [
action_constants.LIVEACTION_STATUS_SCHEDULED,
action_constants.LIVEACTION_STATUS_CANCELING,
action_constants.LIVEACTION_STATUS_PAUSING,
action_constants.LIVEACTION_STATUS_RESUMING
]
class ActionExecutionDispatcher(MessageHandler):
message_type = LiveActionDB
def __init__(self, connection, queues):
super(ActionExecutionDispatcher, self).__init__(connection, queues)
self.container = RunnerContainer()
self._running_liveactions = set()
def get_queue_consumer(self, connection, queues):
# We want to use a special ActionsQueueConsumer which uses 2 dispatcher pools
return ActionsQueueConsumer(connection=connection, queues=queues, handler=self)
def process(self, liveaction):
"""Dispatches the LiveAction to appropriate action runner.
LiveAction in statuses other than "scheduled" and "canceling" are ignored. If
LiveAction is already canceled and result is empty, the LiveAction
is updated with a generic exception message.
:param liveaction: Action execution request.
:type liveaction: ``st2common.models.db.liveaction.LiveActionDB``
:rtype: ``dict``
"""
if liveaction.status == action_constants.LIVEACTION_STATUS_CANCELED:
LOG.info('%s is not executing %s (id=%s) with "%s" status.',
self.__class__.__name__, type(liveaction), liveaction.id, liveaction.status)
if not liveaction.result:
updated_liveaction = action_utils.update_liveaction_status(
status=liveaction.status,
result={'message': 'Action execution canceled by user.'},
liveaction_id=liveaction.id)
executions.update_execution(updated_liveaction)
return
if liveaction.status not in ACTIONRUNNER_DISPATCHABLE_STATES:
LOG.info('%s is not dispatching %s (id=%s) with "%s" status.',
self.__class__.__name__, type(liveaction), liveaction.id, liveaction.status)
return
try:
liveaction_db = action_utils.get_liveaction_by_id(liveaction.id)
except StackStormDBObjectNotFoundError:
LOG.exception('Failed to find liveaction %s in the database.', liveaction.id)
raise
if liveaction.status != liveaction_db.status:
LOG.warning(
'The status of liveaction %s has changed from %s to %s '
'while in the queue waiting for processing.',
liveaction.id,
liveaction.status,
liveaction_db.status
)
dispatchers = {
action_constants.LIVEACTION_STATUS_SCHEDULED: self._run_action,
action_constants.LIVEACTION_STATUS_CANCELING: self._cancel_action,
action_constants.LIVEACTION_STATUS_PAUSING: self._pause_action,
action_constants.LIVEACTION_STATUS_RESUMING: self._resume_action
}
return dispatchers[liveaction.status](liveaction)
def shutdown(self):
super(ActionExecutionDispatcher, self).shutdown()
# Abandon running executions if incomplete
while self._running_liveactions:
liveaction_id = self._running_liveactions.pop()
try:
executions.abandon_execution_if_incomplete(liveaction_id=liveaction_id)
except:
LOG.exception('Failed to abandon liveaction %s.', liveaction_id)
def _run_action(self, liveaction_db):
# stamp liveaction with process_info
runner_info = system_info.get_process_info()
# Update liveaction status to "running"
liveaction_db = action_utils.update_liveaction_status(
status=action_constants.LIVEACTION_STATUS_RUNNING,
runner_info=runner_info,
liveaction_id=liveaction_db.id)
self._running_liveactions.add(liveaction_db.id)
action_execution_db = executions.update_execution(liveaction_db)
# Launch action
extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db}
LOG.audit('Launching action execution.', extra=extra)
# the extra field will not be shown in non-audit logs so temporarily log at info.
LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.',
action_execution_db.id, liveaction_db.id, liveaction_db.status)
extra = {'liveaction_db': liveaction_db}
try:
result = self.container.dispatch(liveaction_db)
LOG.debug('Runner dispatch produced result: %s', result)
if not result:
raise ActionRunnerException('Failed to execute action.')
except:
_, ex, tb = sys.exc_info()
extra['error'] = str(ex)
LOG.info('Action "%s" failed: %s' % (liveaction_db.action, str(ex)), extra=extra)
liveaction_db = action_utils.update_liveaction_status(
status=action_constants.LIVEACTION_STATUS_FAILED,
liveaction_id=liveaction_db.id,
result={'error': str(ex), 'traceback': ''.join(traceback.format_tb(tb, 20))})
executions.update_execution(liveaction_db)
raise
finally:
# In the case of worker shutdown, the items are removed from _running_liveactions.
# As the subprocesses for action executions are terminated, this finally block
# will be executed. Set remove will result in KeyError if item no longer exists.
# Use set discard to not raise the KeyError.
self._running_liveactions.discard(liveaction_db.id)
return result
def _cancel_action(self, liveaction_db):
action_execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id))
extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db}
LOG.audit('Canceling action execution.', extra=extra)
# the extra field will not be shown in non-audit logs so temporarily log at info.
LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.',
action_execution_db.id, liveaction_db.id, liveaction_db.status)
try:
result = self.container.dispatch(liveaction_db)
LOG.debug('Runner dispatch produced result: %s', result)
except:
_, ex, tb = sys.exc_info()
extra['error'] = str(ex)
LOG.info('Failed to cancel action execution %s.' % (liveaction_db.id), extra=extra)
raise
return result
def _pause_action(self, liveaction_db):
action_execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id))
extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db}
LOG.audit('Pausing action execution.', extra=extra)
# the extra field will not be shown in non-audit logs so temporarily log at info.
LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.',
action_execution_db.id, liveaction_db.id, liveaction_db.status)
try:
result = self.container.dispatch(liveaction_db)
LOG.debug('Runner dispatch produced result: %s', result)
except:
_, ex, tb = sys.exc_info()
extra['error'] = str(ex)
LOG.info('Failed to pause action execution %s.' % (liveaction_db.id), extra=extra)
raise
return result
def _resume_action(self, liveaction_db):
action_execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id))
extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db}
LOG.audit('Resuming action execution.', extra=extra)
# the extra field will not be shown in non-audit logs so temporarily log at info.
LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.',
action_execution_db.id, liveaction_db.id, liveaction_db.status)
try:
result = self.container.dispatch(liveaction_db)
LOG.debug('Runner dispatch produced result: %s', result)
except:
_, ex, tb = sys.exc_info()
extra['error'] = str(ex)
LOG.info('Failed to resume action execution %s.' % (liveaction_db.id), extra=extra)
raise
return result
def get_worker():
with Connection(transport_utils.get_messaging_urls()) as conn:
return ActionExecutionDispatcher(conn, ACTIONRUNNER_QUEUES)
7 调用
#########################################
源码文件:st2/st2common/st2common/transport/consumers.py
import abc
import eventlet
import six
from kombu.mixins import ConsumerMixin
from oslo_config import cfg
from st2common import log as logging
from st2common.util.greenpooldispatch import BufferedDispatcher
__all__ = [
'QueueConsumer',
'StagedQueueConsumer',
'ActionsQueueConsumer',
'MessageHandler',
'StagedMessageHandler'
]
LOG = logging.getLogger(__name__)
class QueueConsumer(ConsumerMixin):
def __init__(self, connection, queues, handler):
self.connection = connection
self._dispatcher = BufferedDispatcher()
self._queues = queues
self._handler = handler
def shutdown(self):
self._dispatcher.shutdown()
def get_consumers(self, Consumer, channel):
consumer = Consumer(queues=self._queues, accept=['pickle'], callbacks=[self.process])
# use prefetch_count=1 for fair dispatch. This way workers that finish an item get the next
# task and the work does not get queued behind any single large item.
consumer.qos(prefetch_count=1)
return [consumer]
def process(self, body, message):
try:
if not isinstance(body, self._handler.message_type):
raise TypeError('Received an unexpected type "%s" for payload.' % type(body))
self._dispatcher.dispatch(self._process_message, body)
except:
LOG.exception('%s failed to process message: %s', self.__class__.__name__, body)
finally:
# At this point we will always ack a message.
message.ack()
def _process_message(self, body):
try:
self._handler.process(body)
except:
LOG.exception('%s failed to process message: %s', self.__class__.__name__, body)
class StagedQueueConsumer(QueueConsumer):
"""
Used by ``StagedMessageHandler`` to effectively manage it 2 step message handling.
"""
def process(self, body, message):
try:
if not isinstance(body, self._handler.message_type):
raise TypeError('Received an unexpected type "%s" for payload.' % type(body))
response = self._handler.pre_ack_process(body)
self._dispatcher.dispatch(self._process_message, response)
except:
LOG.exception('%s failed to process message: %s', self.__class__.__name__, body)
finally:
# At this point we will always ack a message.
message.ack()
class ActionsQueueConsumer(QueueConsumer):
"""
Special Queue Consumer for action runner which uses multiple BufferedDispatcher pools:
1. For regular (non-workflow) actions
2. One for workflow actions
This way we can ensure workflow actions never block non-workflow actions.
"""
def __init__(self, connection, queues, handler):
self.connection = connection
self._queues = queues
self._handler = handler
workflows_pool_size = cfg.CONF.actionrunner.workflows_pool_size
actions_pool_size = cfg.CONF.actionrunner.actions_pool_size
self._workflows_dispatcher = BufferedDispatcher(dispatch_pool_size=workflows_pool_size,
name='workflows-dispatcher')
self._actions_dispatcher = BufferedDispatcher(dispatch_pool_size=actions_pool_size,
name='actions-dispatcher')
def process(self, body, message):
try:
if not isinstance(body, self._handler.message_type):
raise TypeError('Received an unexpected type "%s" for payload.' % type(body))
action_is_workflow = getattr(body, 'action_is_workflow', False)
if action_is_workflow:
# Use workflow dispatcher queue
dispatcher = self._workflows_dispatcher
else:
# Use queue for regular or workflow actions
dispatcher = self._actions_dispatcher
LOG.debug('Using BufferedDispatcher pool: "%s"', str(dispatcher))
dispatcher.dispatch(self._process_message, body)
except:
LOG.exception('%s failed to process message: %s', self.__class__.__name__, body)
finally:
# At this point we will always ack a message.
message.ack()
def shutdown(self):
self._workflows_dispatcher.shutdown()
self._actions_dispatcher.shutdown()
@six.add_metaclass(abc.ABCMeta)
class MessageHandler(object):
message_type = None
def __init__(self, connection, queues):
self._queue_consumer = self.get_queue_consumer(connection=connection,
queues=queues)
self._consumer_thread = None
def start(self, wait=False):
LOG.info('Starting %s...', self.__class__.__name__)
self._consumer_thread = eventlet.spawn(self._queue_consumer.run)
if wait:
self.wait()
def wait(self):
self._consumer_thread.wait()
def shutdown(self):
LOG.info('Shutting down %s...', self.__class__.__name__)
self._queue_consumer.shutdown()
@abc.abstractmethod
def process(self, message):
pass
def get_queue_consumer(self, connection, queues):
return QueueConsumer(connection=connection, queues=queues, handler=self)
@six.add_metaclass(abc.ABCMeta)
class StagedMessageHandler(MessageHandler):
"""
MessageHandler to deal with messages in 2 steps.
1. pre_ack_process : This is called on the handler before ack-ing the message.
2. process: Called after ack-in the messages
This 2 step approach provides a way for the handler to do some hadling like saving to DB etc
before acknowleding and then performing future processing async. This way even if the handler
or owning process is taken down system will still maintain track of the message.
"""
@abc.abstractmethod
def pre_ack_process(self, message):
"""
Called before acknowleding a message. Good place to track the message via a DB entry or some
other applicable mechnism.
The reponse of this method is passed into the ``process`` method. This was whatever is the
processed version of the message can be moved forward. It is always possible to simply
return ``message`` and have ``process`` handle the original message.
"""
pass
def get_queue_consumer(self, connection, queues):
return StagedQueueConsumer(connection=connection, queues=queues, handler=self)
8 调用
#####################################
源码文件:st2/st2actions/st2actions/container/base.py
import json
import sys
import traceback
from oslo_config import cfg
from st2common import log as logging
from st2common.util import date as date_utils
from st2common.constants import action as action_constants
from st2common.content import utils as content_utils
from st2common.exceptions import actionrunner
from st2common.exceptions.param import ParamException
from st2common.models.db.executionstate import ActionExecutionStateDB
from st2common.models.system.action import ResolvedActionParameters
from st2common.persistence.execution import ActionExecution
from st2common.persistence.executionstate import ActionExecutionState
from st2common.services import access, executions
from st2common.util.action_db import (get_action_by_ref, get_runnertype_by_name)
from st2common.util.action_db import (update_liveaction_status, get_liveaction_by_id)
from st2common.util import param as param_utils
from st2common.util.config_loader import ContentPackConfigLoader
from st2common.runners.base import get_runner
from st2common.runners.base import AsyncActionRunner
LOG = logging.getLogger(__name__)
__all__ = [
'RunnerContainer',
'get_runner_container'
]
class RunnerContainer(object):
def dispatch(self, liveaction_db):
action_db = get_action_by_ref(liveaction_db.action)
if not action_db:
raise Exception('Action %s not found in DB.' % (liveaction_db.action))
liveaction_db.context['pack'] = action_db.pack
runnertype_db = get_runnertype_by_name(action_db.runner_type['name'])
extra = {'liveaction_db': liveaction_db, 'runnertype_db': runnertype_db}
LOG.info('Dispatching Action to a runner', extra=extra)
# Get runner instance.
runner = self._get_runner(runnertype_db, action_db, liveaction_db)
LOG.debug('Runner instance for RunnerType "%s" is: %s', runnertype_db.name, runner)
# Process the request.
funcs = {
action_constants.LIVEACTION_STATUS_REQUESTED: self._do_run,
action_constants.LIVEACTION_STATUS_SCHEDULED: self._do_run,
action_constants.LIVEACTION_STATUS_RUNNING: self._do_run,
action_constants.LIVEACTION_STATUS_CANCELING: self._do_cancel,
action_constants.LIVEACTION_STATUS_PAUSING: self._do_pause,
action_constants.LIVEACTION_STATUS_RESUMING: self._do_resume
}
if liveaction_db.status not in funcs:
raise actionrunner.ActionRunnerDispatchError(
'Action runner is unable to dispatch the liveaction because it is '
'in an unsupported status of "%s".' % liveaction_db.status
)
liveaction_db = funcs[liveaction_db.status](
runner=runner,
runnertype_db=runnertype_db,
action_db=action_db,
liveaction_db=liveaction_db
)
return liveaction_db.result
def _do_run(self, runner, runnertype_db, action_db, liveaction_db):
# Create a temporary auth token which will be available
# for the duration of the action execution.
runner.auth_token = self._create_auth_token(context=runner.context, action_db=action_db,
liveaction_db=liveaction_db)
try:
# Finalized parameters are resolved and then rendered. This process could
# fail. Handle the exception and report the error correctly.
try:
runner_params, action_params = param_utils.render_final_params(
runnertype_db.runner_parameters, action_db.parameters, liveaction_db.parameters,
liveaction_db.context)
runner.runner_parameters = runner_params
except ParamException as e:
raise actionrunner.ActionRunnerException(str(e))
LOG.debug('Performing pre-run for runner: %s', runner.runner_id)
runner.pre_run()
# Mask secret parameters in the log context
resolved_action_params = ResolvedActionParameters(action_db=action_db,
runner_type_db=runnertype_db,
runner_parameters=runner_params,
action_parameters=action_params)
extra = {'runner': runner, 'parameters': resolved_action_params}
LOG.debug('Performing run for runner: %s' % (runner.runner_id), extra=extra)
(status, result, context) = runner.run(action_params)
try:
result = json.loads(result)
except:
pass
action_completed = status in action_constants.LIVEACTION_COMPLETED_STATES
if isinstance(runner, AsyncActionRunner) and not action_completed:
self._setup_async_query(liveaction_db.id, runnertype_db, context)
except:
LOG.exception('Failed to run action.')
_, ex, tb = sys.exc_info()
# mark execution as failed.
status = action_constants.LIVEACTION_STATUS_FAILED
# include the error message and traceback to try and provide some hints.
result = {'error': str(ex), 'traceback': ''.join(traceback.format_tb(tb, 20))}
context = None
finally:
# Log action completion
extra = {'result': result, 'status': status}
LOG.debug('Action "%s" completed.' % (action_db.name), extra=extra)
# Update the final status of liveaction and corresponding action execution.
liveaction_db = self._update_status(liveaction_db.id, status, result, context)
# Always clean-up the auth_token
# This method should be called in the finally block to ensure post_run is not impacted.
self._clean_up_auth_token(runner=runner, status=status)
LOG.debug('Performing post_run for runner: %s', runner.runner_id)
runner.post_run(status=status, result=result)
LOG.debug('Runner do_run result', extra={'result': liveaction_db.result})
LOG.audit('Liveaction completed', extra={'liveaction_db': liveaction_db})
return liveaction_db
def _do_cancel(self, runner, runnertype_db, action_db, liveaction_db):
try:
extra = {'runner': runner}
LOG.debug('Performing cancel for runner: %s', (runner.runner_id), extra=extra)
(status, result, context) = runner.cancel()
# Update the final status of liveaction and corresponding action execution.
# The status is updated here because we want to keep the workflow running
# as is if the cancel operation failed.
liveaction_db = self._update_status(liveaction_db.id, status, result, context)
except:
_, ex, tb = sys.exc_info()
# include the error message and traceback to try and provide some hints.
result = {'error': str(ex), 'traceback': ''.join(traceback.format_tb(tb, 20))}
LOG.exception('Failed to cancel action %s.' % (liveaction_db.id), extra=result)
finally:
# Always clean-up the auth_token
# This method should be called in the finally block to ensure post_run is not impacted.
self._clean_up_auth_token(runner=runner, status=liveaction_db.status)
LOG.debug('Performing post_run for runner: %s', runner.runner_id)
result = {'error': 'Execution canceled by user.'}
runner.post_run(status=liveaction_db.status, result=result)
return liveaction_db
def _do_pause(self, runner, runnertype_db, action_db, liveaction_db):
try:
extra = {'runner': runner}
LOG.debug('Performing pause for runner: %s', (runner.runner_id), extra=extra)
(status, result, context) = runner.pause()
except:
_, ex, tb = sys.exc_info()
# include the error message and traceback to try and provide some hints.
status = action_constants.LIVEACTION_STATUS_FAILED
result = {'error': str(ex), 'traceback': ''.join(traceback.format_tb(tb, 20))}
context = liveaction_db.context
LOG.exception('Failed to pause action %s.' % (liveaction_db.id), extra=result)
finally:
# Update the final status of liveaction and corresponding action execution.
liveaction_db = self._update_status(liveaction_db.id, status, result, context)
# Always clean-up the auth_token
self._clean_up_auth_token(runner=runner, status=liveaction_db.status)
return liveaction_db
def _do_resume(self, runner, runnertype_db, action_db, liveaction_db):
try:
extra = {'runner': runner}
LOG.debug('Performing resume for runner: %s', (runner.runner_id), extra=extra)
(status, result, context) = runner.resume()
try:
result = json.loads(result)
except:
pass
action_completed = status in action_constants.LIVEACTION_COMPLETED_STATES
if isinstance(runner, AsyncActionRunner) and not action_completed:
self._setup_async_query(liveaction_db.id, runnertype_db, context)
except:
_, ex, tb = sys.exc_info()
# include the error message and traceback to try and provide some hints.
status = action_constants.LIVEACTION_STATUS_FAILED
result = {'error': str(ex), 'traceback': ''.join(traceback.format_tb(tb, 20))}
context = liveaction_db.context
LOG.exception('Failed to resume action %s.' % (liveaction_db.id), extra=result)
finally:
# Update the final status of liveaction and corresponding action execution.
liveaction_db = self._update_status(liveaction_db.id, status, result, context)
# Always clean-up the auth_token
# This method should be called in the finally block to ensure post_run is not impacted.
self._clean_up_auth_token(runner=runner, status=liveaction_db.status)
LOG.debug('Performing post_run for runner: %s', runner.runner_id)
runner.post_run(status=status, result=result)
LOG.debug('Runner do_run result', extra={'result': liveaction_db.result})
LOG.audit('Liveaction completed', extra={'liveaction_db': liveaction_db})
return liveaction_db
def _clean_up_auth_token(self, runner, status):
"""
Clean up the temporary auth token for the current action.
Note: This method should never throw since it's called inside finally block which assumes
it doesn't throw.
"""
# Deletion of the runner generated auth token is delayed until the token expires.
# Async actions such as Mistral workflows uses the auth token to launch other
# actions in the workflow. If the auth token is deleted here, then the actions
# in the workflow will fail with unauthorized exception.
is_async_runner = isinstance(runner, AsyncActionRunner)
action_completed = status in action_constants.LIVEACTION_COMPLETED_STATES
if not is_async_runner or (is_async_runner and action_completed):
try:
self._delete_auth_token(runner.auth_token)
except:
LOG.exception('Unable to clean-up auth_token.')
return True
return False
def _update_live_action_db(self, liveaction_id, status, result, context):
"""
Update LiveActionDB object for the provided liveaction id.
"""
liveaction_db = get_liveaction_by_id(liveaction_id)
state_changed = (liveaction_db.status != status)
if status in action_constants.LIVEACTION_COMPLETED_STATES:
end_timestamp = date_utils.get_datetime_utc_now()
else:
end_timestamp = None
liveaction_db = update_liveaction_status(status=status,
result=result,
context=context,
end_timestamp=end_timestamp,
liveaction_db=liveaction_db)
return (liveaction_db, state_changed)
def _update_status(self, liveaction_id, status, result, context):
try:
LOG.debug('Setting status: %s for liveaction: %s', status, liveaction_id)
liveaction_db, state_changed = self._update_live_action_db(
liveaction_id, status, result, context)
except Exception as e:
LOG.exception(
'Cannot update liveaction '
'(id: %s, status: %s, result: %s).' % (
liveaction_id, status, result)
)
raise e
try:
executions.update_execution(liveaction_db, publish=state_changed)
extra = {'liveaction_db': liveaction_db}
LOG.debug('Updated liveaction after run', extra=extra)
except Exception as e:
LOG.exception(
'Cannot update action execution for liveaction '
'(id: %s, status: %s, result: %s).' % (
liveaction_id, status, result)
)
raise e
return liveaction_db
def _get_entry_point_abs_path(self, pack, entry_point):
return content_utils.get_entry_point_abs_path(pack=pack, entry_point=entry_point)
def _get_action_libs_abs_path(self, pack, entry_point):
return content_utils.get_action_libs_abs_path(pack=pack, entry_point=entry_point)
def _get_rerun_reference(self, context):
execution_id = context.get('re-run', {}).get('ref')
return ActionExecution.get_by_id(execution_id) if execution_id else None
def _get_runner(self, runnertype_db, action_db, liveaction_db):
resolved_entry_point = self._get_entry_point_abs_path(action_db.pack,
action_db.entry_point)
context = getattr(liveaction_db, 'context', dict())
user = context.get('user', cfg.CONF.system_user.user)
# Note: Right now configs are only supported by the Python runner actions
if runnertype_db.runner_module == 'python_runner':
LOG.debug('Loading config for pack')
config_loader = ContentPackConfigLoader(pack_name=action_db.pack, user=user)
config = config_loader.get_config()
else:
config = None
runner = get_runner(module_name=runnertype_db.runner_module, config=config)
# TODO: Pass those arguments to the constructor instead of late
# assignment, late assignment is awful
runner.runner_type_db = runnertype_db
runner.action = action_db
runner.action_name = action_db.name
runner.liveaction = liveaction_db
runner.liveaction_id = str(liveaction_db.id)
runner.execution = ActionExecution.get(liveaction__id=runner.liveaction_id)
runner.execution_id = str(runner.execution.id)
runner.entry_point = resolved_entry_point
runner.context = context
runner.callback = getattr(liveaction_db, 'callback', dict())
runner.libs_dir_path = self._get_action_libs_abs_path(action_db.pack,
action_db.entry_point)
# For re-run, get the ActionExecutionDB in which the re-run is based on.
rerun_ref_id = runner.context.get('re-run', {}).get('ref')
runner.rerun_ex_ref = ActionExecution.get(id=rerun_ref_id) if rerun_ref_id else None
return runner
def _create_auth_token(self, context, action_db, liveaction_db):
if not context:
return None
user = context.get('user', None)
if not user:
return None
metadata = {
'service': 'actions_container',
'action_name': action_db.name,
'live_action_id': str(liveaction_db.id)
}
ttl = cfg.CONF.auth.service_token_ttl
token_db = access.create_token(username=user, ttl=ttl, metadata=metadata, service=True)
return token_db
def _delete_auth_token(self, auth_token):
if auth_token:
access.delete_token(auth_token.token)
def _setup_async_query(self, liveaction_id, runnertype_db, query_context):
query_module = getattr(runnertype_db, 'query_module', None)
if not query_module:
LOG.error('No query module specified for runner %s.', runnertype_db)
return
try:
self._create_execution_state(liveaction_id, runnertype_db, query_context)
except:
LOG.exception('Unable to create action execution state db model ' +
'for liveaction_id %s', liveaction_id)
def _create_execution_state(self, liveaction_id, runnertype_db, query_context):
state_db = ActionExecutionStateDB(
execution_id=liveaction_id,
query_module=runnertype_db.query_module,
query_context=query_context)
try:
return ActionExecutionState.add_or_update(state_db)
except:
LOG.exception('Unable to create execution state db for liveaction_id %s.'
% liveaction_id)
return None
def get_runner_container():
return RunnerContainer()
9 调用
########################################
源码文件:st2/st2common/st2common/util/action_db.py
try:
import simplejson as json
except ImportError:
import json
from collections import OrderedDict
from mongoengine import ValidationError
import six
from st2common import log as logging
from st2common.constants.action import LIVEACTION_STATUSES, LIVEACTION_STATUS_CANCELED
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.persistence.action import Action
from st2common.persistence.liveaction import LiveAction
from st2common.persistence.runner import RunnerType
LOG = logging.getLogger(__name__)
__all__ = [
'get_action_parameters_specs',
'get_runnertype_by_id',
'get_runnertype_by_name',
'get_action_by_id',
'get_action_by_ref',
'get_liveaction_by_id',
'update_liveaction_status',
'serialize_positional_argument',
'get_args'
]
def get_action_parameters_specs(action_ref):
"""
Retrieve parameters specifications schema for the provided action reference.
Note: This function returns a union of action and action runner parameters.
:param action_ref: Action reference.
:type action_ref: ``str``
:rtype: ``dict``
"""
action_db = get_action_by_ref(ref=action_ref)
parameters = {}
if not action_db:
return parameters
runner_type_name = action_db.runner_type['name']
runner_type_db = get_runnertype_by_name(runnertype_name=runner_type_name)
# Runner type parameters should be added first before the action parameters.
parameters.update(runner_type_db['runner_parameters'])
parameters.update(action_db.parameters)
return parameters
def get_runnertype_by_id(runnertype_id):
"""
Get RunnerType by id.
On error, raise StackStormDBObjectNotFoundError
"""
try:
runnertype = RunnerType.get_by_id(runnertype_id)
except (ValueError, ValidationError) as e:
LOG.warning('Database lookup for runnertype with id="%s" resulted in '
'exception: %s', runnertype_id, e)
raise StackStormDBObjectNotFoundError('Unable to find runnertype with '
'id="%s"' % runnertype_id)
return runnertype
def get_runnertype_by_name(runnertype_name):
"""
Get an runnertype by name.
On error, raise ST2ObjectNotFoundError.
"""
try:
runnertypes = RunnerType.query(name=runnertype_name)
except (ValueError, ValidationError) as e:
LOG.error('Database lookup for name="%s" resulted in exception: %s',
runnertype_name, e)
raise StackStormDBObjectNotFoundError('Unable to find runnertype with name="%s"'
% runnertype_name)
if not runnertypes:
raise StackStormDBObjectNotFoundError('Unable to find RunnerType with name="%s"'
% runnertype_name)
if len(runnertypes) > 1:
LOG.warning('More than one RunnerType returned from DB lookup by name. '
'Result list is: %s', runnertypes)
return runnertypes[0]
def get_action_by_id(action_id):
"""
Get Action by id.
On error, raise StackStormDBObjectNotFoundError
"""
action = None
try:
action = Action.get_by_id(action_id)
except (ValueError, ValidationError) as e:
LOG.warning('Database lookup for action with id="%s" resulted in '
'exception: %s', action_id, e)
raise StackStormDBObjectNotFoundError('Unable to find action with '
'id="%s"' % action_id)
return action
def get_action_by_ref(ref):
"""
Returns the action object from db given a string ref.
:param ref: Reference to the trigger type db object.
:type ref: ``str``
:rtype action: ``object``
"""
try:
return Action.get_by_ref(ref)
except ValueError as e:
LOG.debug('Database lookup for ref="%s" resulted ' +
'in exception : %s.', ref, e, exc_info=True)
return None
def get_liveaction_by_id(liveaction_id):
"""
Get LiveAction by id.
On error, raise ST2DBObjectNotFoundError.
"""
liveaction = None
try:
liveaction = LiveAction.get_by_id(liveaction_id)
except (ValidationError, ValueError) as e:
LOG.error('Database lookup for LiveAction with id="%s" resulted in '
'exception: %s', liveaction_id, e)
raise StackStormDBObjectNotFoundError('Unable to find LiveAction with '
'id="%s"' % liveaction_id)
return liveaction
def update_liveaction_status(status=None, result=None, context=None, end_timestamp=None,
liveaction_id=None, runner_info=None, liveaction_db=None,
publish=True):
"""
Update the status of the specified LiveAction to the value provided in
new_status.
The LiveAction may be specified using either liveaction_id, or as an
liveaction_db instance.
"""
if (liveaction_id is None) and (liveaction_db is None):
raise ValueError('Must specify an liveaction_id or an liveaction_db when '
'calling update_LiveAction_status')
if liveaction_db is None:
liveaction_db = get_liveaction_by_id(liveaction_id)
if status not in LIVEACTION_STATUSES:
raise ValueError('Attempting to set status for LiveAction "%s" '
'to unknown status string. Unknown status is "%s"',
liveaction_db, status)
extra = {'liveaction_db': liveaction_db}
LOG.debug('Updating ActionExection: "%s" with status="%s"', liveaction_db.id, status,
extra=extra)
# If liveaction is already canceled, then do not allow status to be updated.
if liveaction_db.status == LIVEACTION_STATUS_CANCELED and status != LIVEACTION_STATUS_CANCELED:
LOG.info('Unable to update ActionExecution "%s" with status="%s". '
'ActionExecution is already canceled.', liveaction_db.id, status, extra=extra)
return liveaction_db
old_status = liveaction_db.status
liveaction_db.status = status
if result:
liveaction_db.result = result
if context:
liveaction_db.context.update(context)
if end_timestamp:
liveaction_db.end_timestamp = end_timestamp
if runner_info:
liveaction_db.runner_info = runner_info
liveaction_db = LiveAction.add_or_update(liveaction_db)
LOG.debug('Updated status for LiveAction object.', extra=extra)
if publish and status != old_status:
LiveAction.publish_status(liveaction_db)
LOG.debug('Published status for LiveAction object.', extra=extra)
return liveaction_db
def serialize_positional_argument(argument_type, argument_value):
"""
Serialize the provided positional argument.
Note: Serialization is NOT performed recursively since it doesn't make much
sense for shell script actions (only the outter / top level value is
serialized).
"""
if argument_type in ['string', 'number', 'float']:
argument_value = str(argument_value) if argument_value else ''
elif argument_type == 'boolean':
# Booleans are serialized as string "1" and "0"
if argument_value is not None:
argument_value = '1' if bool(argument_value) else '0'
else:
argument_value = ''
elif argument_type == 'list':
# Lists are serialized a comma delimited string (foo,bar,baz)
argument_value = ','.join(argument_value) if argument_value else ''
elif argument_type == 'object':
# Objects are serialized as JSON
argument_value = json.dumps(argument_value) if argument_value else ''
elif argument_type is 'null':
# None / null is serialized as en empty string
argument_value = ''
else:
# Other values are simply cast to strings
argument_value = str(argument_value) if argument_value else ''
return argument_value
def get_args(action_parameters, action_db):
"""
Get and serialize positional and named arguments.
:return: (positional_args, named_args)
:rtype: (``str``, ``dict``)
"""
position_args_dict = _get_position_arg_dict(action_parameters, action_db)
action_db_parameters = action_db.parameters or {}
positional_args = []
positional_args_keys = set()
for _, arg in six.iteritems(position_args_dict):
arg_type = action_db_parameters.get(arg, {}).get('type', None)
# Perform serialization for positional arguments
arg_value = action_parameters.get(arg, None)
arg_value = serialize_positional_argument(argument_type=arg_type,
argument_value=arg_value)
positional_args.append(arg_value)
positional_args_keys.add(arg)
named_args = {}
for param in action_parameters:
if param not in positional_args_keys:
named_args[param] = action_parameters.get(param)
return positional_args, named_args
def _get_position_arg_dict(action_parameters, action_db):
action_db_params = action_db.parameters
args_dict = {}
for param in action_db_params:
param_meta = action_db_params.get(param, None)
if param_meta is not None:
pos = param_meta.get('position')
if pos is not None:
args_dict[pos] = param
args_dict = OrderedDict(sorted(args_dict.items()))
return args_dict
10 调用
##################################
源码文件:st2/st2common/st2common/persistence/action.py
from st2common.models.db.action import action_access
from st2common.persistence import base as persistence
from st2common.persistence.actionalias import ActionAlias
from st2common.persistence.execution import ActionExecution
from st2common.persistence.executionstate import ActionExecutionState
from st2common.persistence.liveaction import LiveAction
from st2common.persistence.runner import RunnerType
__all__ = [
'Action',
'ActionAlias',
'ActionExecution',
'ActionExecutionState',
'LiveAction',
'RunnerType'
]
class Action(persistence.ContentPackResource):
impl = action_access
@classmethod
def _get_impl(cls):
return cls.impl
11 调用
###########################
源码文件:st2/st2common/st2common/util/config_loader.py
import copy
import six
from oslo_config import cfg
from st2common import log as logging
from st2common.models.db.pack import ConfigDB
from st2common.persistence.pack import ConfigSchema
from st2common.persistence.pack import Config
from st2common.content import utils as content_utils
from st2common.util import jinja as jinja_utils
from st2common.util.templating import render_template_with_system_and_user_context
from st2common.util.config_parser import ContentPackConfigParser
from st2common.exceptions.db import StackStormDBObjectNotFoundError
__all__ = [
'ContentPackConfigLoader'
]
LOG = logging.getLogger(__name__)
class ContentPackConfigLoader(object):
"""
Class which loads and resolves all the config values and returns a dictionary of resolved values
which can be passed to the resource.
It loads and resolves values in the following order:
1. Static values from <pack path>/config.yaml file
2. Dynamic and or static values from /opt/stackstorm/configs/<pack name>.yaml file.
Values are merged from left to right which means values from "<pack name>.yaml" file have
precedence and override values from pack local config file.
"""
def __init__(self, pack_name, user=None):
self.pack_name = pack_name
self.user = user or cfg.CONF.system_user.user
self.pack_path = content_utils.get_pack_base_path(pack_name=pack_name)
self._config_parser = ContentPackConfigParser(pack_name=pack_name)
def get_config(self):
result = {}
# Retrieve corresponding ConfigDB and ConfigSchemaDB object
# Note: ConfigSchemaDB is optional right now. If it doesn't exist, we assume every value
# is of a type string
try:
config_db = Config.get_by_pack(value=self.pack_name)
except StackStormDBObjectNotFoundError:
# Corresponding pack config doesn't exist. We set config_db to an empty config so
# that the default values from config schema are still correctly applied even if
# pack doesn't contain a config.
config_db = ConfigDB(pack=self.pack_name, values={})
try:
config_schema_db = ConfigSchema.get_by_pack(value=self.pack_name)
except StackStormDBObjectNotFoundError:
config_schema_db = None
# 2. Retrieve values from "global" pack config file (if available) and resolve them if
# necessary
config = self._get_values_for_config(config_schema_db=config_schema_db,
config_db=config_db)
result.update(config)
return result
def _get_values_for_config(self, config_schema_db, config_db):
schema_values = getattr(config_schema_db, 'attributes', {})
config_values = getattr(config_db, 'values', {})
config = copy.deepcopy(config_values)
# Assign dynamic config values based on the values in the datastore
config = self._assign_dynamic_config_values(schema=schema_values, config=config)
# If config_schema is available we do a second pass and set default values for required
# items which values are not provided / available in the config itself
config = self._assign_default_values(schema=schema_values, config=config)
return config
def _assign_dynamic_config_values(self, schema, config, parent_keys=None):
"""
Assign dynamic config value for a particular config item if the ite utilizes a Jinja
expression for dynamic config values.
Note: This method mutates config argument in place.
:rtype: ``dict``
"""
parent_keys = parent_keys or []
for config_item_key, config_item_value in six.iteritems(config):
schema_item = schema.get(config_item_key, {})
is_dictionary = isinstance(config_item_value, dict)
# Inspect nested object properties
if is_dictionary:
parent_keys += [config_item_key]
self._assign_dynamic_config_values(schema=schema_item.get('properties', {}),
config=config[config_item_key],
parent_keys=parent_keys)
else:
is_jinja_expression = jinja_utils.is_jinja_expression(value=config_item_value)
if is_jinja_expression:
# Resolve / render the Jinja template expression
full_config_item_key = '.'.join(parent_keys + [config_item_key])
value = self._get_datastore_value_for_expression(key=full_config_item_key,
value=config_item_value,
config_schema_item=schema_item)
config[config_item_key] = value
else:
# Static value, no resolution needed
config[config_item_key] = config_item_value
return config
def _assign_default_values(self, schema, config):
"""
Assign default values for particular config if default values are provided in the config
schema and a value is not specified in the config.
Note: This method mutates config argument in place.
:rtype: ``dict``
"""
for schema_item_key, schema_item in six.iteritems(schema):
has_default_value = 'default' in schema_item
has_config_value = schema_item_key in config
default_value = schema_item.get('default', None)
is_object = schema_item.get('type', None) == 'object'
has_properties = schema_item.get('properties', None)
if has_default_value and not has_config_value:
# Config value is not provided, but default value is, use a default value
config[schema_item_key] = default_value
# Inspect nested object properties
if is_object and has_properties:
if not config.get(schema_item_key, None):
config[schema_item_key] = {}
self._assign_default_values(schema=schema_item['properties'],
config=config[schema_item_key])
return config
def _get_datastore_value_for_expression(self, key, value, config_schema_item=None):
"""
Retrieve datastore value by first resolving the datastore expression and then retrieving
the value from the datastore.
:param key: Full path to the config item key (e.g. "token" / "auth.settings.token", etc.)
"""
from st2common.services.config import deserialize_key_value
config_schema_item = config_schema_item or {}
secret = config_schema_item.get('secret', False)
try:
value = render_template_with_system_and_user_context(value=value,
user=self.user)
except Exception as e:
# Throw a more user-friendly exception on failed render
exc_class = type(e)
original_msg = str(e)
msg = ('Failed to render dynamic configuration value for key "%s" with value '
'"%s" for pack "%s" config: %s ' % (key, value, self.pack_name, original_msg))
raise exc_class(msg)
if value:
# Deserialize the value
value = deserialize_key_value(value=value, secret=secret)
else:
value = None
return value
def get_config(pack, user):
"""Returns config for given pack and user.
"""
LOG.debug('Attempting to get config')
if pack and user:
LOG.debug('Pack and user found. Loading config.')
config_loader = ContentPackConfigLoader(
pack_name=pack,
user=user
)
config = config_loader.get_config()
else:
config = {}
LOG.debug('Config: %s', config)
return config
参考:
https://github.com/StackStorm/st2/tree/v2.6.0