Test Failed
Pull Request — master (#4197)
by W
03:53
created

get_action_class_instance()   A

Complexity

Conditions 3

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
dl 0
loc 35
rs 9.0399
c 0
b 0
f 0
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
from __future__ import absolute_import
17
18
import os
19
20
import logging as stdlib_logging
21
22
import six
23
from oslo_config import cfg
24
25
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER
26
from st2common import log as logging
27
28
29
__all__ = [
30
    'PackConfigDict',
31
32
    'get_logger_for_python_runner_action',
33
    'get_action_class_instance',
34
35
    'make_read_and_store_stream_func',
36
37
    'invoke_post_run',
38
]
39
40
LOG = logging.getLogger(__name__)
41
42
# Error which is thrown when Python action tries to access self.config key which doesn't exist
43
CONFIG_MISSING_ITEM_ERROR = """
44
Config for pack "%s" is missing key "%s".
45
Make sure that the config file exists on disk (%s) and contains that key.
46
47
Also make sure you run "st2ctl reload --register-configs" when you add a
48
config and after every change you make to the config.
49
"""
50
51
# Maps logger name to the actual logger instance
52
# We re-use loggers for the same actions to make sure only a single instance exists for a
53
# particular action. This way we avoid duplicate log messages, etc.
54
LOGGERS = {}
55
56
57
class PackConfigDict(dict):
58
    """
59
    Dictionary class wraper for pack config dictionaries.
60
61
    This class throws a user-friendly exception in case user tries to access config item which
62
    doesn't exist in the dict.
63
    """
64
    def __init__(self, pack_name, *args):
65
        super(PackConfigDict, self).__init__(*args)
66
        self._pack_name = pack_name
67
68
    def __getitem__(self, key):
69
        try:
70
            value = super(PackConfigDict, self).__getitem__(key)
71
        except KeyError:
72
            # Note: We use late import to avoid performance overhead
73
            from oslo_config import cfg
0 ignored issues
show
Comprehensibility Bug introduced by
cfg is re-defining a name which is already available in the outer-scope (previously defined on line 23).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
74
75
            configs_path = os.path.join(cfg.CONF.system.base_path, 'configs/')
76
            config_path = os.path.join(configs_path, self._pack_name + '.yaml')
77
            msg = CONFIG_MISSING_ITEM_ERROR % (self._pack_name, key, config_path)
78
            raise ValueError(msg)
79
80
        return value
81
82
    def __setitem__(self, key, value):
83
        super(PackConfigDict, self).__setitem__(key, value)
84
85
86
def get_logger_for_python_runner_action(action_name, log_level='debug'):
87
    """
88
    Set up a logger which logs all the messages with level DEBUG and above to stderr.
89
    """
90
    logger_name = 'actions.python.%s' % (action_name)
91
92
    if logger_name not in LOGGERS:
93
        level_name = log_level.upper()
94
        log_level_constant = getattr(stdlib_logging, level_name, stdlib_logging.DEBUG)
95
        logger = logging.getLogger(logger_name)
96
97
        console = stdlib_logging.StreamHandler()
98
        console.setLevel(log_level_constant)
99
100
        formatter = stdlib_logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
101
        console.setFormatter(formatter)
102
        logger.addHandler(console)
103
        logger.setLevel(log_level_constant)
104
105
        LOGGERS[logger_name] = logger
106
    else:
107
        logger = LOGGERS[logger_name]
108
109
    return logger
110
111
112
def get_action_class_instance(action_cls, config=None, action_service=None):
113
    """
114
    Instantiate and return Action class instance.
115
116
    :param action_cls: Action class to instantiate.
117
    :type action_cls: ``class``
118
119
    :param config: Config to pass to the action class.
120
    :type config: ``dict``
121
122
    :param action_service: ActionService instance to pass to the class.
123
    :type action_service: :class:`ActionService`
124
    """
125
    kwargs = {}
126
    kwargs['config'] = config
127
    kwargs['action_service'] = action_service
128
129
    # Note: This is done for backward compatibility reasons. We first try to pass
130
    # "action_service" argument to the action class constructor, but if that doesn't work (e.g. old
131
    # action which hasn't been updated yet), we resort to late assignment post class instantiation.
132
    # TODO: Remove in next major version once all the affected actions have been updated.
133
    try:
134
        action_instance = action_cls(**kwargs)
135
    except TypeError as e:
136
        if 'unexpected keyword argument \'action_service\'' not in str(e):
137
            raise e
138
139
        LOG.debug('Action class (%s) constructor doesn\'t take "action_service" argument, '
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
140
                  'falling back to late assignment...' % (action_cls.__class__.__name__))
141
142
        action_service = kwargs.pop('action_service', None)
143
        action_instance = action_cls(**kwargs)
144
        action_instance.action_service = action_service
145
146
    return action_instance
147
148
149
def make_read_and_store_stream_func(execution_db, action_db, store_data_func):
150
    """
151
    Factory function which returns a function for reading from a stream (stdout / stderr).
152
153
    This function writes read data into a buffer and stores it in a database.
154
    """
155
    # NOTE: This import has intentionally been moved here to avoid massive performance overhead
156
    # (1+ second) for other functions inside this module which don't need to use those imports.
157
    import eventlet
158
159
    def read_and_store_stream(stream, buff):
160
        try:
161
            while not stream.closed:
162
                line = stream.readline()
163
                if not line:
164
                    break
165
166
                if isinstance(line, six.binary_type):
167
                    line = line.decode('utf-8')
168
169
                buff.write(line)
170
171
                # Filter out result delimiter lines
172
                if ACTION_OUTPUT_RESULT_DELIMITER in line:
173
                    continue
174
175
                if cfg.CONF.actionrunner.stream_output:
176
                    store_data_func(execution_db=execution_db, action_db=action_db, data=line)
177
        except RuntimeError:
178
            # process was terminated abruptly
179
            pass
180
        except eventlet.support.greenlets.GreenletExit:
181
            # Green thread exited / was killed
182
            pass
183
184
    return read_and_store_stream
185
186
187
def invoke_post_run(liveaction_db, action_db=None):
188
    # NOTE: This import has intentionally been moved here to avoid massive performance overhead
189
    # (1+ second) for other functions inside this module which don't need to use those imports.
190
    from st2common.runners import base as runners
191
    from st2common.util import action_db as action_db_utils
192
    from st2common.content import utils as content_utils
193
194
    LOG.info('Invoking post run for action execution %s.', liveaction_db.id)
195
196
    # Identify action and runner.
197
    if not action_db:
198
        action_db = action_db_utils.get_action_by_ref(liveaction_db.action)
199
200
    if not action_db:
201
        LOG.error('Unable to invoke post run. Action %s no longer exists.', liveaction_db.action)
202
        return
203
204
    LOG.info('Action execution %s runs %s of runner type %s.',
205
             liveaction_db.id, action_db.name, action_db.runner_type['name'])
206
207
    # Get instance of the action runner and related configuration.
208
    runner_type_db = action_db_utils.get_runnertype_by_name(action_db.runner_type['name'])
209
210
    runner = runners.get_runner(
211
        package_name=runner_type_db.runner_package,
212
        module_name=runner_type_db.runner_module)
213
214
    entry_point = content_utils.get_entry_point_abs_path(
215
        pack=action_db.pack,
216
        entry_point=action_db.entry_point)
217
218
    libs_dir_path = content_utils.get_action_libs_abs_path(
219
        pack=action_db.pack,
220
        entry_point=action_db.entry_point)
221
222
    # Configure the action runner.
223
    runner.runner_type_db = runner_type_db
224
    runner.action = action_db
225
    runner.action_name = action_db.name
226
    runner.liveaction = liveaction_db
227
    runner.liveaction_id = str(liveaction_db.id)
228
    runner.entry_point = entry_point
229
    runner.context = getattr(liveaction_db, 'context', dict())
230
    runner.callback = getattr(liveaction_db, 'callback', dict())
231
    runner.libs_dir_path = libs_dir_path
232
233
    # Invoke the post_run method.
234
    runner.post_run(liveaction_db.status, liveaction_db.result)
235