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 | import sys |
||
20 | import select |
||
21 | import traceback |
||
22 | |||
23 | # Note: This work-around is required to fix the issue with other Python modules which live |
||
24 | # inside this directory polluting and masking sys.path for Python runner actions. |
||
25 | # Since this module is ran as a Python script inside a subprocess, directory where the script |
||
26 | # lives gets added to sys.path and we don't want that. |
||
27 | # Note: We need to use just the suffix, because full path is different depending if the process |
||
28 | # is ran in virtualenv or not |
||
29 | RUNNERS_PATH_SUFFIX = 'st2common/runners' |
||
30 | if __name__ == '__main__': |
||
31 | script_path = sys.path[0] |
||
32 | if RUNNERS_PATH_SUFFIX in script_path: |
||
33 | sys.path.pop(0) |
||
34 | |||
35 | import sys |
||
36 | import json |
||
37 | import argparse |
||
38 | |||
39 | from st2common import log as logging |
||
40 | from st2common import config as st2common_config |
||
41 | from st2common.runners.base_action import Action |
||
42 | from st2common.runners.utils import get_logger_for_python_runner_action |
||
43 | from st2common.runners.utils import get_action_class_instance |
||
44 | from st2common.util import loader as action_loader |
||
45 | from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER |
||
46 | from st2common.constants.keyvalue import SYSTEM_SCOPE |
||
47 | from st2common.constants.runners import PYTHON_RUNNER_INVALID_ACTION_STATUS_EXIT_CODE |
||
48 | from st2common.constants.runners import PYTHON_RUNNER_DEFAULT_LOG_LEVEL |
||
49 | |||
50 | __all__ = [ |
||
51 | 'PythonActionWrapper', |
||
52 | 'ActionService' |
||
53 | ] |
||
54 | |||
55 | LOG = logging.getLogger(__name__) |
||
56 | |||
57 | INVALID_STATUS_ERROR_MESSAGE = """ |
||
58 | If this is an existing action which returns a tuple with two items, it needs to be updated to |
||
59 | either: |
||
60 | |||
61 | 1. Return a list instead of a tuple |
||
62 | 2. Return a tuple where a first items is a status flag - (True, ('item1', 'item2')) |
||
63 | |||
64 | For more information, please see: https://docs.stackstorm.com/upgrade_notes.html#st2-v1-6 |
||
65 | """.strip() |
||
66 | |||
67 | # How many seconds to wait for stdin input when parameters are passed in via stdin before |
||
68 | # timing out |
||
69 | READ_STDIN_INPUT_TIMEOUT = 2 |
||
70 | |||
71 | |||
72 | class ActionService(object): |
||
73 | """ |
||
74 | Instance of this class is passed to the action instance and exposes "public" methods which can |
||
75 | be called by the action. |
||
76 | """ |
||
77 | |||
78 | def __init__(self, action_wrapper): |
||
79 | self._action_wrapper = action_wrapper |
||
80 | self._datastore_service = None |
||
81 | |||
82 | @property |
||
83 | def datastore_service(self): |
||
84 | # Late import to avoid very expensive in-direct import (~1 second) when this function is |
||
85 | # not called / used |
||
86 | from st2common.services.datastore import ActionDatastoreService |
||
87 | |||
88 | if not self._datastore_service: |
||
89 | # Note: We use temporary auth token generated by the container which is valid for the |
||
90 | # duration of the action lifetime |
||
91 | action_name = self._action_wrapper._class_name |
||
0 ignored issues
–
show
|
|||
92 | log_level = self._action_wrapper._log_level |
||
0 ignored issues
–
show
It seems like
_log_level was declared protected and should not be accessed from this context.
Prefixing a member variable 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...
log_level is re-defining a name which is already available in the outer-scope (previously defined on line 275 ).
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...
|
|||
93 | logger = get_logger_for_python_runner_action(action_name=action_name, |
||
94 | log_level=log_level) |
||
95 | pack_name = self._action_wrapper._pack |
||
0 ignored issues
–
show
It seems like
_pack was declared protected and should not be accessed from this context.
Prefixing a member variable 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...
|
|||
96 | class_name = self._action_wrapper._class_name |
||
0 ignored issues
–
show
It seems like
_class_name was declared protected and should not be accessed from this context.
Prefixing a member variable 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...
|
|||
97 | auth_token = os.environ.get('ST2_ACTION_AUTH_TOKEN', None) |
||
98 | self._datastore_service = ActionDatastoreService(logger=logger, |
||
99 | pack_name=pack_name, |
||
100 | class_name=class_name, |
||
101 | auth_token=auth_token) |
||
102 | return self._datastore_service |
||
103 | |||
104 | ################################## |
||
105 | # General methods |
||
106 | ################################## |
||
107 | |||
108 | def get_user_info(self): |
||
109 | return self.datastore_service.get_user_info() |
||
110 | |||
111 | ################################## |
||
112 | # Methods for datastore management |
||
113 | ################################## |
||
114 | |||
115 | def list_values(self, local=True, prefix=None): |
||
116 | return self.datastore_service.list_values(local=local, prefix=prefix) |
||
117 | |||
118 | def get_value(self, name, local=True, scope=SYSTEM_SCOPE, decrypt=False): |
||
119 | return self.datastore_service.get_value(name=name, local=local, scope=scope, |
||
120 | decrypt=decrypt) |
||
121 | |||
122 | def set_value(self, name, value, ttl=None, local=True, scope=SYSTEM_SCOPE, encrypt=False): |
||
123 | return self.datastore_service.set_value(name=name, value=value, ttl=ttl, local=local, |
||
124 | scope=scope, encrypt=encrypt) |
||
125 | |||
126 | def delete_value(self, name, local=True, scope=SYSTEM_SCOPE): |
||
127 | return self.datastore_service.delete_value(name=name, local=local, scope=scope) |
||
128 | |||
129 | |||
130 | class PythonActionWrapper(object): |
||
131 | def __init__(self, pack, file_path, config=None, parameters=None, user=None, parent_args=None, |
||
0 ignored issues
–
show
parameters is re-defining a name which is already available in the outer-scope (previously defined on line 280 ).
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...
user is re-defining a name which is already available in the outer-scope (previously defined on line 273 ).
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...
config is re-defining a name which is already available in the outer-scope (previously defined on line 272 ).
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...
parent_args is re-defining a name which is already available in the outer-scope (previously defined on line 274 ).
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...
|
|||
132 | log_level=PYTHON_RUNNER_DEFAULT_LOG_LEVEL): |
||
0 ignored issues
–
show
log_level is re-defining a name which is already available in the outer-scope (previously defined on line 275 ).
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...
|
|||
133 | """ |
||
134 | :param pack: Name of the pack this action belongs to. |
||
135 | :type pack: ``str`` |
||
136 | |||
137 | :param file_path: Path to the action module. |
||
138 | :type file_path: ``str`` |
||
139 | |||
140 | :param config: Pack config. |
||
141 | :type config: ``dict`` |
||
142 | |||
143 | :param parameters: action parameters. |
||
144 | :type parameters: ``dict`` or ``None`` |
||
145 | |||
146 | :param user: Name of the user who triggered this action execution. |
||
147 | :type user: ``str`` |
||
148 | |||
149 | :param parent_args: Command line arguments passed to the parent process. |
||
150 | :type parse_args: ``list`` |
||
151 | """ |
||
152 | |||
153 | self._pack = pack |
||
154 | self._file_path = file_path |
||
155 | self._config = config or {} |
||
156 | self._parameters = parameters or {} |
||
157 | self._user = user |
||
158 | self._parent_args = parent_args or [] |
||
159 | self._log_level = log_level |
||
160 | |||
161 | self._class_name = None |
||
162 | self._logger = logging.getLogger('PythonActionWrapper') |
||
163 | |||
164 | try: |
||
165 | st2common_config.parse_args(args=self._parent_args) |
||
166 | except Exception as e: |
||
167 | LOG.debug('Failed to parse config using parent args (parent_args=%s): %s' % |
||
168 | (str(self._parent_args), str(e))) |
||
169 | |||
170 | # Note: We can only set a default user value if one is not provided after parsing the |
||
171 | # config |
||
172 | if not self._user: |
||
173 | # Note: We use late import to avoid performance overhead |
||
174 | from oslo_config import cfg |
||
175 | self._user = cfg.CONF.system_user.user |
||
176 | |||
177 | def run(self): |
||
178 | action = self._get_action_instance() |
||
179 | output = action.run(**self._parameters) |
||
180 | |||
181 | if isinstance(output, tuple) and len(output) == 2: |
||
182 | # run() method returned status and data - (status, data) |
||
183 | action_status = output[0] |
||
184 | action_result = output[1] |
||
185 | else: |
||
186 | # run() method returned only data, no status (pre StackStorm v1.6) |
||
187 | action_status = None |
||
188 | action_result = output |
||
189 | |||
190 | action_output = { |
||
191 | 'result': action_result, |
||
192 | 'status': None |
||
193 | } |
||
194 | |||
195 | if action_status is not None and not isinstance(action_status, bool): |
||
196 | sys.stderr.write('Status returned from the action run() method must either be ' |
||
197 | 'True or False, got: %s\n' % (action_status)) |
||
198 | sys.stderr.write(INVALID_STATUS_ERROR_MESSAGE) |
||
199 | sys.exit(PYTHON_RUNNER_INVALID_ACTION_STATUS_EXIT_CODE) |
||
200 | |||
201 | if action_status is not None and isinstance(action_status, bool): |
||
202 | action_output['status'] = action_status |
||
203 | |||
204 | # Special case if result object is not JSON serializable - aka user wanted to return a |
||
205 | # non-simple type (e.g. class instance or other non-JSON serializable type) |
||
206 | try: |
||
207 | json.dumps(action_output['result']) |
||
208 | except TypeError: |
||
209 | action_output['result'] = str(action_output['result']) |
||
210 | |||
211 | try: |
||
212 | print_output = json.dumps(action_output) |
||
213 | except Exception: |
||
214 | print_output = str(action_output) |
||
215 | |||
216 | # Print output to stdout so the parent can capture it |
||
217 | sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER) |
||
218 | sys.stdout.write(print_output + '\n') |
||
219 | sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER) |
||
220 | sys.stdout.flush() |
||
221 | |||
222 | def _get_action_instance(self): |
||
223 | try: |
||
224 | actions_cls = action_loader.register_plugin(Action, self._file_path) |
||
225 | except Exception as e: |
||
226 | tb_msg = traceback.format_exc() |
||
227 | msg = ('Failed to load action class from file "%s" (action file most likely doesn\'t ' |
||
0 ignored issues
–
show
msg is re-defining a name which is already available in the outer-scope (previously defined on line 303 ).
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...
|
|||
228 | 'exist or contains invalid syntax): %s' % (self._file_path, str(e))) |
||
229 | msg += '\n\n' + tb_msg |
||
230 | exc_cls = type(e) |
||
231 | raise exc_cls(msg) |
||
232 | |||
233 | action_cls = actions_cls[0] if actions_cls and len(actions_cls) > 0 else None |
||
234 | |||
235 | if not action_cls: |
||
236 | raise Exception('File "%s" has no action class or the file doesn\'t exist.' % |
||
237 | (self._file_path)) |
||
238 | |||
239 | # Retrieve name of the action class |
||
240 | # Note - we need to either use cls.__name_ or inspect.getmro(cls)[0].__name__ to |
||
241 | # retrieve a correct name |
||
242 | self._class_name = action_cls.__name__ |
||
243 | |||
244 | action_service = ActionService(action_wrapper=self) |
||
245 | action_instance = get_action_class_instance(action_cls=action_cls, |
||
246 | config=self._config, |
||
247 | action_service=action_service) |
||
248 | return action_instance |
||
249 | |||
250 | |||
251 | if __name__ == '__main__': |
||
252 | parser = argparse.ArgumentParser(description='Python action runner process wrapper') |
||
253 | parser.add_argument('--pack', required=True, |
||
254 | help='Name of the pack this action belongs to') |
||
255 | parser.add_argument('--file-path', required=True, |
||
256 | help='Path to the action module') |
||
257 | parser.add_argument('--config', required=False, |
||
258 | help='Pack config serialized as JSON') |
||
259 | parser.add_argument('--parameters', required=False, |
||
260 | help='Serialized action parameters') |
||
261 | parser.add_argument('--stdin-parameters', required=False, action='store_true', |
||
262 | help='Serialized action parameters via stdin') |
||
263 | parser.add_argument('--user', required=False, |
||
264 | help='User who triggered the action execution') |
||
265 | parser.add_argument('--parent-args', required=False, |
||
266 | help='Command line arguments passed to the parent process serialized as ' |
||
267 | ' JSON') |
||
268 | parser.add_argument('--log-level', required=False, default=PYTHON_RUNNER_DEFAULT_LOG_LEVEL, |
||
269 | help='Log level for actions') |
||
270 | args = parser.parse_args() |
||
271 | |||
272 | config = json.loads(args.config) if args.config else {} |
||
273 | user = args.user |
||
274 | parent_args = json.loads(args.parent_args) if args.parent_args else [] |
||
275 | log_level = args.log_level |
||
276 | |||
277 | if not isinstance(config, dict): |
||
278 | raise ValueError('Pack config needs to be a dictionary') |
||
279 | |||
280 | parameters = {} |
||
281 | |||
282 | if args.parameters: |
||
283 | LOG.debug('Getting parameters from argument') |
||
284 | args_parameters = args.parameters |
||
285 | args_parameters = json.loads(args_parameters) if args_parameters else {} |
||
286 | parameters.update(args_parameters) |
||
287 | |||
288 | if args.stdin_parameters: |
||
289 | LOG.debug('Getting parameters from stdin') |
||
290 | |||
291 | i, _, _ = select.select([sys.stdin], [], [], READ_STDIN_INPUT_TIMEOUT) |
||
292 | |||
293 | if not i: |
||
294 | raise ValueError(('No input received and timed out while waiting for ' |
||
295 | 'parameters from stdin')) |
||
296 | |||
297 | stdin_data = sys.stdin.readline().strip() |
||
298 | |||
299 | try: |
||
300 | stdin_parameters = json.loads(stdin_data) |
||
301 | stdin_parameters = stdin_parameters.get('parameters', {}) |
||
302 | except Exception as e: |
||
303 | msg = ('Failed to parse parameters from stdin. Expected a JSON object with ' |
||
304 | '"parameters" attribute: %s' % (str(e))) |
||
305 | raise ValueError(msg) |
||
306 | |||
307 | parameters.update(stdin_parameters) |
||
308 | |||
309 | LOG.debug('Received parameters: %s', parameters) |
||
310 | |||
311 | assert isinstance(parent_args, list) |
||
312 | obj = PythonActionWrapper(pack=args.pack, |
||
313 | file_path=args.file_path, |
||
314 | config=config, |
||
315 | parameters=parameters, |
||
316 | user=user, |
||
317 | parent_args=parent_args, |
||
318 | log_level=log_level) |
||
319 | |||
320 | obj.run() |
||
321 |
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: