firexapp 5.7.162__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. firex_blaze/__init__.py +1 -0
  2. firex_blaze/__main__.py +109 -0
  3. firex_blaze/blaze_event_consumer.py +284 -0
  4. firex_blaze/blaze_helper.py +67 -0
  5. firex_blaze/blaze_launcher.py +144 -0
  6. firex_blaze/fast_blaze_helper.py +11 -0
  7. firex_bundle_ci/__init__.py +5 -0
  8. firex_bundle_ci/tasks.py +216 -0
  9. firex_flame/__init__.py +1 -0
  10. firex_flame/__main__.py +209 -0
  11. firex_flame/api.py +447 -0
  12. firex_flame/controller.py +437 -0
  13. firex_flame/event_broker_processor.py +201 -0
  14. firex_flame/event_file_processor.py +85 -0
  15. firex_flame/flame_helper.py +213 -0
  16. firex_flame/flame_task_graph.py +1206 -0
  17. firex_flame/launcher.py +202 -0
  18. firex_flame/main_app.py +66 -0
  19. firex_flame/model_dumper.py +195 -0
  20. firex_flame/templates/index.html +18 -0
  21. firex_flame/web_app.py +194 -0
  22. firex_flame_ui/COMMITHASH +1 -0
  23. firex_flame_ui/VERSION +1 -0
  24. firex_flame_ui/__init__.py +5 -0
  25. firex_flame_ui/assets/firex_logo.6409b05e.png +0 -0
  26. firex_flame_ui/assets/index.3a452d74.css +7 -0
  27. firex_flame_ui/assets/index.e869f273.js +637 -0
  28. firex_flame_ui/index.html +15 -0
  29. firex_flame_ui/send-firex-user-config.html +13 -0
  30. firex_keeper/__init__.py +1 -0
  31. firex_keeper/__main__.py +67 -0
  32. firex_keeper/db_model.py +61 -0
  33. firex_keeper/keeper_event_consumer.py +379 -0
  34. firex_keeper/keeper_helper.py +49 -0
  35. firex_keeper/keeper_launcher.py +57 -0
  36. firex_keeper/persist.py +258 -0
  37. firex_keeper/task_query.py +343 -0
  38. firexapp/__init__.py +6 -0
  39. firexapp/__main__.py +2 -0
  40. firexapp/application.py +230 -0
  41. firexapp/broker_manager/__init__.py +31 -0
  42. firexapp/broker_manager/broker_factory.py +83 -0
  43. firexapp/broker_manager/redis_manager.py +484 -0
  44. firexapp/celery_manager.py +373 -0
  45. firexapp/common.py +244 -0
  46. firexapp/discovery.py +167 -0
  47. firexapp/engine/__init__.py +0 -0
  48. firexapp/engine/celery.py +73 -0
  49. firexapp/engine/default_celery_config.py +169 -0
  50. firexapp/engine/firex_revoke.py +158 -0
  51. firexapp/engine/logging.py +212 -0
  52. firexapp/engine/run_controller.py +93 -0
  53. firexapp/events/__init__.py +0 -0
  54. firexapp/events/broker_event_consumer.py +144 -0
  55. firexapp/events/event_aggregator.py +372 -0
  56. firexapp/events/model.py +245 -0
  57. firexapp/fileregistry.py +64 -0
  58. firexapp/firex_subprocess.py +590 -0
  59. firexapp/info.py +289 -0
  60. firexapp/plugins.py +369 -0
  61. firexapp/reporters/__init__.py +0 -0
  62. firexapp/reporters/json_reporter.py +656 -0
  63. firexapp/submit/__init__.py +0 -0
  64. firexapp/submit/arguments.py +272 -0
  65. firexapp/submit/console.py +149 -0
  66. firexapp/submit/install_configs.py +166 -0
  67. firexapp/submit/report_trigger.py +27 -0
  68. firexapp/submit/reporting.py +220 -0
  69. firexapp/submit/shutdown.py +249 -0
  70. firexapp/submit/submit.py +774 -0
  71. firexapp/submit/tracking_service.py +50 -0
  72. firexapp/submit/uid.py +185 -0
  73. firexapp/tasks/__init__.py +0 -0
  74. firexapp/tasks/core_tasks.py +99 -0
  75. firexapp/tasks/example.py +159 -0
  76. firexapp/tasks/root_tasks.py +87 -0
  77. firexapp/testing/__init__.py +0 -0
  78. firexapp/testing/config_base.py +120 -0
  79. firexapp/testing/config_interpreter.py +195 -0
  80. firexapp/testing/coverage_plugin.py +41 -0
  81. firexapp/testing/pydev_debug_plugin.py +112 -0
  82. firexapp/testing/test_infra.py +167 -0
  83. firexapp-5.7.162.dist-info/METADATA +77 -0
  84. firexapp-5.7.162.dist-info/RECORD +107 -0
  85. firexapp-5.7.162.dist-info/WHEEL +4 -0
  86. firexapp-5.7.162.dist-info/entry_points.txt +20 -0
  87. firexapp-5.7.162.dist-info/licenses/LICENSE +29 -0
  88. firexkit/__init__.py +1 -0
  89. firexkit/argument_conversion.py +304 -0
  90. firexkit/bag_of_goodies.py +784 -0
  91. firexkit/broker.py +72 -0
  92. firexkit/chain.py +356 -0
  93. firexkit/firex_exceptions.py +38 -0
  94. firexkit/firexkit_common.py +34 -0
  95. firexkit/inspect.py +101 -0
  96. firexkit/install_resources/cloud-ci-install-configs.json +10 -0
  97. firexkit/memory_utils.py +59 -0
  98. firexkit/permissions.py +3 -0
  99. firexkit/proc_utils.py +44 -0
  100. firexkit/resources/firex.css +176 -0
  101. firexkit/resources/firex_logo.png +0 -0
  102. firexkit/resources.py +24 -0
  103. firexkit/result.py +1006 -0
  104. firexkit/revoke.py +95 -0
  105. firexkit/task.py +1832 -0
  106. firexkit/templates/link.html +8 -0
  107. firexkit/templates/log_template.html +90 -0
@@ -0,0 +1 @@
1
+ from firexapp import __version__
@@ -0,0 +1,109 @@
1
+ import argparse
2
+ import logging
3
+ import logging.handlers
4
+ import os
5
+ import sys
6
+ import signal
7
+
8
+ # Prevent dependencies from taking module loading hit of pkg_resources.
9
+ sys.modules["pkg_resources"] = type('noop', (object,), {})
10
+
11
+
12
+ from firexapp.events.model import FireXRunMetadata
13
+
14
+ from firex_blaze.blaze_event_consumer import BlazeKafkaSenderThread
15
+ from firex_blaze.fast_blaze_helper import get_blaze_dir
16
+ from firex_blaze.blaze_helper import BlazeSenderConfig, get_blaze_events_file, celery_app_from_logs_dir
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _parse_blaze_args():
22
+ parser = argparse.ArgumentParser()
23
+ parser.add_argument("--instance_name", help="Name of the blaze instance")
24
+ parser.add_argument("--logs_dir", help="Logs directory for the run to keep task data for.",
25
+ required=True)
26
+ parser.add_argument("--uid", help="FireX UID for the run to keep task data for.",
27
+ required=True)
28
+ parser.add_argument("--firex_requester", help="Requester username, if different than running user",
29
+ required=True)
30
+ parser.add_argument('--broker_recv_ready_file', help='File to create immediately before capturing celery events.',
31
+ default=None)
32
+ parser.add_argument('--logs_url', help='Webserver used from which logs can be accessed.',
33
+ default=None)
34
+
35
+ parser.add_argument('--kafka_topic', help='Topic use for the Kafka bus', required=True)
36
+ parser.add_argument('--bootstrap_servers', help='Comma seperated list of Kafka bootrap servers.', required=True)
37
+ parser.add_argument('--security_protocol', help='Protocol used to communicate with brokers. '
38
+ 'Valid values are: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL. '
39
+ 'Default: PLAINTEXT.',
40
+ default='PLAINTEXT')
41
+
42
+ # SASL-SSL OAuth 2.0 arguments
43
+ parser.add_argument('--sasl_mechanism', help='SASL mechanism (e.g., OAUTHBEARER).',
44
+ default=None)
45
+ parser.add_argument('--sasl_oauthbearer_method', help='OAuth bearer method (e.g., oidc).',
46
+ default=None)
47
+ parser.add_argument('--sasl_oauthbearer_client_id', help='OAuth client ID.',
48
+ default=None)
49
+ parser.add_argument('--sasl_oauthbearer_client_secret', help='OAuth client secret.',
50
+ default=None)
51
+ parser.add_argument('--sasl_oauthbearer_token_endpoint_url', help='OAuth token endpoint URL.',
52
+ default=None)
53
+ parser.add_argument('--ssl_ca_location', help='CA certificate location for SSL verification.',
54
+ default=None)
55
+
56
+ return parser.parse_args()
57
+
58
+
59
+ def init_blaze():
60
+ args = _parse_blaze_args()
61
+
62
+ run_metadata = FireXRunMetadata(args.uid, args.logs_dir, None, None, args.firex_requester)
63
+
64
+ blaze_dir = get_blaze_dir(run_metadata.logs_dir, args.instance_name)
65
+ os.makedirs(blaze_dir, exist_ok=True)
66
+ logging.basicConfig(
67
+ level=logging.DEBUG,
68
+ handlers=[
69
+ logging.handlers.RotatingFileHandler(
70
+ os.path.join(blaze_dir, 'blaze.log'),
71
+ maxBytes=20*1024*1024, # Max size of a log file (20 MB)
72
+ backupCount=1,
73
+ )
74
+ ],
75
+ format='[%(asctime)s][%(levelname)s][%(name)s]: %(message)s',
76
+ datefmt="%Y-%m-%d %H:%M:%S")
77
+
78
+ logging.getLogger('kafka.producer').setLevel(logging.INFO)
79
+ logger.info(f'Starting Blaze with args: {args}')
80
+
81
+ signal.signal(signal.SIGTERM, lambda _, __: sys.exit(1))
82
+
83
+ celery_app = celery_app_from_logs_dir(run_metadata.logs_dir)
84
+ blaze_sender_config = BlazeSenderConfig(
85
+ kafka_topic=args.kafka_topic,
86
+ kafka_bootstrap_servers=args.bootstrap_servers.split(','),
87
+ max_kafka_connection_retries=2,
88
+ security_protocol=args.security_protocol,
89
+ sasl_mechanism=args.sasl_mechanism,
90
+ sasl_oauthbearer_method=args.sasl_oauthbearer_method,
91
+ sasl_oauthbearer_client_id=args.sasl_oauthbearer_client_id,
92
+ sasl_oauthbearer_client_secret=args.sasl_oauthbearer_client_secret,
93
+ sasl_oauthbearer_token_endpoint_url=args.sasl_oauthbearer_token_endpoint_url,
94
+ ssl_ca_location=args.ssl_ca_location,
95
+ )
96
+ recording_file = get_blaze_events_file(run_metadata.logs_dir, args.instance_name)
97
+ return celery_app, run_metadata, args.broker_recv_ready_file, blaze_sender_config, args.logs_url, recording_file
98
+
99
+
100
+ def main():
101
+ celery_app, run_metadata, receiver_ready_file, blaze_sender_config, logs_url, recording_file = init_blaze()
102
+ BlazeKafkaSenderThread(
103
+ celery_app, run_metadata, blaze_sender_config, logs_url,
104
+ receiver_ready_file=receiver_ready_file,
105
+ recording_file=recording_file).run()
106
+
107
+
108
+ if __name__ == '__main__':
109
+ main()
@@ -0,0 +1,284 @@
1
+ """
2
+ Process events from Celery and put them on a kafka bus.
3
+ """
4
+
5
+ import logging
6
+ import json
7
+ import time
8
+ from getpass import getuser
9
+ from typing import Optional, Any
10
+
11
+ from confluent_kafka import Producer, KafkaException
12
+ from confluent_kafka.admin import AdminClient
13
+
14
+ from firexapp.events.broker_event_consumer import BrokerEventConsumerThread
15
+ from firexapp.events.model import FireXRunMetadata, COMPLETE_RUNSTATES, RunStates
16
+
17
+ from firex_blaze.blaze_helper import BlazeSenderConfig, KAFKA_EVENTS_FILE_DELIMITER
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ TASK_EVENT_TO_STATE = {
22
+ 'task-started-info': 'STARTED',
23
+ RunStates.FAILED.to_celery_event_type(): 'FAILURE',
24
+ RunStates.SUCCEEDED.to_celery_event_type(): 'SUCCESS',
25
+ RunStates.REVOKED.to_celery_event_type(): 'REVOKED',
26
+ RunStates.REVOKE_COMPLETED.to_celery_event_type(): 'REVOKED',
27
+ # historically were mapped but never sent.
28
+ # 'task-sent': 'PENDING',
29
+ # 'task-received': 'RECEIVED',
30
+ # 'task-started': 'STARTED',
31
+ # 'task-rejected': 'REJECTED',
32
+ # 'task-retried': 'RETRY',
33
+ }
34
+
35
+ BLAZE_SEND_EVENT_TYPES = tuple(
36
+ list(TASK_EVENT_TO_STATE.keys()) + ['task-completed', 'task-results', 'task-instrumentation', 'task-args']
37
+ )
38
+
39
+
40
+ def format_kafka_message(firex_id, event_data, uuid, logs_url, submitter=getuser(), firex_requester=None) -> dict[str, Any]:
41
+ return {'FIREX_ID': firex_id,
42
+ 'SUBMITTER': submitter,
43
+ 'FIREX_REQUESTER': firex_requester,
44
+ 'LOGS_URL': logs_url, # Shouldn't be required, but Lumens needs it!
45
+ 'EVENTS': [{'DATA': event_data,
46
+ 'UUID': uuid}]}
47
+
48
+
49
+
50
+ def send_kafka_mssg(kafka_producer: Producer, kafka_mssg: dict[str, Any], kafka_topic: str, firex_id: str,
51
+ partition: Optional[int] = None):
52
+ """Send message using confluent-kafka Producer."""
53
+ try:
54
+ kafka_producer.produce(
55
+ topic=kafka_topic,
56
+ value=json.dumps(kafka_mssg).encode('ascii'),
57
+ key=firex_id.encode('ascii'),
58
+ )
59
+ # Poll to handle any internal events and queued delivery reports (in case of delivery callback functions)
60
+ kafka_producer.poll(0)
61
+ except BufferError:
62
+ # Buffer is full, wait for messages to be delivered
63
+ logger.warning('Kafka producer buffer full, waiting for delivery...')
64
+ kafka_producer.poll(1)
65
+ # Retry the send
66
+ kafka_producer.produce(
67
+ topic=kafka_topic,
68
+ value=json.dumps(kafka_mssg).encode('ascii'),
69
+ key=firex_id.encode('ascii'),
70
+ )
71
+ kafka_producer.poll(0)
72
+ except KafkaException as e:
73
+ logger.error(f'Failed to send Kafka message: {e}')
74
+
75
+
76
+ def get_basic_event(name, event_type, timestamp=None, event_timestamp=time.time()):
77
+ if timestamp is None:
78
+ timestamp = event_timestamp
79
+
80
+ event_data = {'name': name,
81
+ 'type': event_type,
82
+ 'timestamp': timestamp, # Shouldn't be required, but Lumens needs it!
83
+ 'event_timestamp': event_timestamp}
84
+
85
+ # Not all types map to states (e.g. task-results), so only populate state for some event types.
86
+ if event_type in TASK_EVENT_TO_STATE:
87
+ event_data['state'] = TASK_EVENT_TO_STATE[event_type]
88
+
89
+ return event_data
90
+
91
+
92
+ class NoNameForEvent(Exception):
93
+ pass
94
+
95
+
96
+ class KafkaSenderThread(BrokerEventConsumerThread):
97
+
98
+ def __init__(
99
+ self,
100
+ celery_app,
101
+ run_metadata: FireXRunMetadata,
102
+ config: BlazeSenderConfig,
103
+ max_retry_attempts: Optional[int] = None,
104
+ receiver_ready_file: Optional[str] = None,
105
+ recording_file: Optional[str] = None,
106
+ partition: Optional[int] = None,
107
+ ):
108
+
109
+ super().__init__(celery_app, max_retry_attempts, receiver_ready_file)
110
+ self.firex_id = run_metadata.firex_id
111
+ self.kafka_topic = config.kafka_topic
112
+ self.recording_file = recording_file
113
+ self.partition = partition
114
+
115
+ # Connect to bootstrap servers and get a Producer instance
116
+ self.producer = self.get_kafka_producer(config)
117
+ logger.debug (f'Kafka producer created for topic {self.kafka_topic}')
118
+ self.root_task = {'uuid': None, 'is_complete': False}
119
+
120
+ @classmethod
121
+ def get_kafka_producer(cls, config: BlazeSenderConfig) -> Producer:
122
+ """Create confluent-kafka Producer with retry logic."""
123
+ _retries = 0
124
+
125
+ # Build configuration dictionary for confluent-kafka
126
+ producer_config = {
127
+ 'bootstrap.servers': ','.join(config.kafka_bootstrap_servers)
128
+ if isinstance(config.kafka_bootstrap_servers, list)
129
+ else config.kafka_bootstrap_servers,
130
+ 'security.protocol': config.security_protocol,
131
+ }
132
+
133
+ # Add SASL-SSL OAuth parameters
134
+ if config.sasl_mechanism:
135
+ producer_config['sasl.mechanism'] = config.sasl_mechanism
136
+ if config.sasl_oauthbearer_method:
137
+ producer_config['sasl.oauthbearer.method'] = config.sasl_oauthbearer_method
138
+ if config.sasl_oauthbearer_client_id:
139
+ producer_config['sasl.oauthbearer.client.id'] = config.sasl_oauthbearer_client_id
140
+ if config.sasl_oauthbearer_client_secret:
141
+ producer_config['sasl.oauthbearer.client.secret'] = config.sasl_oauthbearer_client_secret
142
+ if config.sasl_oauthbearer_token_endpoint_url:
143
+ producer_config['sasl.oauthbearer.token.endpoint.url'] = config.sasl_oauthbearer_token_endpoint_url
144
+ if config.ssl_ca_location:
145
+ producer_config['ssl.ca.location'] = config.ssl_ca_location
146
+
147
+ while True:
148
+ try:
149
+ # Create the producer - it will handle connection and authentication
150
+ producer = Producer(producer_config)
151
+ logger.info('Successfully created Kafka producer')
152
+ return producer
153
+
154
+ except KafkaException as e:
155
+ if _retries < config.max_kafka_connection_retries:
156
+ _retries += 1
157
+ logger.exception(e)
158
+ logger.warning(f'Retrying connecting to bootstrap servers '
159
+ f'[retry {_retries}/{config.max_kafka_connection_retries}]')
160
+ time.sleep(min(2 ** _retries, 30)) # Exponential backoff
161
+ else:
162
+ logger.error(f'Failed to connect to Kafka after {config.max_kafka_connection_retries} retries')
163
+ raise
164
+
165
+ def _is_root_complete(self):
166
+ return self.root_task['is_complete']
167
+
168
+ def _update_root_task(self, event):
169
+ if (
170
+ event.get('type') == 'task-received'
171
+ and 'root_id' in event
172
+ and self.root_task['uuid'] is None
173
+ ):
174
+ self.root_task['uuid'] = event['root_id']
175
+
176
+ if (
177
+ event['uuid'] == self.root_task['uuid']
178
+ # crazy things can happen with the celery task state model;
179
+ # avoid switching out of completed.
180
+ and RunStates.is_complete_state(event.get('type'))
181
+ ):
182
+ self.root_task['is_complete'] = True
183
+
184
+ def _send_celery_event_to_kafka(self, celery_event: dict[str, Any]) -> list[dict[str, Any]]:
185
+ raise NotImplementedError("Subclasses must implement sending.")
186
+
187
+ def _on_celery_event(self, event):
188
+ if 'uuid' not in event:
189
+ return
190
+
191
+ self._update_root_task(event)
192
+
193
+ sent_kafka_events = self._send_celery_event_to_kafka(event)
194
+
195
+ if sent_kafka_events and self.recording_file:
196
+ # Append the event to the recording file.
197
+ with open(self.recording_file, "a") as rec:
198
+ for e in sent_kafka_events:
199
+ event_data_str = json.dumps(e, sort_keys=True, indent=2)
200
+ rec.write(event_data_str + KAFKA_EVENTS_FILE_DELIMITER)
201
+
202
+ def _on_cleanup(self):
203
+ """Flush and close the producer with proper timeout handling."""
204
+ logger.info('Flushing Kafka producer...')
205
+
206
+ # Flush with timeout - returns number of messages still in queue
207
+ remaining = self.producer.flush(timeout=120)
208
+
209
+ if remaining > 0:
210
+ logger.warning(f'{remaining} messages were not delivered before timeout')
211
+ else:
212
+ logger.info('All messages successfully delivered to Kafka')
213
+
214
+
215
+ class BlazeKafkaSenderThread(KafkaSenderThread):
216
+ """Captures Celery events and puts them on a Kafka bus."""
217
+
218
+ def __init__(self,
219
+ celery_app,
220
+ run_metadata: FireXRunMetadata,
221
+ config: BlazeSenderConfig,
222
+ logs_url: str,
223
+ max_retry_attempts: Optional[int] = None,
224
+ receiver_ready_file: Optional[str] = None,
225
+ recording_file: Optional[str] = None,
226
+ ):
227
+
228
+ super().__init__(
229
+ celery_app, run_metadata, config, max_retry_attempts,
230
+ receiver_ready_file, recording_file)
231
+
232
+ self.submitter = getuser()
233
+ self.firex_requester = run_metadata.firex_requester
234
+ self.firex_id = run_metadata.firex_id
235
+ self.logs_url = logs_url
236
+ self.kafka_topic = config.kafka_topic
237
+ self.uuid_to_task_name_mapping : dict[str, str] = {}
238
+
239
+ def _get_kafka_event(self, event: dict[str, Any]) -> dict[str, Any]:
240
+ uuid = event.pop('uuid')
241
+ if uuid not in self.uuid_to_task_name_mapping and 'long_name' in event:
242
+ self.uuid_to_task_name_mapping[uuid] = event['long_name']
243
+
244
+ if uuid in self.uuid_to_task_name_mapping:
245
+ task_name = self.uuid_to_task_name_mapping[uuid]
246
+ else:
247
+ # No need to produce this event since it won't be processed by Lumens anyways
248
+ raise NoNameForEvent(f'No task name found for {event}; can not send the event')
249
+
250
+ # Remove result since we only should report firex_result, not the native result
251
+ event.pop('result', None)
252
+
253
+ basic_event_data = get_basic_event(
254
+ name=task_name,
255
+ event_type=event.get('type'),
256
+ timestamp=event['timestamp'],
257
+ # Add the event_timestamp (copy of the local_received), since the native timestamp that
258
+ # Celery provides is broken (its local time instead of UTC, and utcoffset is inaccurate).
259
+ # This piece of -redundant- data is just because Lumens can't make local_received query-able
260
+ event_timestamp=event['local_received'])
261
+
262
+ return format_kafka_message(
263
+ firex_id=self.firex_id,
264
+ event_data=event | basic_event_data,
265
+ uuid=uuid,
266
+ logs_url=self.logs_url,
267
+ submitter=self.submitter,
268
+ firex_requester=self.firex_requester)
269
+
270
+ def _send_celery_event_to_kafka(self, celery_event: dict[str, Any]) -> list[dict[str, Any]]:
271
+ if celery_event.get('type') in BLAZE_SEND_EVENT_TYPES:
272
+ try:
273
+ kafka_event = self._get_kafka_event(celery_event)
274
+ except NoNameForEvent as e:
275
+ logger.exception(e)
276
+ else:
277
+ send_kafka_mssg(kafka_producer=self.producer,
278
+ kafka_mssg=kafka_event,
279
+ kafka_topic=self.kafka_topic,
280
+ firex_id=self.firex_id,
281
+ partition=self.partition)
282
+ return [kafka_event]
283
+
284
+ return []
@@ -0,0 +1,67 @@
1
+ """
2
+ Utility functions for the firex_blaze package.
3
+ """
4
+ import os
5
+ from dataclasses import dataclass
6
+ import json
7
+ from typing import Optional
8
+
9
+ from celery.app.base import Celery
10
+
11
+ from firexapp.events.event_aggregator import FireXEventAggregator
12
+ from firexapp.broker_manager.broker_factory import RedisManager
13
+ from firex_blaze.fast_blaze_helper import get_blaze_dir
14
+
15
+ KAFKA_EVENTS_FILE_DELIMITER = '--END_OF_EVENT--'
16
+
17
+
18
+ @dataclass
19
+ class BlazeSenderConfig:
20
+ kafka_topic: str
21
+ kafka_bootstrap_servers: list[str]
22
+ max_kafka_connection_retries: int
23
+ security_protocol: str = 'PLAINTEXT'
24
+ # SASL-SSL OAuth 2.0 parameters
25
+ sasl_mechanism: Optional[str] = None
26
+ sasl_oauthbearer_method: Optional[str] = None
27
+ sasl_oauthbearer_client_id: Optional[str] = None
28
+ sasl_oauthbearer_client_secret: Optional[str] = None
29
+ sasl_oauthbearer_token_endpoint_url: Optional[str] = None
30
+ ssl_ca_location: Optional[str] = None
31
+
32
+
33
+ def get_blaze_events_file(logs_dir, instance_name=None):
34
+ return os.path.join(get_blaze_dir(logs_dir, instance_name), 'kafka_events.json')
35
+
36
+
37
+ def get_kafka_events(logs_dir, instance_name=None):
38
+ import gzip
39
+ real_rec = os.path.realpath(get_blaze_events_file(logs_dir, instance_name))
40
+ if real_rec.endswith('.gz'):
41
+ with gzip.open(real_rec, 'rt', encoding='utf-8') as rec:
42
+ all_text = rec.read()
43
+ else:
44
+ with open(real_rec) as rec:
45
+ all_text = rec.read()
46
+ event_records = all_text.split(sep=KAFKA_EVENTS_FILE_DELIMITER)
47
+ return [
48
+ json.loads(e) for e in event_records
49
+ if e
50
+ ]
51
+
52
+
53
+ def aggregate_blaze_kafka_msgs(firex_id, kafka_msgs):
54
+ event_aggregator = FireXEventAggregator()
55
+ for kafka_event in kafka_msgs:
56
+ if kafka_event['FIREX_ID'] == firex_id:
57
+ inner_event = kafka_event['EVENTS'][0]
58
+ celery_event = dict(inner_event['DATA'])
59
+ celery_event['uuid'] = inner_event['UUID']
60
+ event_aggregator.aggregate_events([celery_event])
61
+
62
+ return event_aggregator.tasks_by_uuid
63
+
64
+
65
+ def celery_app_from_logs_dir(logs_dir):
66
+ return Celery(broker=RedisManager.get_broker_url_from_logs_dir(logs_dir),
67
+ accept_content=['pickle', 'json'])
@@ -0,0 +1,144 @@
1
+ import os
2
+ from psutil import Process, TimeoutExpired
3
+ import subprocess
4
+ import time
5
+
6
+ from firexapp.submit.install_configs import FireXInstallConfigs
7
+ from firexapp.submit.submit import OptionalBoolean
8
+ from firexapp.submit.tracking_service import TrackingService
9
+ from firexapp.common import qualify_firex_bin, select_env_vars
10
+ from firexapp.submit.console import setup_console_logging
11
+
12
+ from firex_blaze.fast_blaze_helper import get_blaze_dir
13
+ from firexapp.discovery import PkgVersionInfo
14
+
15
+
16
+ logger = setup_console_logging(__name__)
17
+
18
+
19
+ class FireXBlazeLauncher(TrackingService):
20
+
21
+ instance_name = 'blaze'
22
+
23
+ def __init__(self):
24
+ self.broker_recv_ready_file = None
25
+ self.is_ready_for_tasks = False
26
+ self.stdout_file = None
27
+ self.start_time = None
28
+
29
+ def extra_cli_arguments(self, arg_parser):
30
+
31
+ arg_parser.add_argument('--disable_blaze', '-disable_blaze',
32
+ help='Disable blaze data collection', default=None, const=True, nargs='?',
33
+ action=OptionalBoolean)
34
+
35
+ arg_parser.add_argument('--blaze_logs_url',
36
+ help='Server URL from which logs can be fetched.',
37
+ default=None)
38
+
39
+ # TODO: consider sensible default values, or not launching subprocess when these required args aren't supplied.
40
+ arg_parser.add_argument('--blaze_kafka_topic',
41
+ help="Topic used for Blaze's Kafka bus",
42
+ default=None)
43
+
44
+ arg_parser.add_argument('--blaze_bootstrap_servers',
45
+ help='Comma separated list of Kafka bootrap servers.',
46
+ default=None)
47
+
48
+ arg_parser.add_argument('--blaze_security_protocol',
49
+ help='Protocol used to communicate with brokers. '
50
+ 'Valid values are: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL.',
51
+ default='PLAINTEXT')
52
+
53
+ # SASL-SSL OAuth 2.0 arguments
54
+ arg_parser.add_argument('--blaze_sasl_mechanism',
55
+ help='SASL mechanism to use (e.g., OAUTHBEARER).')
56
+ arg_parser.add_argument('--blaze_sasl_oauthbearer_method',
57
+ help='OAuth bearer method (e.g., oidc).')
58
+ arg_parser.add_argument('--blaze_sasl_oauthbearer_client_id',
59
+ help='OAuth client ID.')
60
+ arg_parser.add_argument('--blaze_sasl_oauthbearer_client_secret',
61
+ help='OAuth client secret.')
62
+ arg_parser.add_argument('--blaze_sasl_oauthbearer_token_endpoint_url',
63
+ help='OAuth token endpoint URL.')
64
+ arg_parser.add_argument('--blaze_ssl_ca_location',
65
+ help='CA certificate location for SSL verification.')
66
+
67
+ @classmethod
68
+ def _create_blaze_command(cls, uid, args, broker_recv_ready_file):
69
+ cmd = [qualify_firex_bin("firex_blaze"),
70
+ "--uid", str(uid),
71
+ "--firex_requester", uid.firex_requester,
72
+ "--logs_dir", uid.logs_dir,
73
+ "--broker_recv_ready_file", broker_recv_ready_file,
74
+ '--logs_url', uid.logs_url,
75
+ '--kafka_topic', args.blaze_kafka_topic,
76
+ '--bootstrap_servers', args.blaze_bootstrap_servers,
77
+ '--instance_name', cls.instance_name,
78
+ '--security_protocol', args.blaze_security_protocol]
79
+
80
+ # SASL-SSL OAuth arguments
81
+ if hasattr(args, 'blaze_sasl_mechanism') and args.blaze_sasl_mechanism:
82
+ cmd += ['--sasl_mechanism', args.blaze_sasl_mechanism]
83
+ if hasattr(args, 'blaze_sasl_oauthbearer_method') and args.blaze_sasl_oauthbearer_method:
84
+ cmd += ['--sasl_oauthbearer_method', args.blaze_sasl_oauthbearer_method]
85
+ if hasattr(args, 'blaze_sasl_oauthbearer_client_id') and args.blaze_sasl_oauthbearer_client_id:
86
+ cmd += ['--sasl_oauthbearer_client_id', args.blaze_sasl_oauthbearer_client_id]
87
+ if hasattr(args, 'blaze_sasl_oauthbearer_client_secret') and args.blaze_sasl_oauthbearer_client_secret:
88
+ cmd += ['--sasl_oauthbearer_client_secret', args.blaze_sasl_oauthbearer_client_secret]
89
+ if hasattr(args, 'blaze_sasl_oauthbearer_token_endpoint_url') and args.blaze_sasl_oauthbearer_token_endpoint_url:
90
+ cmd += ['--sasl_oauthbearer_token_endpoint_url', args.blaze_sasl_oauthbearer_token_endpoint_url]
91
+ if hasattr(args, 'blaze_ssl_ca_location') and args.blaze_ssl_ca_location:
92
+ cmd += ['--ssl_ca_location', args.blaze_ssl_ca_location]
93
+
94
+ return cmd
95
+
96
+ def start(self, args, install_configs: FireXInstallConfigs, uid=None, **kwargs) -> {}:
97
+ super().start(args, install_configs, uid=uid, **kwargs)
98
+ sufficient_args = uid.logs_url and args.blaze_kafka_topic and args.blaze_bootstrap_servers
99
+ if args.disable_blaze or not sufficient_args:
100
+ if args.disable_blaze:
101
+ logger.debug("Blaze disabled; will not launch subprocess.")
102
+ if not sufficient_args:
103
+ logger.warning("Blaze did not receive sufficient arguments; will not launch subprocess.")
104
+ self.is_ready_for_tasks = True
105
+ return {}
106
+
107
+ blaze_debug_dir = get_blaze_dir(uid.logs_dir, instance_name=self.instance_name)
108
+ os.makedirs(blaze_debug_dir, exist_ok=True)
109
+ self.broker_recv_ready_file = os.path.join(blaze_debug_dir, 'blaze_celery_recvr_ready')
110
+ self.stdout_file = os.path.join(blaze_debug_dir, 'blaze.stdout')
111
+
112
+ self.start_time = time.time()
113
+ with open(self.stdout_file, 'w+') as f:
114
+ pid = subprocess.Popen(
115
+ self._create_blaze_command(uid, args, self.broker_recv_ready_file),
116
+ stdout=f,
117
+ stderr=subprocess.STDOUT,
118
+ close_fds=True,
119
+ env=select_env_vars(['PATH']),
120
+ cwd=blaze_debug_dir,
121
+ ).pid
122
+
123
+ try:
124
+ Process(pid).wait(0.1)
125
+ except TimeoutExpired:
126
+ logger.debug("Started background FireXBlaze with pid %s" % pid)
127
+ else:
128
+ logger.error("Failed to start FireXBlaze -- task data will not be put on Kafka bus.")
129
+
130
+ return {}
131
+
132
+ def ready_for_tasks(self, **kwargs) -> bool:
133
+ if not self.is_ready_for_tasks:
134
+ self.is_ready_for_tasks = os.path.isfile(self.broker_recv_ready_file)
135
+ if self.is_ready_for_tasks:
136
+ logger.debug("Blaze up after %.2f s" % (time.time() - self.start_time))
137
+
138
+ return self.is_ready_for_tasks
139
+
140
+ @staticmethod
141
+ def get_pkg_version_info() -> PkgVersionInfo:
142
+ import firex_blaze
143
+ return PkgVersionInfo(pkg='firex-blaze',
144
+ version=firex_blaze.__version__)
@@ -0,0 +1,11 @@
1
+ # LOAD NO SLOW MODULES HERE!
2
+ # This is to keep loading the launcher module fast.
3
+ import os
4
+
5
+ from firexapp.submit.uid import Uid
6
+
7
+
8
+ def get_blaze_dir(logs_dir, instance_name=None):
9
+ if instance_name is None:
10
+ instance_name = 'blaze'
11
+ return os.path.join(logs_dir, Uid.debug_dirname, instance_name)
@@ -0,0 +1,5 @@
1
+ """CI-oriented FireX tasks distributed by the owning firexapp package."""
2
+
3
+ from firexapp import __version__
4
+
5
+ __all__ = ["__version__"]