Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2reactor/st2reactor/container/sensor_wrapper.py (6 issues)

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
import os
18
import json
19
import atexit
20
import argparse
21
import traceback
22
23
from oslo_config import cfg
24
25
from st2common import log as logging
26
from st2common.constants.keyvalue import SYSTEM_SCOPE
27
from st2common.logging.misc import set_log_level_for_all_loggers
28
from st2common.models.api.trigger import TriggerAPI
29
from st2common.persistence.db_init import db_setup_with_retry
30
from st2common.util import loader
31
from st2common.util.config_loader import ContentPackConfigLoader
32
from st2common.services.triggerwatcher import TriggerWatcher
33
from st2common.services.trigger_dispatcher import TriggerDispatcherService
34
from st2reactor.sensor.base import Sensor
35
from st2reactor.sensor.base import PollingSensor
36
from st2reactor.sensor import config
37
from st2common.services.datastore import SensorDatastoreService
38
from st2common.util.monkey_patch import monkey_patch
39
from st2common.util.monkey_patch import use_select_poll_workaround
40
41
__all__ = [
42
    'SensorWrapper',
43
    'SensorService'
44
]
45
46
monkey_patch()
47
use_select_poll_workaround(nose_only=False)
48
49
50
class SensorService(object):
51
    """
52
    Instance of this class is passed to the sensor instance and exposes "public"
53
    methods which can be called by the sensor.
54
    """
55
56
    def __init__(self, sensor_wrapper):
57
        self._sensor_wrapper = sensor_wrapper
58
        self._logger = self._sensor_wrapper._logger
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _logger 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...
59
60
        self._trigger_dispatcher_service = TriggerDispatcherService(logger=sensor_wrapper._logger)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _logger 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...
61
        self._datastore_service = SensorDatastoreService(
62
            logger=self._logger,
63
            pack_name=self._sensor_wrapper._pack,
64
            class_name=self._sensor_wrapper._class_name,
65
            api_username='sensor_service')
66
67
        self._client = None
68
69
    @property
70
    def datastore_service(self):
71
        return self._datastore_service
72
73
    def get_logger(self, name):
74
        """
75
        Retrieve an instance of a logger to be used by the sensor class.
76
        """
77
        logger_name = '%s.%s' % (self._sensor_wrapper._logger.name, name)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _logger 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...
78
        logger = logging.getLogger(logger_name)
79
        logger.propagate = True
80
81
        return logger
82
83
    ##################################
84
    # General methods
85
    ##################################
86
87
    def get_user_info(self):
88
        return self._datastore_service.get_user_info()
89
90
    ##################################
91
    # Sensor related methods
92
    ##################################
93
94
    def dispatch(self, trigger, payload=None, trace_tag=None):
95
        # Provided by the parent BaseTriggerDispatcherService class
96
        return self._trigger_dispatcher_service.dispatch(trigger=trigger, payload=payload,
97
                                                         trace_tag=trace_tag,
98
                                                         throw_on_validation_error=False)
99
100
    def dispatch_with_context(self, trigger, payload=None, trace_context=None):
101
        """
102
        Method which dispatches the trigger.
103
104
        :param trigger: Full name / reference of the trigger.
105
        :type trigger: ``str``
106
107
        :param payload: Trigger payload.
108
        :type payload: ``dict``
109
110
        :param trace_context: Trace context to associate with Trigger.
111
        :type trace_context: ``st2common.api.models.api.trace.TraceContext``
112
        """
113
        # Provided by the parent BaseTriggerDispatcherService class
114
        return self._trigger_dispatcher_service.dispatch_with_context(trigger=trigger,
115
            payload=payload,
116
            trace_context=trace_context,
117
            throw_on_validation_error=False)
118
119
    ##################################
120
    # Methods for datastore management
121
    ##################################
122
123
    def list_values(self, local=True, prefix=None):
124
        return self.datastore_service.list_values(local=local, prefix=prefix)
125
126
    def get_value(self, name, local=True, scope=SYSTEM_SCOPE, decrypt=False):
127
        return self.datastore_service.get_value(name=name, local=local, scope=scope,
128
                                                decrypt=decrypt)
129
130
    def set_value(self, name, value, ttl=None, local=True, scope=SYSTEM_SCOPE, encrypt=False):
131
        return self.datastore_service.set_value(name=name, value=value, ttl=ttl, local=local,
132
                                                scope=scope, encrypt=encrypt)
133
134
    def delete_value(self, name, local=True, scope=SYSTEM_SCOPE):
135
        return self.datastore_service.delete_value(name=name, local=local, scope=scope)
136
137
138
class SensorWrapper(object):
139
    def __init__(self, pack, file_path, class_name, trigger_types,
0 ignored issues
show
Comprehensibility Bug introduced by
trigger_types is re-defining a name which is already available in the outer-scope (previously defined on line 344).

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...
140
                 poll_interval=None, parent_args=None):
0 ignored issues
show
Comprehensibility Bug introduced by
parent_args is re-defining a name which is already available in the outer-scope (previously defined on line 346).

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...
141
        """
142
        :param pack: Name of the pack this sensor belongs to.
143
        :type pack: ``str``
144
145
        :param file_path: Path to the sensor module file.
146
        :type file_path: ``str``
147
148
        :param class_name: Sensor class name.
149
        :type class_name: ``str``
150
151
        :param trigger_types: A list of references to trigger types which
152
                                  belong to this sensor.
153
        :type trigger_types: ``list`` of ``str``
154
155
        :param poll_interval: Sensor poll interval (in seconds).
156
        :type poll_interval: ``int`` or ``None``
157
158
        :param parent_args: Command line arguments passed to the parent process.
159
        :type parse_args: ``list``
160
        """
161
        self._pack = pack
162
        self._file_path = file_path
163
        self._class_name = class_name
164
        self._trigger_types = trigger_types or []
165
        self._poll_interval = poll_interval
166
        self._parent_args = parent_args or []
167
        self._trigger_names = {}
168
169
        # 1. Parse the config with inherited parent args
170
        try:
171
            config.parse_args(args=self._parent_args)
172
        except Exception:
173
            pass
174
175
        # 2. Establish DB connection
176
        username = cfg.CONF.database.username if hasattr(cfg.CONF.database, 'username') else None
177
        password = cfg.CONF.database.password if hasattr(cfg.CONF.database, 'password') else None
178
        db_setup_with_retry(cfg.CONF.database.db_name, cfg.CONF.database.host,
179
                            cfg.CONF.database.port, username=username, password=password,
180
                            ssl=cfg.CONF.database.ssl, ssl_keyfile=cfg.CONF.database.ssl_keyfile,
181
                            ssl_certfile=cfg.CONF.database.ssl_certfile,
182
                            ssl_cert_reqs=cfg.CONF.database.ssl_cert_reqs,
183
                            ssl_ca_certs=cfg.CONF.database.ssl_ca_certs,
184
                            ssl_match_hostname=cfg.CONF.database.ssl_match_hostname)
185
186
        # 3. Instantiate the watcher
187
        self._trigger_watcher = TriggerWatcher(create_handler=self._handle_create_trigger,
188
                                               update_handler=self._handle_update_trigger,
189
                                               delete_handler=self._handle_delete_trigger,
190
                                               trigger_types=self._trigger_types,
191
                                               queue_suffix='sensorwrapper_%s_%s' %
192
                                               (self._pack, self._class_name),
193
                                               exclusive=True)
194
195
        # 4. Set up logging
196
        self._logger = logging.getLogger('SensorWrapper.%s.%s' %
197
                                         (self._pack, self._class_name))
198
        logging.setup(cfg.CONF.sensorcontainer.logging)
199
200
        if '--debug' in parent_args:
201
            set_log_level_for_all_loggers()
202
203
        self._sensor_instance = self._get_sensor_instance()
204
205
    def run(self):
206
        atexit.register(self.stop)
207
208
        self._trigger_watcher.start()
209
        self._logger.info('Watcher started')
210
211
        self._logger.info('Running sensor initialization code')
212
        self._sensor_instance.setup()
213
214
        if self._poll_interval:
215
            message = ('Running sensor in active mode (poll interval=%ss)' %
216
                       (self._poll_interval))
217
        else:
218
            message = 'Running sensor in passive mode'
219
220
        self._logger.info(message)
221
222
        try:
223
            self._sensor_instance.run()
224
        except Exception as e:
225
            # Include traceback
226
            msg = ('Sensor "%s" run method raised an exception: %s.' %
227
                   (self._class_name, str(e)))
228
            self._logger.warn(msg, exc_info=True)
229
            raise Exception(msg)
230
231
    def stop(self):
232
        # Stop watcher
233
        self._logger.info('Stopping trigger watcher')
234
        self._trigger_watcher.stop()
235
236
        # Run sensor cleanup code
237
        self._logger.info('Invoking cleanup on sensor')
238
        self._sensor_instance.cleanup()
239
240
    ##############################################
241
    # Event handler methods for the trigger events
242
    ##############################################
243
244
    def _handle_create_trigger(self, trigger):
245
        self._logger.debug('Calling sensor "add_trigger" method (trigger.type=%s)' %
246
                           (trigger.type))
247
        self._trigger_names[str(trigger.id)] = trigger
248
249
        trigger = self._sanitize_trigger(trigger=trigger)
250
        self._sensor_instance.add_trigger(trigger=trigger)
251
252
    def _handle_update_trigger(self, trigger):
253
        self._logger.debug('Calling sensor "update_trigger" method (trigger.type=%s)' %
254
                           (trigger.type))
255
        self._trigger_names[str(trigger.id)] = trigger
256
257
        trigger = self._sanitize_trigger(trigger=trigger)
258
        self._sensor_instance.update_trigger(trigger=trigger)
259
260
    def _handle_delete_trigger(self, trigger):
261
        trigger_id = str(trigger.id)
262
        if trigger_id not in self._trigger_names:
263
            return
264
265
        self._logger.debug('Calling sensor "remove_trigger" method (trigger.type=%s)' %
266
                           (trigger.type))
267
        del self._trigger_names[trigger_id]
268
269
        trigger = self._sanitize_trigger(trigger=trigger)
270
        self._sensor_instance.remove_trigger(trigger=trigger)
271
272
    def _get_sensor_instance(self):
273
        """
274
        Retrieve instance of a sensor class.
275
        """
276
        _, filename = os.path.split(self._file_path)
277
        module_name, _ = os.path.splitext(filename)
278
279
        try:
280
            sensor_class = loader.register_plugin_class(base_class=Sensor,
281
                                                        file_path=self._file_path,
282
                                                        class_name=self._class_name)
283
        except Exception as e:
284
            tb_msg = traceback.format_exc()
285
            msg = ('Failed to load sensor class from file "%s" (sensor file most likely doesn\'t '
286
                   'exist or contains invalid syntax): %s' % (self._file_path, str(e)))
287
            msg += '\n\n' + tb_msg
288
            exc_cls = type(e)
289
            raise exc_cls(msg)
290
291
        if not sensor_class:
292
            raise ValueError('Sensor module is missing a class with name "%s"' %
293
                             (self._class_name))
294
295
        sensor_class_kwargs = {}
296
        sensor_class_kwargs['sensor_service'] = SensorService(sensor_wrapper=self)
297
298
        sensor_config = self._get_sensor_config()
299
        sensor_class_kwargs['config'] = sensor_config
300
301
        if self._poll_interval and issubclass(sensor_class, PollingSensor):
302
            sensor_class_kwargs['poll_interval'] = self._poll_interval
303
304
        try:
305
            sensor_instance = sensor_class(**sensor_class_kwargs)
306
        except Exception:
307
            self._logger.exception('Failed to instantiate "%s" sensor class' % (self._class_name))
308
            raise Exception('Failed to instantiate "%s" sensor class' % (self._class_name))
309
310
        return sensor_instance
311
312
    def _get_sensor_config(self):
313
        config_loader = ContentPackConfigLoader(pack_name=self._pack)
314
        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 36).

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...
315
316
        if config:
317
            self._logger.info('Found config for sensor "%s"' % (self._class_name))
318
        else:
319
            self._logger.info('No config found for sensor "%s"' % (self._class_name))
320
321
        return config
322
323
    def _sanitize_trigger(self, trigger):
324
        sanitized = TriggerAPI.from_model(trigger).to_dict()
325
        return sanitized
326
327
328
if __name__ == '__main__':
329
    parser = argparse.ArgumentParser(description='Sensor runner wrapper')
330
    parser.add_argument('--pack', required=True,
331
                        help='Name of the pack this sensor belongs to')
332
    parser.add_argument('--file-path', required=True,
333
                        help='Path to the sensor module')
334
    parser.add_argument('--class-name', required=True,
335
                        help='Name of the sensor class')
336
    parser.add_argument('--trigger-type-refs', required=False,
337
                        help='Comma delimited string of trigger type references')
338
    parser.add_argument('--poll-interval', type=int, default=None, required=False,
339
                        help='Sensor poll interval')
340
    parser.add_argument('--parent-args', required=False,
341
                        help='Command line arguments passed to the parent process')
342
    args = parser.parse_args()
343
344
    trigger_types = args.trigger_type_refs
345
    trigger_types = trigger_types.split(',') if trigger_types else []
346
    parent_args = json.loads(args.parent_args) if args.parent_args else []
347
    assert isinstance(parent_args, list)
348
349
    obj = SensorWrapper(pack=args.pack,
350
                        file_path=args.file_path,
351
                        class_name=args.class_name,
352
                        trigger_types=trigger_types,
353
                        poll_interval=args.poll_interval,
354
                        parent_args=parent_args)
355
    obj.run()
356