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
|
|||
59 | |||
60 | self._trigger_dispatcher_service = TriggerDispatcherService(logger=sensor_wrapper._logger) |
||
0 ignored issues
–
show
It seems like
_logger 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...
|
|||
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
It seems like
_logger 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...
|
|||
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, |
||
140 | poll_interval=None, parent_args=None): |
||
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() |
||
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 |
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: