nost-tools 2.2.0__py3-none-any.whl → 2.3.0__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.
Potentially problematic release.
This version of nost-tools might be problematic. Click here for more details.
- nost_tools/__init__.py +1 -1
- nost_tools/application.py +120 -50
- nost_tools/configuration.py +9 -7
- nost_tools/managed_application.py +50 -41
- nost_tools/manager.py +78 -69
- nost_tools/schemas.py +42 -7
- nost_tools/simulator.py +1 -3
- {nost_tools-2.2.0.dist-info → nost_tools-2.3.0.dist-info}/METADATA +1 -1
- nost_tools-2.3.0.dist-info/RECORD +18 -0
- nost_tools-2.2.0.dist-info/RECORD +0 -18
- {nost_tools-2.2.0.dist-info → nost_tools-2.3.0.dist-info}/WHEEL +0 -0
- {nost_tools-2.2.0.dist-info → nost_tools-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {nost_tools-2.2.0.dist-info → nost_tools-2.3.0.dist-info}/top_level.txt +0 -0
nost_tools/__init__.py
CHANGED
nost_tools/application.py
CHANGED
|
@@ -48,13 +48,19 @@ class Application:
|
|
|
48
48
|
time_status_init (:obj:`datetime`): Scenario time of first time status message
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
-
def __init__(
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
app_name: str,
|
|
54
|
+
app_description: str = None,
|
|
55
|
+
setup_signal_handlers: bool = True,
|
|
56
|
+
):
|
|
52
57
|
"""
|
|
53
58
|
Initializes a new application.
|
|
54
59
|
|
|
55
60
|
Args:
|
|
56
61
|
app_name (str): application name
|
|
57
62
|
app_description (str): application description (optional)
|
|
63
|
+
setup_signal_handlers (bool): whether to set up signal handlers (default: True)
|
|
58
64
|
"""
|
|
59
65
|
self.simulator = Simulator()
|
|
60
66
|
self.connection = None
|
|
@@ -85,8 +91,12 @@ class Application:
|
|
|
85
91
|
self._token_refresh_thread = None
|
|
86
92
|
self.token_refresh_interval = None
|
|
87
93
|
self._reconnect_delay = None
|
|
94
|
+
# Offset
|
|
95
|
+
self._wallclock_refresh_thread = None
|
|
96
|
+
self.wallclock_offset_refresh_interval = None
|
|
88
97
|
# Set up signal handlers for graceful shutdown
|
|
89
|
-
|
|
98
|
+
if setup_signal_handlers:
|
|
99
|
+
self._setup_signal_handlers()
|
|
90
100
|
|
|
91
101
|
def _setup_signal_handlers(self):
|
|
92
102
|
"""
|
|
@@ -194,6 +204,42 @@ class Application:
|
|
|
194
204
|
self._token_refresh_thread.start()
|
|
195
205
|
logger.debug("Starting refresh token thread successfully completed.")
|
|
196
206
|
|
|
207
|
+
def start_wallclock_refresh_thread(self): # , interval=30, host="pool.ntp.org"):
|
|
208
|
+
"""
|
|
209
|
+
Starts a background thread to refresh the wallclock offset periodically.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
interval (int): Seconds between wallclock offset refreshes (default: 3600 seconds/1 hour)
|
|
213
|
+
host (str): NTP host to query (default: 'pool.ntp.org')
|
|
214
|
+
"""
|
|
215
|
+
logger.debug("Starting wallclock offset refresh thread.")
|
|
216
|
+
|
|
217
|
+
def refresh_wallclock_periodically():
|
|
218
|
+
while not self._should_stop.wait(
|
|
219
|
+
timeout=self.config.rc.wallclock_offset_properties.wallclock_offset_refresh_interval
|
|
220
|
+
):
|
|
221
|
+
logger.debug("Wallclock refresh thread is running.")
|
|
222
|
+
try:
|
|
223
|
+
logger.info(
|
|
224
|
+
f"Contacting {self.config.rc.wallclock_offset_properties.ntp_host} to retrieve wallclock offset."
|
|
225
|
+
)
|
|
226
|
+
response = ntplib.NTPClient().request(
|
|
227
|
+
self.config.rc.wallclock_offset_properties.ntp_host,
|
|
228
|
+
version=3,
|
|
229
|
+
timeout=2,
|
|
230
|
+
)
|
|
231
|
+
offset = timedelta(seconds=response.offset)
|
|
232
|
+
self.simulator.set_wallclock_offset(offset)
|
|
233
|
+
logger.info(f"Wallclock offset updated to {offset}.")
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.debug(f"Failed to refresh wallclock offset: {e}")
|
|
236
|
+
|
|
237
|
+
self._wallclock_refresh_thread = threading.Thread(
|
|
238
|
+
target=refresh_wallclock_periodically
|
|
239
|
+
)
|
|
240
|
+
self._wallclock_refresh_thread.start()
|
|
241
|
+
logger.debug("Starting wallclock offset refresh thread successfully completed.")
|
|
242
|
+
|
|
197
243
|
def update_connection_credentials(self, access_token):
|
|
198
244
|
"""
|
|
199
245
|
Updates the connection credentials with the new access token.
|
|
@@ -203,14 +249,33 @@ class Application:
|
|
|
203
249
|
"""
|
|
204
250
|
self.connection.update_secret(access_token, "secret")
|
|
205
251
|
|
|
252
|
+
def _get_parameters_from_config(self):
|
|
253
|
+
"""
|
|
254
|
+
Gets application parameters from configuration or returns None if not available.
|
|
255
|
+
This method can be overridden by subclasses to customize parameter retrieval.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
object: Configuration parameters object or None
|
|
259
|
+
"""
|
|
260
|
+
if self.config and self.config.rc.yaml_file:
|
|
261
|
+
try:
|
|
262
|
+
return getattr(
|
|
263
|
+
self.config.rc.simulation_configuration.execution_parameters,
|
|
264
|
+
"application",
|
|
265
|
+
None,
|
|
266
|
+
)
|
|
267
|
+
except (AttributeError, KeyError):
|
|
268
|
+
return None
|
|
269
|
+
return None
|
|
270
|
+
|
|
206
271
|
def start_up(
|
|
207
272
|
self,
|
|
208
273
|
prefix: str,
|
|
209
274
|
config: ConnectionConfig,
|
|
210
|
-
set_offset: bool =
|
|
275
|
+
set_offset: bool = True,
|
|
211
276
|
time_status_step: timedelta = None,
|
|
212
277
|
time_status_init: datetime = None,
|
|
213
|
-
shut_down_when_terminated: bool =
|
|
278
|
+
shut_down_when_terminated: bool = False,
|
|
214
279
|
) -> None:
|
|
215
280
|
"""
|
|
216
281
|
Starts up the application to prepare for scenario execution.
|
|
@@ -225,31 +290,45 @@ class Application:
|
|
|
225
290
|
time_status_init (:obj:`datetime`): scenario time for first time status message
|
|
226
291
|
shut_down_when_terminated (bool): True, if the application should shut down when the simulation is terminated
|
|
227
292
|
"""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
293
|
+
self.config = config
|
|
294
|
+
|
|
295
|
+
if self.config.rc.yaml_file:
|
|
296
|
+
logger.info(
|
|
297
|
+
f"Collecting start up parameters from YAML configuration file: {self.config.rc.yaml_file}"
|
|
298
|
+
)
|
|
299
|
+
parameters = self._get_parameters_from_config()
|
|
300
|
+
if parameters:
|
|
301
|
+
self.set_offset = getattr(parameters, "set_offset", set_offset)
|
|
302
|
+
self.time_status_step = getattr(
|
|
303
|
+
parameters, "time_status_step", time_status_step
|
|
304
|
+
)
|
|
305
|
+
self.time_status_init = getattr(
|
|
306
|
+
parameters, "time_status_init", time_status_init
|
|
307
|
+
)
|
|
308
|
+
self.shut_down_when_terminated = getattr(
|
|
309
|
+
parameters, "shut_down_when_terminated", shut_down_when_terminated
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
logger.warning("No parameters found in configuration, using defaults")
|
|
313
|
+
self.set_offset = set_offset
|
|
314
|
+
self.time_status_step = time_status_step
|
|
315
|
+
self.time_status_init = time_status_init
|
|
316
|
+
self.shut_down_when_terminated = shut_down_when_terminated
|
|
317
|
+
else:
|
|
318
|
+
logger.info(
|
|
319
|
+
f"Collecting start up parameters from user input or default values."
|
|
320
|
+
)
|
|
234
321
|
self.set_offset = set_offset
|
|
235
322
|
self.time_status_step = time_status_step
|
|
236
323
|
self.time_status_init = time_status_init
|
|
237
324
|
self.shut_down_when_terminated = shut_down_when_terminated
|
|
238
|
-
else:
|
|
239
|
-
self.config = config
|
|
240
|
-
parameters = getattr(
|
|
241
|
-
self.config.rc.simulation_configuration.execution_parameters,
|
|
242
|
-
"application",
|
|
243
|
-
None,
|
|
244
|
-
)
|
|
245
|
-
self.set_offset = parameters.set_offset
|
|
246
|
-
self.time_status_step = parameters.time_status_step
|
|
247
|
-
self.time_status_init = parameters.time_status_init
|
|
248
|
-
self.shut_down_when_terminated = parameters.shut_down_when_terminated
|
|
249
325
|
|
|
250
326
|
if self.set_offset:
|
|
251
|
-
#
|
|
252
|
-
|
|
327
|
+
# Start periodic wallclock offset updates instead of one-time call
|
|
328
|
+
logger.info(
|
|
329
|
+
f"Wallclock offset will be set every {self.config.rc.wallclock_offset_properties.wallclock_offset_refresh_interval} seconds using {self.config.rc.wallclock_offset_properties.ntp_host}."
|
|
330
|
+
)
|
|
331
|
+
self.start_wallclock_refresh_thread()
|
|
253
332
|
|
|
254
333
|
# Set the prefix and configuration parameters
|
|
255
334
|
self.prefix = prefix
|
|
@@ -290,7 +369,6 @@ class Application:
|
|
|
290
369
|
locale=config.rc.server_configuration.servers.rabbitmq.locale,
|
|
291
370
|
blocked_connection_timeout=config.rc.server_configuration.servers.rabbitmq.blocked_connection_timeout,
|
|
292
371
|
)
|
|
293
|
-
logger.info(parameters)
|
|
294
372
|
|
|
295
373
|
# Configure transport layer security (TLS) if needed
|
|
296
374
|
if self.config.rc.server_configuration.servers.rabbitmq.tls:
|
|
@@ -1348,34 +1426,26 @@ class Application:
|
|
|
1348
1426
|
)
|
|
1349
1427
|
else:
|
|
1350
1428
|
logger.info("Closing token refresh thread completed successfully")
|
|
1351
|
-
|
|
1429
|
+
# Also stop wallclock refresh thread if it exists
|
|
1430
|
+
if (
|
|
1431
|
+
hasattr(self, "_wallclock_refresh_thread")
|
|
1432
|
+
and self._wallclock_refresh_thread
|
|
1433
|
+
and self._wallclock_refresh_thread.is_alive()
|
|
1434
|
+
):
|
|
1435
|
+
logger.info("Closing wallclock refresh thread.")
|
|
1436
|
+
# Set a timeout to avoid hanging indefinitely
|
|
1437
|
+
self._wallclock_refresh_thread.join(timeout=60.0)
|
|
1438
|
+
# Check if it's still alive after timeout
|
|
1439
|
+
if self._wallclock_refresh_thread.is_alive():
|
|
1440
|
+
logger.warning(
|
|
1441
|
+
"Closing wallclock refresh thread timed out after 60 seconds. "
|
|
1442
|
+
)
|
|
1443
|
+
else:
|
|
1444
|
+
logger.info(
|
|
1445
|
+
"Closing wallclock refresh thread completed successfully"
|
|
1446
|
+
)
|
|
1352
1447
|
logger.debug("Stop_application completed successfully.")
|
|
1353
1448
|
|
|
1354
|
-
def set_wallclock_offset(
|
|
1355
|
-
self, host="pool.ntp.org", retry_delay_s: int = 5, max_retry: int = 5
|
|
1356
|
-
) -> None:
|
|
1357
|
-
"""
|
|
1358
|
-
Issues a Network Time Protocol (NTP) request to determine the system clock offset.
|
|
1359
|
-
|
|
1360
|
-
Args:
|
|
1361
|
-
host (str): NTP host (default: 'pool.ntp.org')
|
|
1362
|
-
retry_delay_s (int): number of seconds to wait before retrying
|
|
1363
|
-
max_retry (int): maximum number of retries allowed
|
|
1364
|
-
"""
|
|
1365
|
-
for i in range(max_retry):
|
|
1366
|
-
try:
|
|
1367
|
-
logger.info(f"Contacting {host} to retrieve wallclock offset.")
|
|
1368
|
-
response = ntplib.NTPClient().request(host, version=3, timeout=2)
|
|
1369
|
-
offset = timedelta(seconds=response.offset)
|
|
1370
|
-
self.simulator.set_wallclock_offset(offset)
|
|
1371
|
-
logger.info(f"Wallclock offset updated to {offset}.")
|
|
1372
|
-
return
|
|
1373
|
-
except ntplib.NTPException:
|
|
1374
|
-
logger.warning(
|
|
1375
|
-
f"Could not connect to {host}, attempt #{i+1}/{max_retry} in {retry_delay_s} s."
|
|
1376
|
-
)
|
|
1377
|
-
time.sleep(retry_delay_s)
|
|
1378
|
-
|
|
1379
1449
|
def _create_time_status_publisher(
|
|
1380
1450
|
self, time_status_step: timedelta, time_status_init: datetime
|
|
1381
1451
|
) -> None:
|
nost_tools/configuration.py
CHANGED
|
@@ -21,6 +21,7 @@ from .schemas import (
|
|
|
21
21
|
RuntimeConfig,
|
|
22
22
|
ServersConfig,
|
|
23
23
|
SimulationConfig,
|
|
24
|
+
WallclockOffsetProperties,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -52,6 +53,7 @@ class ConnectionConfig:
|
|
|
52
53
|
password: str = None,
|
|
53
54
|
rabbitmq_host: str = None,
|
|
54
55
|
rabbitmq_port: int = None,
|
|
56
|
+
keycloak_authentication: bool = False,
|
|
55
57
|
keycloak_host: str = None,
|
|
56
58
|
keycloak_port: int = None,
|
|
57
59
|
keycloak_realm: str = None,
|
|
@@ -70,6 +72,7 @@ class ConnectionConfig:
|
|
|
70
72
|
password (str): client password, provided by NOS-T operator
|
|
71
73
|
host (str): broker hostname
|
|
72
74
|
rabbitmq_port (int): RabbitMQ broker port number
|
|
75
|
+
keycloak_authentication (bool): True, if Keycloak IAM authentication is used
|
|
73
76
|
keycloak_port (int): Keycloak IAM port number
|
|
74
77
|
keycloak_realm (str): Keycloak realm name
|
|
75
78
|
client_id (str): Keycloak client ID
|
|
@@ -84,6 +87,7 @@ class ConnectionConfig:
|
|
|
84
87
|
self.rabbitmq_host = rabbitmq_host
|
|
85
88
|
self.keycloak_host = keycloak_host
|
|
86
89
|
self.rabbitmq_port = rabbitmq_port
|
|
90
|
+
self.keycloak_authentication = keycloak_authentication
|
|
87
91
|
self.keycloak_port = keycloak_port
|
|
88
92
|
self.keycloak_realm = keycloak_realm
|
|
89
93
|
self.client_id = client_id
|
|
@@ -283,6 +287,7 @@ class ConnectionConfig:
|
|
|
283
287
|
port=self.rabbitmq_port,
|
|
284
288
|
virtual_host=self.virtual_host,
|
|
285
289
|
tls=self.is_tls,
|
|
290
|
+
keycloak_authentication=self.keycloak_authentication,
|
|
286
291
|
),
|
|
287
292
|
keycloak=KeycloakConfig(
|
|
288
293
|
host=self.keycloak_host,
|
|
@@ -312,13 +317,8 @@ class ConnectionConfig:
|
|
|
312
317
|
del server_config.execution
|
|
313
318
|
self.server_config = server_config
|
|
314
319
|
|
|
315
|
-
if
|
|
316
|
-
|
|
317
|
-
and self.password is not None
|
|
318
|
-
and self.client_id is not None
|
|
319
|
-
and self.client_secret_key is not None
|
|
320
|
-
):
|
|
321
|
-
logger.info("Using provided credentials.")
|
|
320
|
+
if self.username is not None and self.password is not None:
|
|
321
|
+
logger.info("Using user-provided credentials.")
|
|
322
322
|
self.credentials_config = Credentials(
|
|
323
323
|
username=self.username,
|
|
324
324
|
password=self.password,
|
|
@@ -329,8 +329,10 @@ class ConnectionConfig:
|
|
|
329
329
|
self.load_environment_variables()
|
|
330
330
|
|
|
331
331
|
self.rc = RuntimeConfig(
|
|
332
|
+
wallclock_offset_properties=WallclockOffsetProperties(),
|
|
332
333
|
credentials=self.credentials_config,
|
|
333
334
|
server_configuration=server_config,
|
|
334
335
|
simulation_configuration=self.simulation_config,
|
|
335
336
|
application_configuration=self.app_specific,
|
|
337
|
+
yaml_file=self.yaml_file,
|
|
336
338
|
)
|
|
@@ -32,27 +32,57 @@ class ManagedApplication(Application):
|
|
|
32
32
|
time_step (:obj:`timedelta`): scenario time step used in execution
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
def __init__(
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
app_name: str,
|
|
38
|
+
app_description: str = None,
|
|
39
|
+
setup_signal_handlers: bool = True,
|
|
40
|
+
):
|
|
36
41
|
"""
|
|
37
42
|
Initializes a new managed application.
|
|
38
43
|
|
|
39
44
|
Args:
|
|
40
45
|
app_name (str): application name
|
|
41
46
|
app_description (str): application description
|
|
47
|
+
setup_signal_handlers (bool): whether to set up signal handlers (default: True)
|
|
42
48
|
"""
|
|
43
|
-
super().__init__(
|
|
49
|
+
super().__init__(
|
|
50
|
+
app_name, app_description, setup_signal_handlers=setup_signal_handlers
|
|
51
|
+
)
|
|
44
52
|
self.time_step = None
|
|
45
53
|
self._sim_start_time = None
|
|
46
54
|
self._sim_stop_time = None
|
|
47
55
|
|
|
56
|
+
def _get_parameters_from_config(self):
|
|
57
|
+
"""
|
|
58
|
+
Override to get parameters specific to managed applications
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
object: Configuration parameters for this managed application
|
|
62
|
+
"""
|
|
63
|
+
if self.config and self.config.rc.yaml_file:
|
|
64
|
+
try:
|
|
65
|
+
parameters = (
|
|
66
|
+
self.config.rc.simulation_configuration.execution_parameters.managed_applications
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
# Try to get app-specific parameters
|
|
70
|
+
return parameters[self.app_name]
|
|
71
|
+
except KeyError:
|
|
72
|
+
# Fall back to default parameters
|
|
73
|
+
return parameters.get("default")
|
|
74
|
+
except (AttributeError, KeyError):
|
|
75
|
+
return None
|
|
76
|
+
return None
|
|
77
|
+
|
|
48
78
|
def start_up(
|
|
49
79
|
self,
|
|
50
80
|
prefix: str,
|
|
51
81
|
config: ConnectionConfig,
|
|
52
|
-
set_offset: bool =
|
|
82
|
+
set_offset: bool = True,
|
|
53
83
|
time_status_step: timedelta = None,
|
|
54
84
|
time_status_init: datetime = None,
|
|
55
|
-
shut_down_when_terminated: bool =
|
|
85
|
+
shut_down_when_terminated: bool = False,
|
|
56
86
|
time_step: timedelta = None,
|
|
57
87
|
manager_app_name: str = None,
|
|
58
88
|
) -> None:
|
|
@@ -70,48 +100,27 @@ class ManagedApplication(Application):
|
|
|
70
100
|
time_step (:obj:`timedelta`): scenario time step used in execution (Default: 1 second)
|
|
71
101
|
manager_app_name (str): manager application name (Default: manager)
|
|
72
102
|
"""
|
|
73
|
-
|
|
74
|
-
set_offset is not None
|
|
75
|
-
and time_status_step is not None
|
|
76
|
-
and time_status_init is not None
|
|
77
|
-
and shut_down_when_terminated is not None
|
|
78
|
-
and time_step is not None
|
|
79
|
-
and manager_app_name is not None
|
|
80
|
-
):
|
|
81
|
-
self.set_offset = set_offset
|
|
82
|
-
self.time_status_step = time_status_step
|
|
83
|
-
self.time_status_init = time_status_init
|
|
84
|
-
self.shut_down_when_terminated = shut_down_when_terminated
|
|
85
|
-
self.time_step = time_step
|
|
86
|
-
self.manager_app_name = manager_app_name
|
|
87
|
-
else:
|
|
88
|
-
self.config = config
|
|
89
|
-
parameters = (
|
|
90
|
-
self.config.rc.simulation_configuration.execution_parameters.managed_applications
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
parameters = parameters[self.app_name]
|
|
95
|
-
except KeyError:
|
|
96
|
-
parameters = parameters["default"]
|
|
97
|
-
self.set_offset = parameters.set_offset
|
|
98
|
-
self.time_status_step = parameters.time_status_step
|
|
99
|
-
self.time_status_init = parameters.time_status_init
|
|
100
|
-
self.shut_down_when_terminated = parameters.shut_down_when_terminated
|
|
101
|
-
self.time_step = parameters.time_step
|
|
102
|
-
self.manager_app_name = parameters.manager_app_name
|
|
103
|
+
self.config = config
|
|
103
104
|
|
|
104
|
-
#
|
|
105
|
+
# Call base start_up to handle common parameters
|
|
105
106
|
super().start_up(
|
|
106
107
|
prefix,
|
|
107
108
|
config,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
set_offset,
|
|
110
|
+
time_status_step,
|
|
111
|
+
time_status_init,
|
|
112
|
+
shut_down_when_terminated,
|
|
112
113
|
)
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
|
|
115
|
+
# Get additional parameters specific to managed applications
|
|
116
|
+
if self.config.rc.yaml_file:
|
|
117
|
+
parameters = self._get_parameters_from_config()
|
|
118
|
+
if parameters:
|
|
119
|
+
self.time_step = parameters.time_step
|
|
120
|
+
self.manager_app_name = parameters.manager_app_name
|
|
121
|
+
else:
|
|
122
|
+
self.time_step = time_step
|
|
123
|
+
self.manager_app_name = manager_app_name
|
|
115
124
|
|
|
116
125
|
# Register callback functions
|
|
117
126
|
self.add_message_callback(
|
nost_tools/manager.py
CHANGED
|
@@ -67,12 +67,22 @@ class Manager(Application):
|
|
|
67
67
|
required_apps_status (dict): Ready status for all required applications
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
|
-
def __init__(
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
app_name: str = "manager",
|
|
73
|
+
app_description: str = None,
|
|
74
|
+
setup_signal_handlers: bool = True,
|
|
75
|
+
):
|
|
71
76
|
"""
|
|
72
77
|
Initializes a new manager.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
setup_signal_handlers (bool): whether to set up signal handlers (default: True)
|
|
73
81
|
"""
|
|
74
82
|
# call super class constructor
|
|
75
|
-
super().__init__(
|
|
83
|
+
super().__init__(
|
|
84
|
+
app_name, app_description, setup_signal_handlers=setup_signal_handlers
|
|
85
|
+
)
|
|
76
86
|
self.required_apps_status = {}
|
|
77
87
|
|
|
78
88
|
self.sim_start_time = None
|
|
@@ -129,16 +139,32 @@ class Manager(Application):
|
|
|
129
139
|
f"Heartbeat check: {remaining:.2f} seconds remaining in sleep"
|
|
130
140
|
)
|
|
131
141
|
|
|
142
|
+
def _get_parameters_from_config(self):
|
|
143
|
+
"""
|
|
144
|
+
Override to get parameters specific to manager application
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
object: Configuration parameters for the manager application
|
|
148
|
+
"""
|
|
149
|
+
if self.config and self.config.rc.yaml_file:
|
|
150
|
+
try:
|
|
151
|
+
return getattr(
|
|
152
|
+
self.config.rc.simulation_configuration.execution_parameters,
|
|
153
|
+
"manager",
|
|
154
|
+
None,
|
|
155
|
+
)
|
|
156
|
+
except (AttributeError, KeyError):
|
|
157
|
+
return None
|
|
158
|
+
return None
|
|
159
|
+
|
|
132
160
|
def start_up(
|
|
133
161
|
self,
|
|
134
162
|
prefix: str,
|
|
135
163
|
config: ConnectionConfig,
|
|
136
|
-
set_offset: bool =
|
|
164
|
+
set_offset: bool = True,
|
|
137
165
|
time_status_step: timedelta = None,
|
|
138
166
|
time_status_init: datetime = None,
|
|
139
|
-
shut_down_when_terminated: bool =
|
|
140
|
-
time_step: timedelta = None,
|
|
141
|
-
manager_app_name: str = None,
|
|
167
|
+
shut_down_when_terminated: bool = False,
|
|
142
168
|
) -> None:
|
|
143
169
|
"""
|
|
144
170
|
Starts up the application by connecting to message broker, starting a background event loop,
|
|
@@ -151,45 +177,20 @@ class Manager(Application):
|
|
|
151
177
|
time_status_step (:obj:`timedelta`): scenario duration between time status messages
|
|
152
178
|
time_status_init (:obj:`datetime`): scenario time for first time status message
|
|
153
179
|
shut_down_when_terminated (bool): True, if the application should shut down when the simulation is terminated
|
|
154
|
-
time_step (:obj:`timedelta`): scenario time step used in execution (Default: 1 second)
|
|
155
|
-
manager_app_name (str): manager application name (Default: manager)
|
|
156
180
|
"""
|
|
157
|
-
|
|
158
|
-
set_offset is not None
|
|
159
|
-
and time_status_step is not None
|
|
160
|
-
and time_status_init is not None
|
|
161
|
-
and shut_down_when_terminated is not None
|
|
162
|
-
and time_step is not None
|
|
163
|
-
and manager_app_name is not None
|
|
164
|
-
):
|
|
165
|
-
self.set_offset = set_offset
|
|
166
|
-
self.time_status_step = time_status_step
|
|
167
|
-
self.time_status_init = time_status_init
|
|
168
|
-
self.shut_down_when_terminated = shut_down_when_terminated
|
|
169
|
-
self.time_step = time_step
|
|
170
|
-
self.manager_app_name = manager_app_name
|
|
171
|
-
else:
|
|
172
|
-
self.config = config
|
|
173
|
-
parameters = (
|
|
174
|
-
self.config.rc.simulation_configuration.execution_parameters.manager
|
|
175
|
-
)
|
|
176
|
-
self.set_offset = parameters.set_offset
|
|
177
|
-
self.time_status_step = parameters.time_status_step
|
|
178
|
-
self.time_status_init = parameters.time_status_init
|
|
179
|
-
self.shut_down_when_terminated = parameters.shut_down_when_terminated
|
|
180
|
-
self.time_step = parameters.time_step
|
|
181
|
+
self.config = config
|
|
181
182
|
|
|
182
|
-
#
|
|
183
|
+
# Call base start_up to handle common parameters
|
|
183
184
|
super().start_up(
|
|
184
185
|
prefix,
|
|
185
186
|
config,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
set_offset,
|
|
188
|
+
time_status_step,
|
|
189
|
+
time_status_init,
|
|
190
|
+
shut_down_when_terminated,
|
|
190
191
|
)
|
|
191
192
|
|
|
192
|
-
#
|
|
193
|
+
# Additional manager-specific setup: establish the exchange
|
|
193
194
|
self.establish_exchange()
|
|
194
195
|
|
|
195
196
|
def execute_test_plan(self, *args, **kwargs) -> None:
|
|
@@ -241,8 +242,33 @@ class Manager(Application):
|
|
|
241
242
|
init_retry_delay_s (float): number of seconds to wait between initialization commands while waiting for required applications
|
|
242
243
|
init_max_retry (int): number of initialization commands while waiting for required applications before continuing to execution
|
|
243
244
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
if self.config.rc.yaml_file:
|
|
246
|
+
logger.info(
|
|
247
|
+
f"Collecting execution parameters from YAML configuration file: {self.config.rc.yaml_file}"
|
|
248
|
+
)
|
|
249
|
+
parameters = getattr(
|
|
250
|
+
self.config.rc.simulation_configuration.execution_parameters,
|
|
251
|
+
self.app_name,
|
|
252
|
+
None,
|
|
253
|
+
)
|
|
254
|
+
self.sim_start_time = parameters.sim_start_time
|
|
255
|
+
self.sim_stop_time = parameters.sim_stop_time
|
|
256
|
+
self.start_time = parameters.start_time
|
|
257
|
+
self.time_step = parameters.time_step
|
|
258
|
+
self.time_scale_factor = parameters.time_scale_factor
|
|
259
|
+
self.time_scale_updates = parameters.time_scale_updates
|
|
260
|
+
self.time_status_step = parameters.time_status_step
|
|
261
|
+
self.time_status_init = parameters.time_status_init
|
|
262
|
+
self.command_lead = parameters.command_lead
|
|
263
|
+
self.required_apps = [
|
|
264
|
+
app for app in parameters.required_apps if app != self.app_name
|
|
265
|
+
]
|
|
266
|
+
self.init_retry_delay_s = parameters.init_retry_delay_s
|
|
267
|
+
self.init_max_retry = parameters.init_max_retry
|
|
268
|
+
else:
|
|
269
|
+
logger.info(
|
|
270
|
+
f"Collecting execution parameters from user input or default values."
|
|
271
|
+
)
|
|
246
272
|
self.sim_start_time = sim_start_time
|
|
247
273
|
self.sim_stop_time = sim_stop_time
|
|
248
274
|
self.start_time = start_time
|
|
@@ -255,36 +281,10 @@ class Manager(Application):
|
|
|
255
281
|
self.required_apps = required_apps
|
|
256
282
|
self.init_retry_delay_s = init_retry_delay_s
|
|
257
283
|
self.init_max_retry = init_max_retry
|
|
258
|
-
else:
|
|
259
|
-
if self.config.rc:
|
|
260
|
-
logger.info("Retrieving execution parameters from YAML file.")
|
|
261
|
-
parameters = getattr(
|
|
262
|
-
self.config.rc.simulation_configuration.execution_parameters,
|
|
263
|
-
self.app_name,
|
|
264
|
-
None,
|
|
265
|
-
)
|
|
266
|
-
self.sim_start_time = parameters.sim_start_time
|
|
267
|
-
self.sim_stop_time = parameters.sim_stop_time
|
|
268
|
-
self.start_time = parameters.start_time
|
|
269
|
-
self.time_step = parameters.time_step
|
|
270
|
-
self.time_scale_factor = parameters.time_scale_factor
|
|
271
|
-
self.time_scale_updates = parameters.time_scale_updates
|
|
272
|
-
self.time_status_step = parameters.time_status_step
|
|
273
|
-
self.time_status_init = parameters.time_status_init
|
|
274
|
-
self.command_lead = parameters.command_lead
|
|
275
|
-
self.required_apps = [
|
|
276
|
-
app for app in parameters.required_apps if app != self.app_name
|
|
277
|
-
]
|
|
278
|
-
self.init_retry_delay_s = parameters.init_retry_delay_s
|
|
279
|
-
self.init_max_retry = parameters.init_max_retry
|
|
280
|
-
else:
|
|
281
|
-
raise ValueError(
|
|
282
|
-
"No configuration runtime. Please provide simulation start and stop times."
|
|
283
|
-
)
|
|
284
284
|
|
|
285
285
|
# Convert TimeScaleUpdateSchema objects to TimeScaleUpdate objects
|
|
286
286
|
converted_updates = []
|
|
287
|
-
for update_schema in
|
|
287
|
+
for update_schema in self.time_scale_updates:
|
|
288
288
|
converted_updates.append(
|
|
289
289
|
TimeScaleUpdate(
|
|
290
290
|
time_scale_factor=update_schema.time_scale_factor,
|
|
@@ -563,8 +563,17 @@ class Manager(Application):
|
|
|
563
563
|
app_topics="stop",
|
|
564
564
|
payload=command.model_dump_json(by_alias=True),
|
|
565
565
|
)
|
|
566
|
-
|
|
567
|
-
|
|
566
|
+
|
|
567
|
+
# Update the execution end time if simulator is in EXECUTING mode
|
|
568
|
+
if self.simulator.get_mode() == Mode.EXECUTING:
|
|
569
|
+
try:
|
|
570
|
+
self.simulator.set_end_time(sim_stop_time)
|
|
571
|
+
except RuntimeError as e:
|
|
572
|
+
logger.warning(f"Could not set simulator end time: {e}")
|
|
573
|
+
else:
|
|
574
|
+
logger.debug(
|
|
575
|
+
"Skipping setting simulator end time as simulator is not in EXECUTING mode"
|
|
576
|
+
)
|
|
568
577
|
|
|
569
578
|
def update(self, time_scale_factor: float, sim_update_time: datetime) -> None:
|
|
570
579
|
"""
|
nost_tools/schemas.py
CHANGED
|
@@ -307,8 +307,21 @@ class ServersConfig(BaseModel):
|
|
|
307
307
|
return values
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
class WallclockOffsetProperties(BaseModel):
|
|
311
|
+
"""
|
|
312
|
+
Properties to report wallclock offset.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
wallclock_offset_refresh_interval: Optional[int] = Field(
|
|
316
|
+
10800, description="Wallclock offset refresh interval, in seconds."
|
|
317
|
+
)
|
|
318
|
+
ntp_host: Optional[str] = Field(
|
|
319
|
+
"pool.ntp.org", description="NTP host for wallclock offset synchronization."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
310
323
|
class GeneralConfig(BaseModel):
|
|
311
|
-
prefix: str = Field("nost", description="Execution prefix.")
|
|
324
|
+
prefix: Optional[str] = Field("nost", description="Execution prefix.")
|
|
312
325
|
|
|
313
326
|
|
|
314
327
|
class TimeScaleUpdateSchema(BaseModel):
|
|
@@ -352,6 +365,10 @@ class ManagerConfig(BaseModel):
|
|
|
352
365
|
shut_down_when_terminated: bool = Field(
|
|
353
366
|
False, description="Shut down when terminated."
|
|
354
367
|
)
|
|
368
|
+
is_scenario_time_step: bool = Field(
|
|
369
|
+
True,
|
|
370
|
+
description="If True, time_step is in scenario time and won't be scaled. If False, time_step is in wallclock time and will be scaled by the time scale factor.",
|
|
371
|
+
)
|
|
355
372
|
is_scenario_time_status_step: bool = Field(
|
|
356
373
|
True,
|
|
357
374
|
description="If True, time_status_step is in scenario time and won't be scaled. If False, time_status_step is in wallclock time and will be scaled by the time scale factor.",
|
|
@@ -361,6 +378,16 @@ class ManagerConfig(BaseModel):
|
|
|
361
378
|
def scale_time(cls, values):
|
|
362
379
|
time_scale_factor = values.get("time_scale_factor", 1.0)
|
|
363
380
|
|
|
381
|
+
if "time_step" in values and not values.get("is_scenario_time_step", True):
|
|
382
|
+
time_step = values["time_step"]
|
|
383
|
+
if isinstance(time_step, str):
|
|
384
|
+
hours, minutes, seconds = map(int, time_step.split(":"))
|
|
385
|
+
time_step = timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
|
386
|
+
if isinstance(time_step, timedelta):
|
|
387
|
+
values["time_step"] = timedelta(
|
|
388
|
+
seconds=time_step.total_seconds() * time_scale_factor
|
|
389
|
+
)
|
|
390
|
+
|
|
364
391
|
if "time_status_step" in values and not values.get(
|
|
365
392
|
"is_scenario_time_status_step", True
|
|
366
393
|
):
|
|
@@ -440,7 +467,7 @@ class LoggerApplicationConfig(BaseModel):
|
|
|
440
467
|
timedelta(seconds=10), description="Time status step."
|
|
441
468
|
)
|
|
442
469
|
time_status_init: Optional[datetime] = Field(
|
|
443
|
-
datetime(
|
|
470
|
+
datetime.now(), description="Time status init."
|
|
444
471
|
)
|
|
445
472
|
shut_down_when_terminated: Optional[bool] = Field(
|
|
446
473
|
False, description="Shut down when terminated."
|
|
@@ -471,7 +498,9 @@ class ApplicationConfig(BaseModel):
|
|
|
471
498
|
|
|
472
499
|
|
|
473
500
|
class ExecConfig(BaseModel):
|
|
474
|
-
general: GeneralConfig
|
|
501
|
+
general: Optional[GeneralConfig] = Field(
|
|
502
|
+
None, description="General configuration for the execution."
|
|
503
|
+
)
|
|
475
504
|
manager: Optional[ManagerConfig] = Field(None, description="Manager configuration.")
|
|
476
505
|
managed_applications: Optional[Dict[str, ManagedApplicationConfig]] = Field(
|
|
477
506
|
default_factory=lambda: {"default": ManagedApplicationConfig()},
|
|
@@ -509,11 +538,11 @@ class ChannelConfig(BaseModel):
|
|
|
509
538
|
|
|
510
539
|
|
|
511
540
|
class Credentials(BaseModel):
|
|
512
|
-
username: str = Field(
|
|
513
|
-
password: str = Field(
|
|
514
|
-
client_id: Optional[str] = Field(
|
|
541
|
+
username: Optional[str] = Field("admin", description="Username for authentication.")
|
|
542
|
+
password: Optional[str] = Field("admin", description="Password for authentication.")
|
|
543
|
+
client_id: Optional[str] = Field(None, description="Client ID for authentication.")
|
|
515
544
|
client_secret_key: Optional[str] = Field(
|
|
516
|
-
|
|
545
|
+
None, description="Client secret key for authentication."
|
|
517
546
|
)
|
|
518
547
|
|
|
519
548
|
|
|
@@ -529,6 +558,9 @@ class SimulationConfig(BaseModel):
|
|
|
529
558
|
|
|
530
559
|
|
|
531
560
|
class RuntimeConfig(BaseModel):
|
|
561
|
+
wallclock_offset_properties: WallclockOffsetProperties = Field(
|
|
562
|
+
..., description="Properties for wallclock offset."
|
|
563
|
+
)
|
|
532
564
|
credentials: Credentials = Field(..., description="Credentials for authentication.")
|
|
533
565
|
server_configuration: Config = (
|
|
534
566
|
Field(..., description="Simulation configuration."),
|
|
@@ -539,3 +571,6 @@ class RuntimeConfig(BaseModel):
|
|
|
539
571
|
application_configuration: Optional[Dict] = Field(
|
|
540
572
|
None, description="Application-specific, user-provided configuration."
|
|
541
573
|
)
|
|
574
|
+
yaml_file: Optional[str] = Field(
|
|
575
|
+
None, description="Path to the YAML file containing the configuration."
|
|
576
|
+
)
|
nost_tools/simulator.py
CHANGED
|
@@ -516,9 +516,7 @@ class Simulator(Observable):
|
|
|
516
516
|
Args:
|
|
517
517
|
wallclock_offset(:obj:`timedelta`): difference between system clock and trusted wallclock source
|
|
518
518
|
"""
|
|
519
|
-
if self._mode == Mode.
|
|
520
|
-
raise RuntimeError("Cannot set wallclock offset: simulator is executing")
|
|
521
|
-
elif self._mode == Mode.TERMINATING:
|
|
519
|
+
if self._mode == Mode.TERMINATING:
|
|
522
520
|
raise RuntimeError("Cannot set wallclock offset: simulator is terminating")
|
|
523
521
|
self._wallclock_offset = wallclock_offset
|
|
524
522
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nost_tools
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Tools for Novel Observing Strategies Testbed (NOS-T) Applications
|
|
5
5
|
Author-email: "Paul T. Grogan" <paul.grogan@asu.edu>, "Emmanuel M. Gonzalez" <emmanuelgonzalez@asu.edu>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
nost_tools/__init__.py,sha256=5DeYQxWXibC9y__BvjwL8YC8K4EYMZbAV6E75k5nuL8,870
|
|
2
|
+
nost_tools/application.py,sha256=gh48fokVifuhKyAoJylZ2mF_WHW90G9L8j99Ct7j2Pg,64257
|
|
3
|
+
nost_tools/application_utils.py,sha256=jiMzuuP6-47UlUO64HhwNvbl6uKvVnsksYgOw7CmxL4,9327
|
|
4
|
+
nost_tools/configuration.py,sha256=4WLs1BrHMMvVhSIpJfjVZe-zw04WygAzjiLX2pVXibY,13146
|
|
5
|
+
nost_tools/entity.py,sha256=JrSN5rz7h-N9zIQsaJOQVBIYPDfygacedks55fsq_QQ,2802
|
|
6
|
+
nost_tools/errors.py,sha256=0JcDlMEkZAya3-5c0rRozLuxp8qF58StG4JgRsaxfKU,344
|
|
7
|
+
nost_tools/logger_application.py,sha256=rxPBfyA7Zym5b_EsoSJvT9JWNIVWZX1a-4czFwCqaQ4,7217
|
|
8
|
+
nost_tools/managed_application.py,sha256=Xa6qGsNVrr_XNnpJt_-L5PVnpeUy7GXIf0p2aEI4dSE,11673
|
|
9
|
+
nost_tools/manager.py,sha256=KqFyE-vv9dPRuVdz_r1SAsPeWt9LwKOTF-a2cJId0rY,24321
|
|
10
|
+
nost_tools/observer.py,sha256=D64V0KTvHRPEqbB8q3BosJhoAlpBah2vyBlVbxWQR44,8161
|
|
11
|
+
nost_tools/publisher.py,sha256=omU8tb0AXnA6RfhYSh0vnXbJtrRo4ukx1J5ANl4bDLQ,5291
|
|
12
|
+
nost_tools/schemas.py,sha256=RZH9LCWSZT8hGteI_Cc5a_w_TyFYx0Kh5mBM02SObrk,20575
|
|
13
|
+
nost_tools/simulator.py,sha256=pWfMSarMCuInQTlvlJ5l53w5ZZP6jjyUtY8uWOkbe-4,20062
|
|
14
|
+
nost_tools-2.3.0.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
|
|
15
|
+
nost_tools-2.3.0.dist-info/METADATA,sha256=6ovwTQnDSv0ODsIsMQcFI5-kN652WW8FYOYlwzg-SH8,4256
|
|
16
|
+
nost_tools-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
nost_tools-2.3.0.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
|
|
18
|
+
nost_tools-2.3.0.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
nost_tools/__init__.py,sha256=4AGyjwpOcyyQSwtFNemFAQYyZyoRUJbH5dEkJ75xxCI,870
|
|
2
|
-
nost_tools/application.py,sha256=1msGzjDbPNY5Bv5OPu0pUctBEpXVBCkfBzjpF8G6skg,60914
|
|
3
|
-
nost_tools/application_utils.py,sha256=jiMzuuP6-47UlUO64HhwNvbl6uKvVnsksYgOw7CmxL4,9327
|
|
4
|
-
nost_tools/configuration.py,sha256=DBsISmhtMiWUw2PC4CfvIcfPOh_ayujFt5gif_2PvOI,12852
|
|
5
|
-
nost_tools/entity.py,sha256=JrSN5rz7h-N9zIQsaJOQVBIYPDfygacedks55fsq_QQ,2802
|
|
6
|
-
nost_tools/errors.py,sha256=0JcDlMEkZAya3-5c0rRozLuxp8qF58StG4JgRsaxfKU,344
|
|
7
|
-
nost_tools/logger_application.py,sha256=rxPBfyA7Zym5b_EsoSJvT9JWNIVWZX1a-4czFwCqaQ4,7217
|
|
8
|
-
nost_tools/managed_application.py,sha256=7393kCF9FeXlf_pSP_C-tAeNSW9_-smQH6pQgyHC6Cw,11576
|
|
9
|
-
nost_tools/manager.py,sha256=SrGu3TX8cQnvSPzJC5W5izZPJe9beQPQIKyT7PHwepQ,24485
|
|
10
|
-
nost_tools/observer.py,sha256=D64V0KTvHRPEqbB8q3BosJhoAlpBah2vyBlVbxWQR44,8161
|
|
11
|
-
nost_tools/publisher.py,sha256=omU8tb0AXnA6RfhYSh0vnXbJtrRo4ukx1J5ANl4bDLQ,5291
|
|
12
|
-
nost_tools/schemas.py,sha256=VAwHuIxVVbf-Te_CWkZ6iNvJwRvr9PYJRRD2dt0nMJ4,19068
|
|
13
|
-
nost_tools/simulator.py,sha256=ALnGDmnA_ga-1Lq-bVWi2vcrspgjS4vtuDE0jWsI7fE,20191
|
|
14
|
-
nost_tools-2.2.0.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
|
|
15
|
-
nost_tools-2.2.0.dist-info/METADATA,sha256=Mvx484RUws2yo8PdAZPnCIgx0l2ssODBeJkQucB-Xuk,4256
|
|
16
|
-
nost_tools-2.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
-
nost_tools-2.2.0.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
|
|
18
|
-
nost_tools-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|