Completed
Push — master ( d1f0a7...3b2ece )
by Edward
21:04 queued 05:38
created

_handle_update_trigger()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 7
rs 9.4286
cc 1
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 os
17
import sys
18
import json
19
import atexit
20
import argparse
21
22
import eventlet
23
from oslo_config import cfg
24
from st2client.client import Client
25
26
from st2common import log as logging
27
from st2common.logging.misc import set_log_level_for_all_loggers
28
from st2common.models.api.trace import TraceContext
29
from st2common.persistence.db_init import db_setup_with_retry
30
from st2common.transport.reactor import TriggerDispatcher
31
from st2common.util import loader
32
from st2common.util.config_parser import ContentPackConfigParser
33
from st2common.services.triggerwatcher import TriggerWatcher
34
from st2reactor.sensor.base import Sensor, PollingSensor
35
from st2reactor.sensor import config
36
from st2common.constants.system import API_URL_ENV_VARIABLE_NAME
37
from st2common.constants.system import AUTH_TOKEN_ENV_VARIABLE_NAME
38
from st2client.models.keyvalue import KeyValuePair
39
40
__all__ = [
41
    'SensorWrapper'
42
]
43
44
eventlet.monkey_patch(
45
    os=True,
46
    select=True,
47
    socket=True,
48
    thread=False if '--use-debugger' in sys.argv else True,
49
    time=True)
50
51
52
class SensorService(object):
53
    """
54
    Instance of this class is passed to the sensor instance and exposes "public"
55
    methods which can be called by the sensor.
56
    """
57
58
    DATASTORE_NAME_SEPARATOR = ':'
59
60
    def __init__(self, sensor_wrapper):
61
        self._sensor_wrapper = sensor_wrapper
62
        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...
63
        self._dispatcher = TriggerDispatcher(self._logger)
64
65
        self._client = None
66
67
    def get_logger(self, name):
68
        """
69
        Retrieve an instance of a logger to be used by the sensor class.
70
        """
71
        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...
72
        logger = logging.getLogger(logger_name)
73
        logger.propagate = True
74
75
        return logger
76
77
    def dispatch(self, trigger, payload=None, trace_tag=None):
78
        """
79
        Method which dispatches the trigger.
80
81
        :param trigger: Full name / reference of the trigger.
82
        :type trigger: ``str``
83
84
        :param payload: Trigger payload.
85
        :type payload: ``dict``
86
87
        :param trace_tag: Tracer to track the triggerinstance.
88
        :type trace_tags: ``str``
89
        """
90
        # empty strings
91
        trace_context = TraceContext(trace_tag=trace_tag) if trace_tag else None
92
        self.dispatch_with_context(trigger, payload=payload, trace_context=trace_context)
93
94
    def dispatch_with_context(self, trigger, payload=None, trace_context=None):
95
        """
96
        Method which dispatches the trigger.
97
98
        :param trigger: Full name / reference of the trigger.
99
        :type trigger: ``str``
100
101
        :param payload: Trigger payload.
102
        :type payload: ``dict``
103
104
        :param trace_context: Trace context to associate with Trigger.
105
        :type trace_context: ``st2common.api.models.api.trace.TraceContext``
106
        """
107
        self._dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)
108
109
    ##################################
110
    # Methods for datastore management
111
    ##################################
112
113
    def list_values(self, local=True, prefix=None):
114
        """
115
        Retrieve all the datastores items.
116
117
        :param local: List values from a namespace local to this sensor. Defaults to True.
118
        :type: local: ``bool``
119
120
        :param prefix: Optional key name prefix / startswith filter.
121
        :type prefix: ``str``
122
123
        :rtype: ``list`` of :class:`KeyValuePair`
124
        """
125
        client = self._get_api_client()
126
127
        self._logger.audit('Retrieving all the value from the datastore')
128
129
        if local:
130
            key_prefix = self._get_datastore_key_prefix() + self.DATASTORE_NAME_SEPARATOR
131
132
            if prefix:
133
                key_prefix += prefix
134
        else:
135
            key_prefix = prefix
136
137
        kvps = client.keys.get_all(prefix=key_prefix)
138
        return kvps
139
140
    def get_value(self, name, local=True):
141
        """
142
        Retrieve a value from the datastore for the provided key.
143
144
        By default, value is retrieved from the namespace local to the sensor. If you want to
145
        retrieve a global value from a datastore, pass local=False to this method.
146
147
        :param name: Key name.
148
        :type name: ``str``
149
150
        :param local: Retrieve value from a namespace local to the sensor. Defaults to True.
151
        :type: local: ``bool``
152
153
        :rtype: ``str`` or ``None``
154
        """
155
        if local:
156
            name = self._get_key_name_with_sensor_prefix(name=name)
157
158
        client = self._get_api_client()
159
160
        self._logger.audit('Retrieving value from the datastore (name=%s)', name)
161
162
        try:
163
            kvp = client.keys.get_by_id(id=name)
164
        except Exception:
165
            return None
166
167
        if kvp:
168
            return kvp.value
169
170
        return None
171
172
    def set_value(self, name, value, ttl=None, local=True):
173
        """
174
        Set a value for the provided key.
175
176
        By default, value is set in a namespace local to the sensor. If you want to
177
        set a global value, pass local=False to this method.
178
179
        :param name: Key name.
180
        :type name: ``str``
181
182
        :param value: Key value.
183
        :type value: ``str``
184
185
        :param ttl: Optional TTL (in seconds).
186
        :type ttl: ``int``
187
188
        :param local: Set value in a namespace local to the sensor. Defaults to True.
189
        :type: local: ``bool``
190
191
        :return: ``True`` on success, ``False`` otherwise.
192
        :rtype: ``bool``
193
        """
194
        if local:
195
            name = self._get_key_name_with_sensor_prefix(name=name)
196
197
        value = str(value)
198
        client = self._get_api_client()
199
200
        self._logger.audit('Setting value in the datastore (name=%s)', name)
201
202
        instance = KeyValuePair()
203
        instance.id = name
204
        instance.name = name
205
        instance.value = value
206
207
        if ttl:
208
            instance.ttl = ttl
209
210
        client.keys.update(instance=instance)
211
        return True
212
213
    def delete_value(self, name, local=True):
214
        """
215
        Delete the provided key.
216
217
        By default, value is deleted from a namespace local to the sensor. If you want to
218
        delete a global value, pass local=False to this method.
219
220
        :param name: Name of the key to delete.
221
        :type name: ``str``
222
223
        :param local: Delete a value in a namespace local to the sensor. Defaults to True.
224
        :type: local: ``bool``
225
226
        :return: ``True`` on success, ``False`` otherwise.
227
        :rtype: ``bool``
228
        """
229
        if local:
230
            name = self._get_key_name_with_sensor_prefix(name=name)
231
232
        client = self._get_api_client()
233
234
        instance = KeyValuePair()
235
        instance.id = name
236
        instance.name = name
237
238
        self._logger.audit('Deleting value from the datastore (name=%s)', name)
239
240
        try:
241
            client.keys.delete(instance=instance)
242
        except Exception:
243
            return False
244
245
        return True
246
247
    def _get_api_client(self):
248
        """
249
        Retrieve API client instance.
250
        """
251
        # TODO: API client is really unfriendly and needs to be re-designed and
252
        # improved
253
        api_url = os.environ.get(API_URL_ENV_VARIABLE_NAME, None)
254
        auth_token = os.environ.get(AUTH_TOKEN_ENV_VARIABLE_NAME, None)
255
256
        if not api_url or not auth_token:
257
            raise ValueError('%s and %s environment variable must be set' %
258
                             (API_URL_ENV_VARIABLE_NAME, AUTH_TOKEN_ENV_VARIABLE_NAME))
259
260
        if not self._client:
261
            self._client = Client(api_url=api_url)
262
263
        return self._client
264
265
    def _get_key_name_with_sensor_prefix(self, name):
266
        """
267
        Retrieve a full key name which is local to the current sensor.
268
269
        :param name: Base datastore key name.
270
        :type name: ``str``
271
272
        :rtype: ``str``
273
        """
274
        prefix = self._get_datastore_key_prefix()
275
        full_name = prefix + self.DATASTORE_NAME_SEPARATOR + name
276
        return full_name
277
278
    def _get_datastore_key_prefix(self):
279
        prefix = '%s.%s' % (self._sensor_wrapper._pack, self._sensor_wrapper._class_name)
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...
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...
280
        return prefix
281
282
283
class SensorWrapper(object):
284
    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 480).

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...
285
                 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 482).

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...
286
        """
287
        :param pack: Name of the pack this sensor belongs to.
288
        :type pack: ``str``
289
290
        :param file_path: Path to the sensor module file.
291
        :type file_path: ``str``
292
293
        :param class_name: Sensor class name.
294
        :type class_name: ``str``
295
296
        :param trigger_types: A list of references to trigger types which
297
                                  belong to this sensor.
298
        :type trigger_types: ``list`` of ``str``
299
300
        :param poll_interval: Sensor poll interval (in seconds).
301
        :type poll_interval: ``int`` or ``None``
302
303
        :param parent_args: Command line arguments passed to the parent process.
304
        :type parse_args: ``list``
305
        """
306
        self._pack = pack
307
        self._file_path = file_path
308
        self._class_name = class_name
309
        self._trigger_types = trigger_types or []
310
        self._poll_interval = poll_interval
311
        self._parent_args = parent_args or []
312
        self._trigger_names = {}
313
314
        # 1. Parse the config with inherited parent args
315
        try:
316
            config.parse_args(args=self._parent_args)
317
        except Exception:
318
            pass
319
320
        # 2. Establish DB connection
321
        username = cfg.CONF.database.username if hasattr(cfg.CONF.database, 'username') else None
322
        password = cfg.CONF.database.password if hasattr(cfg.CONF.database, 'password') else None
323
        db_setup_with_retry(cfg.CONF.database.db_name, cfg.CONF.database.host,
324
                            cfg.CONF.database.port, username=username, password=password)
325
326
        # 3. Instantiate the watcher
327
        self._trigger_watcher = TriggerWatcher(create_handler=self._handle_create_trigger,
328
                                               update_handler=self._handle_update_trigger,
329
                                               delete_handler=self._handle_delete_trigger,
330
                                               trigger_types=self._trigger_types,
331
                                               queue_suffix='sensorwrapper_%s_%s' %
332
                                               (self._pack, self._class_name),
333
                                               exclusive=True)
334
335
        # 4. Set up logging
336
        self._logger = logging.getLogger('SensorWrapper.%s' %
337
                                         (self._class_name))
338
        logging.setup(cfg.CONF.sensorcontainer.logging)
339
340
        if '--debug' in parent_args:
341
            set_log_level_for_all_loggers()
342
343
        self._sensor_instance = self._get_sensor_instance()
344
345
    def run(self):
346
        atexit.register(self.stop)
347
348
        self._trigger_watcher.start()
349
        self._logger.info('Watcher started')
350
351
        self._logger.info('Running sensor initialization code')
352
        self._sensor_instance.setup()
353
354
        if self._poll_interval:
355
            message = ('Running sensor in active mode (poll interval=%ss)' %
356
                       (self._poll_interval))
357
        else:
358
            message = 'Running sensor in passive mode'
359
360
        self._logger.info(message)
361
362
        try:
363
            self._sensor_instance.run()
364
        except Exception as e:
365
            # Include traceback
366
            msg = ('Sensor "%s" run method raised an exception: %s.' %
367
                   (self._class_name, str(e)))
368
            self._logger.warn(msg, exc_info=True)
369
            raise Exception(msg)
370
371
    def stop(self):
372
        # Stop watcher
373
        self._logger.info('Stopping trigger watcher')
374
        self._trigger_watcher.stop()
375
376
        # Run sensor cleanup code
377
        self._logger.info('Invoking cleanup on sensor')
378
        self._sensor_instance.cleanup()
379
380
    ##############################################
381
    # Event handler methods for the trigger events
382
    ##############################################
383
384
    def _handle_create_trigger(self, trigger):
385
        self._logger.debug('Calling sensor "add_trigger" method (trigger.type=%s)' %
386
                           (trigger.type))
387
        self._trigger_names[str(trigger.id)] = trigger
388
389
        trigger = self._sanitize_trigger(trigger=trigger)
390
        self._sensor_instance.add_trigger(trigger=trigger)
391
392
    def _handle_update_trigger(self, trigger):
393
        self._logger.debug('Calling sensor "update_trigger" method (trigger.type=%s)' %
394
                           (trigger.type))
395
        self._trigger_names[str(trigger.id)] = trigger
396
397
        trigger = self._sanitize_trigger(trigger=trigger)
398
        self._sensor_instance.update_trigger(trigger=trigger)
399
400
    def _handle_delete_trigger(self, trigger):
401
        trigger_id = str(trigger.id)
402
        if trigger_id not in self._trigger_names:
403
            return
404
405
        self._logger.debug('Calling sensor "remove_trigger" method (trigger.type=%s)' %
406
                           (trigger.type))
407
        del self._trigger_names[trigger_id]
408
409
        trigger = self._sanitize_trigger(trigger=trigger)
410
        self._sensor_instance.remove_trigger(trigger=trigger)
411
412
    def _get_sensor_instance(self):
413
        """
414
        Retrieve instance of a sensor class.
415
        """
416
        _, filename = os.path.split(self._file_path)
417
        module_name, _ = os.path.splitext(filename)
418
419
        sensor_class = loader.register_plugin_class(base_class=Sensor,
420
                                                    file_path=self._file_path,
421
                                                    class_name=self._class_name)
422
423
        if not sensor_class:
424
            raise ValueError('Sensor module is missing a class with name "%s"' %
425
                             (self._class_name))
426
427
        sensor_class_kwargs = {}
428
        sensor_class_kwargs['sensor_service'] = SensorService(sensor_wrapper=self)
429
430
        sensor_config = self._get_sensor_config()
431
        sensor_class_kwargs['config'] = sensor_config
432
433
        if self._poll_interval and issubclass(sensor_class, PollingSensor):
434
            sensor_class_kwargs['poll_interval'] = self._poll_interval
435
436
        try:
437
            sensor_instance = sensor_class(**sensor_class_kwargs)
438
        except Exception:
439
            self._logger.exception('Failed to instantiate "%s" sensor class' % (self._class_name))
440
            raise Exception('Failed to instantiate "%s" sensor class' % (self._class_name))
441
442
        return sensor_instance
443
444
    def _get_sensor_config(self):
445
        config_parser = ContentPackConfigParser(pack_name=self._pack)
446
        config = config_parser.get_sensor_config(sensor_file_path=self._file_path)
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 35).

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...
447
448
        if config:
449
            self._logger.info('Using config "%s" for sensor "%s"' % (config.file_path,
450
                                                                     self._class_name))
451
            return config.config
452
        else:
453
            self._logger.info('No config found for sensor "%s"' % (self._class_name))
454
            return {}
455
456
    def _sanitize_trigger(self, trigger):
457
        sanitized = trigger._data
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _data 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...
458
        if 'id' in sanitized:
459
            # Friendly objectid rather than the MongoEngine representation.
460
            sanitized['id'] = str(sanitized['id'])
461
        return sanitized
462
463
464
if __name__ == '__main__':
465
    parser = argparse.ArgumentParser(description='Sensor runner wrapper')
466
    parser.add_argument('--pack', required=True,
467
                        help='Name of the pack this sensor belongs to')
468
    parser.add_argument('--file-path', required=True,
469
                        help='Path to the sensor module')
470
    parser.add_argument('--class-name', required=True,
471
                        help='Name of the sensor class')
472
    parser.add_argument('--trigger-type-refs', required=False,
473
                        help='Comma delimited string of trigger type references')
474
    parser.add_argument('--poll-interval', type=int, default=None, required=False,
475
                        help='Sensor poll interval')
476
    parser.add_argument('--parent-args', required=False,
477
                        help='Command line arguments passed to the parent process')
478
    args = parser.parse_args()
479
480
    trigger_types = args.trigger_type_refs
481
    trigger_types = trigger_types.split(',') if trigger_types else []
482
    parent_args = json.loads(args.parent_args) if args.parent_args else []
483
    assert isinstance(parent_args, list)
484
485
    obj = SensorWrapper(pack=args.pack,
486
                        file_path=args.file_path,
487
                        class_name=args.class_name,
488
                        trigger_types=trigger_types,
489
                        poll_interval=args.poll_interval,
490
                        parent_args=parent_args)
491
    obj.run()
492