Passed
Push — master ( ccf839...2e3794 )
by Plexxi
02:27
created

ActionService.datastore_service()   A

Complexity

Conditions 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
c 2
b 0
f 0
dl 0
loc 14
rs 9.4285
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 must be called before other imports to affect speedsup
19
# We only ran it if script is ran as subprocess by action runner so we don't break the tests and
20
# other code
21
if '--is-subprocess' in sys.argv:
22
    from st2common.util.monkey_patch import monkey_patch_pkg_resources
23
    monkey_patch_pkg_resources()
24
25
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...
26
import json
27
import argparse
28
29
from oslo_config import cfg
30
31
from st2common import log as logging
32
from st2actions import config
33
from st2common.runners.base_action import Action
34
from st2common.runners.utils import get_logger_for_python_runner_action
35
from st2common.runners.utils import get_action_class_instance
36
from st2common.util import loader as action_loader
37
from st2common.util.config_loader import ContentPackConfigLoader
38
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER
39
from st2common.constants.keyvalue import SYSTEM_SCOPE
40
from st2common.constants.runners import PYTHON_RUNNER_INVALID_ACTION_STATUS_EXIT_CODE
41
from st2common.database_setup import db_setup
42
43
__all__ = [
44
    'PythonActionWrapper',
45
    'ActionService'
46
]
47
48
LOG = logging.getLogger(__name__)
49
50
INVALID_STATUS_ERROR_MESSAGE = """
51
If this is an existing action which returns a tuple with two items, it needs to be updated to
52
either:
53
54
1. Return a list instead of a tuple
55
2. Return a tuple where a first items is a status flag - (True, ('item1', 'item2'))
56
57
For more information, please see: https://docs.stackstorm.com/upgrade_notes.html#st2-v1-6
58
""".strip()
59
60
61
class ActionService(object):
62
    """
63
    Instance of this class is passed to the action instance and exposes "public"
64
    methods which can be called by the action.
65
    """
66
67
    def __init__(self, action_wrapper):
68
        self._action_wrapper = action_wrapper
69
        self._datastore_service = None
70
71
    @property
72
    def datastore_service(self):
73
        # Late import to avoid very expensive in-direct import (~1 second) when this function is
74
        # not called / used
75
        from st2common.services.datastore import DatastoreService
76
77
        if not self._datastore_service:
78
            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...
79
            logger = get_logger_for_python_runner_action(action_name=action_name)
80
            self._datastore_service = DatastoreService(logger=logger,
81
                                                       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...
82
                                                       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...
83
                                                       api_username='action_service')
84
        return self._datastore_service
85
86
    ##################################
87
    # Methods for datastore management
88
    ##################################
89
90
    def list_values(self, local=True, prefix=None):
91
        return self.datastore_service.list_values(local, prefix)
92
93
    def get_value(self, name, local=True, scope=SYSTEM_SCOPE, decrypt=False):
94
        return self.datastore_service.get_value(name, local, scope=scope, decrypt=decrypt)
95
96
    def set_value(self, name, value, ttl=None, local=True, scope=SYSTEM_SCOPE, encrypt=False):
97
        return self.datastore_service.set_value(name, value, ttl, local, scope=scope,
98
                                                encrypt=encrypt)
99
100
    def delete_value(self, name, local=True, scope=SYSTEM_SCOPE):
101
        return self.datastore_service.delete_value(name, local)
102
103
104
class PythonActionWrapper(object):
105
    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 223).

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 226).

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 225).

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...
106
        """
107
        :param pack: Name of the pack this action belongs to.
108
        :type pack: ``str``
109
110
        :param file_path: Path to the action module.
111
        :type file_path: ``str``
112
113
        :param parameters: action parameters.
114
        :type parameters: ``dict`` or ``None``
115
116
        :param user: Name of the user who triggered this action execution.
117
        :type user: ``str``
118
119
        :param parent_args: Command line arguments passed to the parent process.
120
        :type parse_args: ``list``
121
        """
122
123
        self._pack = pack
124
        self._file_path = file_path
125
        self._parameters = parameters or {}
126
        self._user = user
127
        self._parent_args = parent_args or []
128
        self._class_name = None
129
        self._logger = logging.getLogger('PythonActionWrapper')
130
131
        try:
132
            config.parse_args(args=self._parent_args)
133
        except Exception:
134
            pass
135
136
        # We don't need to ensure indexes every subprocess because they should already be created
137
        # and ensured by other services
138
        db_setup(ensure_indexes=False)
139
140
        # Note: We can only set a default user value if one is not provided after parsing the
141
        # config
142
        if not self._user:
143
            self._user = cfg.CONF.system_user.user
144
145
    def run(self):
146
        action = self._get_action_instance()
147
        output = action.run(**self._parameters)
148
149
        if isinstance(output, tuple) and len(output) == 2:
150
            # run() method returned status and data - (status, data)
151
            action_status = output[0]
152
            action_result = output[1]
153
        else:
154
            # run() method returned only data, no status (pre StackStorm v1.6)
155
            action_status = None
156
            action_result = output
157
158
        action_output = {
159
            'result': action_result,
160
            'status': None
161
        }
162
163
        if action_status is not None and not isinstance(action_status, bool):
164
            sys.stderr.write('Status returned from the action run() method must either be '
165
                             'True or False, got: %s\n' % (action_status))
166
            sys.stderr.write(INVALID_STATUS_ERROR_MESSAGE)
167
            sys.exit(PYTHON_RUNNER_INVALID_ACTION_STATUS_EXIT_CODE)
168
169
        if action_status is not None and isinstance(action_status, bool):
170
            action_output['status'] = action_status
171
172
        try:
173
            print_output = json.dumps(action_output)
174
        except Exception:
175
            print_output = str(action_output)
176
177
        # Print output to stdout so the parent can capture it
178
        sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER)
179
        sys.stdout.write(print_output + '\n')
180
        sys.stdout.write(ACTION_OUTPUT_RESULT_DELIMITER)
181
        sys.stdout.flush()
182
183
    def _get_action_instance(self):
184
        actions_cls = action_loader.register_plugin(Action, self._file_path)
185
        action_cls = actions_cls[0] if actions_cls and len(actions_cls) > 0 else None
186
187
        if not action_cls:
188
            raise Exception('File "%s" has no action or the file doesn\'t exist.' %
189
                            (self._file_path))
190
191
        config_loader = ContentPackConfigLoader(pack_name=self._pack, user=self._user)
192
        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 32).

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...
193
194
        if config:
195
            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...
196
        else:
197
            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...
198
            config = None
199
200
        action_service = ActionService(action_wrapper=self)
201
        action_instance = get_action_class_instance(action_cls=action_cls,
202
                                                    config=config,
203
                                                    action_service=action_service)
204
        return action_instance
205
206
207
if __name__ == '__main__':
208
    parser = argparse.ArgumentParser(description='Python action runner process wrapper')
209
    parser.add_argument('--pack', required=True,
210
                        help='Name of the pack this action belongs to')
211
    parser.add_argument('--file-path', required=True,
212
                        help='Path to the action module')
213
    parser.add_argument('--parameters', required=False,
214
                        help='Serialized action parameters')
215
    parser.add_argument('--user', required=False,
216
                        help='User who triggered the action execution')
217
    parser.add_argument('--parent-args', required=False,
218
                        help='Command line arguments passed to the parent process')
219
    parser.add_argument('--is-subprocess', required=False, action='store_true', default=False,
220
                        help='Flag which indicates script was ran by action runner')
221
    args = parser.parse_args()
222
223
    parameters = args.parameters
224
    parameters = json.loads(parameters) if parameters else {}
225
    user = args.user
226
    parent_args = json.loads(args.parent_args) if args.parent_args else []
227
228
    assert isinstance(parent_args, list)
229
    obj = PythonActionWrapper(pack=args.pack,
230
                              file_path=args.file_path,
231
                              parameters=parameters,
232
                              user=user,
233
                              parent_args=parent_args)
234
235
    obj.run()
236