Passed
Push — develop ( 28324e...c851a6 )
by Plexxi
05:20 queued 02:38
created

PythonActionWrapper._get_action_instance()   B

Complexity

Conditions 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
c 1
b 0
f 0
dl 0
loc 24
rs 8.6845
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
import sys
17
18
# Note: This work-around is required to fix the issue with other Python modules which live
19
# inside this directory polluting and masking sys.path for Python runner actions.
20
# Since this module is ran as a Python script inside a subprocess, directory where the script
21
# lives gets added to sys.path and we don't want that.
22
# Note: We need to use just the suffix, because full path is different depending if the process
23
# is ran in virtualenv or not
24
RUNNERS_PATH_SUFFIX = 'st2common/runners'
25
if __name__ == '__main__':
26
    script_path = sys.path[0]
27
    if RUNNERS_PATH_SUFFIX in script_path:
28
        sys.path.pop(0)
29
30
import sys
0 ignored issues
show
Unused Code introduced by
The import sys was already done on line 16. You should be able to
remove this line.
Loading history...
31
import json
32
import argparse
33
34
from oslo_config import cfg
35
36
from st2common import log as logging
37
from st2actions import config
38
from st2common.runners.base_action import Action
39
from st2common.runners.utils import get_logger_for_python_runner_action
40
from st2common.runners.utils import get_action_class_instance
41
from st2common.util import loader as action_loader
42
from st2common.util.config_loader import ContentPackConfigLoader
43
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER
44
from st2common.constants.keyvalue import SYSTEM_SCOPE
45
from st2common.constants.runners import PYTHON_RUNNER_INVALID_ACTION_STATUS_EXIT_CODE
46
from st2common.database_setup import db_setup
47
48
__all__ = [
49
    'PythonActionWrapper',
50
    'ActionService'
51
]
52
53
LOG = logging.getLogger(__name__)
54
55
INVALID_STATUS_ERROR_MESSAGE = """
56
If this is an existing action which returns a tuple with two items, it needs to be updated to
57
either:
58
59
1. Return a list instead of a tuple
60
2. Return a tuple where a first items is a status flag - (True, ('item1', 'item2'))
61
62
For more information, please see: https://docs.stackstorm.com/upgrade_notes.html#st2-v1-6
63
""".strip()
64
65
66
class ActionService(object):
67
    """
68
    Instance of this class is passed to the action instance and exposes "public" methods which can
69
    be called by the action.
70
    """
71
72
    def __init__(self, action_wrapper):
73
        self._action_wrapper = action_wrapper
74
        self._datastore_service = None
75
76
    @property
77
    def datastore_service(self):
78
        # Late import to avoid very expensive in-direct import (~1 second) when this function is
79
        # not called / used
80
        from st2common.services.datastore import DatastoreService
81
82
        if not self._datastore_service:
83
            action_name = self._action_wrapper._class_name
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _class_name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
84
            logger = get_logger_for_python_runner_action(action_name=action_name)
85
            self._datastore_service = DatastoreService(logger=logger,
86
                                                       pack_name=self._action_wrapper._pack,
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _pack was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
87
                                                       class_name=self._action_wrapper._class_name,
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _class_name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
88
                                                       api_username='action_service')
89
        return self._datastore_service
90
91
    ##################################
92
    # Methods for datastore management
93
    ##################################
94
95
    def list_values(self, local=True, prefix=None):
96
        return self.datastore_service.list_values(local, prefix)
97
98
    def get_value(self, name, local=True, scope=SYSTEM_SCOPE, decrypt=False):
99
        return self.datastore_service.get_value(name, local, scope=scope, decrypt=decrypt)
100
101
    def set_value(self, name, value, ttl=None, local=True, scope=SYSTEM_SCOPE, encrypt=False):
102
        return self.datastore_service.set_value(name, value, ttl, local, scope=scope,
103
                                                encrypt=encrypt)
104
105
    def delete_value(self, name, local=True, scope=SYSTEM_SCOPE):
106
        return self.datastore_service.delete_value(name, local)
107
108
109
class PythonActionWrapper(object):
110
    def __init__(self, pack, file_path, parameters=None, user=None, parent_args=None):
0 ignored issues
show
Comprehensibility Bug introduced by
parameters is re-defining a name which is already available in the outer-scope (previously defined on line 237).

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...
Comprehensibility Bug introduced by
user is re-defining a name which is already available in the outer-scope (previously defined on line 239).

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...
Comprehensibility Bug introduced by
parent_args is re-defining a name which is already available in the outer-scope (previously defined on line 240).

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...
111
        """
112
        :param pack: Name of the pack this action belongs to.
113
        :type pack: ``str``
114
115
        :param file_path: Path to the action module.
116
        :type file_path: ``str``
117
118
        :param parameters: action parameters.
119
        :type parameters: ``dict`` or ``None``
120
121
        :param user: Name of the user who triggered this action execution.
122
        :type user: ``str``
123
124
        :param parent_args: Command line arguments passed to the parent process.
125
        :type parse_args: ``list``
126
        """
127
128
        self._pack = pack
129
        self._file_path = file_path
130
        self._parameters = parameters or {}
131
        self._user = user
132
        self._parent_args = parent_args or []
133
134
        self._class_name = None
135
        self._logger = logging.getLogger('PythonActionWrapper')
136
137
        try:
138
            config.parse_args(args=self._parent_args)
139
        except Exception as e:
140
            LOG.debug('Failed to parse config using parent args (parent_args=%s): %s' %
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
141
                      (str(self._parent_args), str(e)))
142
143
        # We don't need to ensure indexes every subprocess because they should already be created
144
        # and ensured by other services
145
        db_setup(ensure_indexes=False)
146
147
        # Note: We can only set a default user value if one is not provided after parsing the
148
        # config
149
        if not self._user:
150
            self._user = cfg.CONF.system_user.user
151
152
    def run(self):
153
        action = self._get_action_instance()
154
        output = action.run(**self._parameters)
155
156
        if isinstance(output, tuple) and len(output) == 2:
157
            # run() method returned status and data - (status, data)
158
            action_status = output[0]
159
            action_result = output[1]
160
        else:
161
            # run() method returned only data, no status (pre StackStorm v1.6)
162
            action_status = None
163
            action_result = output
164
165
        action_output = {
166
            'result': action_result,
167
            'status': None
168
        }
169
170
        if action_status is not None and not isinstance(action_status, bool):
171
            sys.stderr.write('Status returned from the action run() method must either be '
172
                             'True or False, got: %s\n' % (action_status))
173
            sys.stderr.write(INVALID_STATUS_ERROR_MESSAGE)
174
            sys.exit(PYTHON_RUNNER_INVALID_ACTION_STATUS_EXIT_CODE)
175
176
        if action_status is not None and isinstance(action_status, bool):
177
            action_output['status'] = action_status
178
179
            # Special case if result object is not JSON serializable - aka user wanted to return a
180
            # non-simple type (e.g. class instance or other non-JSON serializable type)
181
            try:
182
                json.dumps(action_output['result'])
183
            except TypeError:
184
                action_output['result'] = str(action_output['result'])
185
186
        try:
187
            print_output = json.dumps(action_output)
188
        except Exception:
189
            print_output = str(action_output)
190
191
        # Print output to stdout so the parent can capture it
192
        sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER)
193
        sys.stdout.write(print_output + '\n')
194
        sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER)
195
        sys.stdout.flush()
196
197
    def _get_action_instance(self):
198
        actions_cls = action_loader.register_plugin(Action, self._file_path)
199
        action_cls = actions_cls[0] if actions_cls and len(actions_cls) > 0 else None
200
201
        if not action_cls:
202
            raise Exception('File "%s" has no action or the file doesn\'t exist.' %
203
                            (self._file_path))
204
205
        self._class_name = action_cls.__class__.__name__
206
207
        config_loader = ContentPackConfigLoader(pack_name=self._pack, user=self._user)
208
        config = config_loader.get_config()
0 ignored issues
show
Comprehensibility Bug introduced by
config is re-defining a name which is already available in the outer-scope (previously defined on line 37).

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...
209
210
        if config:
211
            LOG.info('Found config for action "%s"' % (self._file_path))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
212
        else:
213
            LOG.info('No config found for action "%s"' % (self._file_path))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
214
            config = None
215
216
        action_service = ActionService(action_wrapper=self)
217
        action_instance = get_action_class_instance(action_cls=action_cls,
218
                                                    config=config,
219
                                                    action_service=action_service)
220
        return action_instance
221
222
223
if __name__ == '__main__':
224
    parser = argparse.ArgumentParser(description='Python action runner process wrapper')
225
    parser.add_argument('--pack', required=True,
226
                        help='Name of the pack this action belongs to')
227
    parser.add_argument('--file-path', required=True,
228
                        help='Path to the action module')
229
    parser.add_argument('--parameters', required=False,
230
                        help='Serialized action parameters')
231
    parser.add_argument('--user', required=False,
232
                        help='User who triggered the action execution')
233
    parser.add_argument('--parent-args', required=False,
234
                        help='Command line arguments passed to the parent process')
235
    args = parser.parse_args()
236
237
    parameters = args.parameters
238
    parameters = json.loads(parameters) if parameters else {}
239
    user = args.user
240
    parent_args = json.loads(args.parent_args) if args.parent_args else []
241
242
    assert isinstance(parent_args, list)
243
    obj = PythonActionWrapper(pack=args.pack,
244
                              file_path=args.file_path,
245
                              parameters=parameters,
246
                              user=user,
247
                              parent_args=parent_args)
248
249
    obj.run()
250