manta-node 0.5b0.dev9__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 (42) hide show
  1. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +83 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +9 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/manager.py +536 -0
  22. manta_node/infrastructure/filesystem/__init__.py +7 -0
  23. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  24. manta_node/infrastructure/grpc/__init__.py +11 -0
  25. manta_node/infrastructure/grpc/client.py +373 -0
  26. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  27. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  28. manta_node/infrastructure/metrics/__init__.py +10 -0
  29. manta_node/infrastructure/metrics/collector.py +94 -0
  30. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  31. manta_node/infrastructure/mqtt/__init__.py +7 -0
  32. manta_node/infrastructure/mqtt/command_handler.py +315 -0
  33. manta_node/node_orchestrator.py +462 -0
  34. manta_node/task_manager.py +519 -0
  35. manta_node/tasks.py +272 -0
  36. manta_node/utils.py +52 -0
  37. manta_node-0.5b0.dev9.dist-info/METADATA +799 -0
  38. manta_node-0.5b0.dev9.dist-info/RECORD +42 -0
  39. manta_node-0.5b0.dev9.dist-info/WHEEL +5 -0
  40. manta_node-0.5b0.dev9.dist-info/entry_points.txt +2 -0
  41. manta_node-0.5b0.dev9.dist-info/licenses/LICENSE +683 -0
  42. manta_node-0.5b0.dev9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,462 @@
1
+ import asyncio
2
+ import socket
3
+ import struct
4
+ import sys
5
+ import uuid
6
+ from logging import getLogger
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Union
9
+
10
+ import blake3
11
+ from aiodocker.exceptions import DockerError
12
+ from docker.errors import DockerException
13
+ from grpclib.exceptions import GRPCError
14
+ from grpclib.server import Server
15
+
16
+ from manta_common.build.common.informations import (
17
+ NodeOverview,
18
+ NodeStatus,
19
+ NodeStatusEnum,
20
+ )
21
+ from manta_common.build.common.system import Empty
22
+ from manta_common.conversions import HEX_SIZE, ID, oid
23
+ from manta_common.errors import MantaError
24
+ from manta_common.traces import Tracer
25
+
26
+ # NEW - use domain logic
27
+
28
+ # In node_orchestrator.py
29
+ from .infrastructure.config.node_config_manager import NodeConfigManager
30
+ from .infrastructure.filesystem.dataset_manager import DatasetManager
31
+ from .infrastructure.grpc.client import NodeClient
32
+ from .infrastructure.grpc.local_servicer import LocalServicer
33
+ from .infrastructure.grpc.world_servicer import WorldServicer
34
+ from .infrastructure.metrics.metrics_collector import GenericMetricsCollector
35
+ from .infrastructure.mqtt.command_handler import CommandHandler
36
+
37
+ __all__ = ["Node"]
38
+
39
+
40
+ def make_id(random_id: bool, alias: Optional[str] = None) -> ID:
41
+ """
42
+ Returns the node ID
43
+
44
+ Parameters
45
+ ----------
46
+ random_id : bool
47
+ If :code:`True`, the ID is randomized
48
+ alias : Optional[str]
49
+ Alias name of the node
50
+
51
+ Returns
52
+ -------
53
+ ID
54
+ Node ID
55
+ """
56
+ if alias is not None:
57
+ return ID(oid(alias))
58
+ elif random_id:
59
+ return ID(oid(uuid.uuid4().hex))
60
+ else:
61
+ return ID(
62
+ oid(blake3.blake3(struct.pack("Q", uuid.getnode())).hexdigest(HEX_SIZE))
63
+ )
64
+
65
+
66
+ def find_available_port(start_port: int = 50051, max_attempts: int = 100) -> int:
67
+ """
68
+ Find an available port for the light service.
69
+
70
+ Parameters
71
+ ----------
72
+ start_port : int
73
+ The port to start searching from (default: 50051)
74
+ max_attempts : int
75
+ Maximum number of ports to try (default: 100)
76
+
77
+ Returns
78
+ -------
79
+ int
80
+ An available port number
81
+
82
+ Raises
83
+ ------
84
+ RuntimeError
85
+ If no available port is found after max_attempts
86
+ """
87
+ for port_offset in range(max_attempts):
88
+ port = start_port + port_offset
89
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
90
+ try:
91
+ # Try to bind to the port
92
+ sock.bind(("", port))
93
+ sock.close()
94
+ return port
95
+ except OSError:
96
+ # Port is in use, try the next one
97
+ sock.close()
98
+ continue
99
+
100
+ raise RuntimeError(
101
+ f"Could not find an available port in range {start_port}-{start_port + max_attempts - 1}"
102
+ )
103
+
104
+
105
+ class Node:
106
+ """
107
+ Node class runs on embedded device and executes tasks requested
108
+ by the Manager
109
+
110
+ It is composed of several components :
111
+
112
+ * the :code:`CommandHandler` which deploys tasks in containers \
113
+ and manages tasks given available resources
114
+ * the :code:`LocalServicer` to allow containers to access local data on the device
115
+ * the :code:`WorldServicer` to allow containers to access data stored in the Manager
116
+ * the :code:`MQTTClient` for gathering MQTT messages from the Manager
117
+ * the :code:`DatasetManager` for handling datasets and providing metadata
118
+
119
+ Parameters
120
+ ----------
121
+ config : Union[NodeConfigManager, str, Path]
122
+ NodeConfigManager instance or path to configuration file.
123
+ If string/Path is provided, a NodeConfigManager will be created.
124
+ """
125
+
126
+ __slots__ = [
127
+ "config_manager",
128
+ "config",
129
+ "node_id",
130
+ "node_client",
131
+ "task_runner",
132
+ "command_handler",
133
+ "server",
134
+ "tracer",
135
+ "cert_folder",
136
+ "dataset_manager",
137
+ "metrics_collector",
138
+ "command_loop",
139
+ "light_server_task",
140
+ ]
141
+
142
+ def __init__(self, config: Union[NodeConfigManager, str, Path]):
143
+ # Initialize configuration manager
144
+ if isinstance(config, NodeConfigManager):
145
+ self.config_manager = config
146
+ else:
147
+ # Create NodeConfigManager from path
148
+ self.config_manager = NodeConfigManager(config)
149
+
150
+ # Get configuration
151
+ self.config = self.config_manager.config
152
+
153
+ # Validate configuration
154
+ issues = self.config_manager.validate()
155
+ if issues:
156
+ raise MantaError(f"Configuration validation failed: {'; '.join(issues)}")
157
+
158
+ # Initialize tracer with logging configuration
159
+ self.tracer = Tracer(getLogger(__name__), None)
160
+
161
+ # Certificate folder setup (if TLS enabled)
162
+ if self.config.security.use_tls:
163
+ self.cert_folder = Path(self.config.security.cert_folder).expanduser()
164
+ else:
165
+ self.cert_folder = None
166
+
167
+ self.tracer.info(f"Configuration: {self.config.metadata.name}")
168
+
169
+ # Initialize the dataset manager with configuration
170
+ self.dataset_manager = DatasetManager(self.config.datasets.mappings)
171
+
172
+ # Validate dataset paths exist and are accessible
173
+ dataset_issues = self.dataset_manager.validate_datasets()
174
+ if dataset_issues:
175
+ error_msg = "Dataset validation failed:\n - " + "\n - ".join(
176
+ dataset_issues
177
+ )
178
+ self.tracer.error(error_msg)
179
+ raise MantaError(error_msg)
180
+
181
+ # Initialize the metrics collector
182
+ self.metrics_collector = GenericMetricsCollector()
183
+
184
+ # Initialize node client with configuration
185
+ self.node_client = NodeClient(
186
+ manager_host=self.config.network.manager_host,
187
+ manager_port=self.config.network.manager_port,
188
+ secured_token=self.config.identity.secured_token,
189
+ ssl_cert_folder=self.cert_folder,
190
+ )
191
+
192
+ # Initialize the command handler, the command loop and light server task
193
+ self.command_handler = None
194
+ self.command_loop = None
195
+ self.light_server_task = None
196
+
197
+ # Register configuration observer for runtime updates
198
+ self.config_manager.register_observer(self._on_config_change)
199
+
200
+ def _ensure_light_service_port(self) -> None:
201
+ """
202
+ Ensure light service port is assigned.
203
+
204
+ If not specified in configuration, automatically finds an available port.
205
+ Logs the assigned port for debugging purposes.
206
+ """
207
+ if self.config.network.light_service_port is None:
208
+ self.tracer.info(
209
+ "Light service port not specified, finding an available port..."
210
+ )
211
+ self.config.network.light_service_port = find_available_port()
212
+ self.tracer.info(
213
+ f"Automatically assigned light service port: {self.config.network.light_service_port}"
214
+ )
215
+
216
+ def _on_config_change(self, change: Dict[str, Any]) -> None:
217
+ """Handle configuration changes at runtime.
218
+
219
+ Parameters
220
+ ----------
221
+ change : Dict[str, Any]
222
+ Dictionary describing the configuration change
223
+ """
224
+ self.tracer.info(f"Configuration changed: {change}")
225
+
226
+ # Handle safe runtime updates
227
+ if change.get("action") == "update":
228
+ updates = change.get("updates", {})
229
+
230
+ # Update logging level if changed
231
+ if "logging.level" in updates:
232
+ import logging
233
+
234
+ logging.getLogger().setLevel(updates["logging.level"])
235
+ self.tracer.info(f"Updated logging level to {updates['logging.level']}")
236
+
237
+ # Update dataset mappings if changed
238
+ if "datasets.mappings" in updates:
239
+ self.dataset_manager.update_datasets(updates["datasets.mappings"])
240
+ self.tracer.info("Updated dataset mappings")
241
+
242
+ # Update resource limits if changed
243
+ if any(key.startswith("resources.") for key in updates):
244
+ if self.command_handler:
245
+ # Command handler can update resource limits
246
+ self.tracer.info("Resource limits updated")
247
+
248
+ async def check_manager_availability(self) -> None:
249
+ """
250
+ Check if the manager is available.
251
+
252
+ If secured mode is activated, this function will get the
253
+ certificates from the manager.
254
+ Then, it will check if the manager is available by calling
255
+ the `is_available` function of the node client.
256
+ If the manager is not available, it will log an error and
257
+ exit with a non-zero status code.
258
+
259
+ Raises
260
+ ------
261
+ SystemExit
262
+ If the manager is not available.
263
+ """
264
+ # Log endpoint details before attempting connection
265
+ security_mode = (
266
+ "TLS"
267
+ if (self.cert_folder is not None or self.config.network.manager_port == 443)
268
+ else "insecure"
269
+ )
270
+ endpoint = f"{security_mode}://{self.config.network.manager_host}:{self.config.network.manager_port}"
271
+
272
+ self.tracer.info(f"Checking manager availability at {endpoint}")
273
+
274
+ try:
275
+ # If secured mode, get the certificates
276
+ # if (
277
+ # self.secured_token
278
+ # ): # TODO: refactor with node bootstrap certificate and new identity service in node servicer
279
+ # raise NotImplementedError("Secured mode is not implemented yet")
280
+ await self.node_client.is_available(Empty())
281
+ self.tracer.info(f"Manager is available at {endpoint}")
282
+ except (GRPCError, ConnectionRefusedError) as error:
283
+ message = f"Manager is not available at {endpoint}: {error}"
284
+ self.tracer.exception(message)
285
+
286
+ # Provide troubleshooting guidance
287
+ troubleshooting = (
288
+ f"\n[ERROR] {message}\n"
289
+ f"\nTroubleshooting steps:\n"
290
+ f" 1. Verify endpoint: {endpoint}\n"
291
+ f" 2. Check if manager services are running\n"
292
+ f" 3. Verify JWT_SECRET matches between node and manager\n"
293
+ f" 4. Check network connectivity to {self.config.network.manager_host}:{self.config.network.manager_port}\n"
294
+ )
295
+ sys.exit(troubleshooting)
296
+ except MantaError as manta_error:
297
+ message = f"Manager authentication failed at {endpoint}: {manta_error}"
298
+ self.tracer.exception(message)
299
+
300
+ # Provide specific guidance for authentication errors
301
+ troubleshooting = (
302
+ f"\n[ERROR] {message}\n"
303
+ f"\nAuthentication issue detected:\n"
304
+ f" - Endpoint: {endpoint}\n"
305
+ f" - This usually means JWT_SECRET mismatch between node and manager\n"
306
+ f" - Verify secured_token in node config matches manager's JWT_SECRET\n"
307
+ f" - Check token format: should be a valid JWT with 3 parts (header.payload.signature)\n"
308
+ )
309
+ sys.exit(troubleshooting)
310
+ except Exception as exc:
311
+ message = f"Unknown error connecting to {endpoint}: {exc}"
312
+ self.tracer.exception(message)
313
+ sys.exit(f"[ERROR] {message}")
314
+
315
+ async def start(self) -> None:
316
+ """
317
+ Start the Node:
318
+
319
+ - Register with the Manager
320
+ - Start the MQTT client
321
+ - Subscribe to topics
322
+ - Start the Light Server
323
+ - Start the MQTT client loop
324
+
325
+ Raises
326
+ ------
327
+ SystemExit
328
+ If MQTT client unreacheable or gRPC error occurs
329
+ """
330
+ await self.check_manager_availability()
331
+ try:
332
+ identification = NodeOverview(
333
+ self.metrics_collector.get_platform_info(),
334
+ self.dataset_manager.to_proto(),
335
+ )
336
+ registration = await self.node_client.register_node(identification)
337
+
338
+ # Initialize, start and prepare MQTT Client
339
+ self.tracer.debug(f"Registration: {registration}")
340
+ mqtt_broker_host = registration.mqtt_broker_host
341
+ mqtt_broker_port = registration.mqtt_broker_port
342
+ topics = registration.mqtt_topics
343
+ node_token = registration.node_token
344
+
345
+ # Update MQTT configuration with registration data
346
+ self.config.mqtt.broker_host = mqtt_broker_host
347
+ self.config.mqtt.broker_port = mqtt_broker_port
348
+ self.config.mqtt.topics = topics
349
+
350
+ # Save the node token to config file
351
+ if not node_token:
352
+ error_msg = "Manager did not return a node token during registration"
353
+ self.tracer.error(error_msg)
354
+ raise MantaError(error_msg)
355
+
356
+ self.tracer.info("Received node token from manager, updating config")
357
+ self.config.identity.secured_token = node_token
358
+ self.config_manager.save()
359
+
360
+ # Reinitialize the node client with the new token
361
+ self.node_client = NodeClient(
362
+ manager_host=self.config.network.manager_host,
363
+ manager_port=self.config.network.manager_port,
364
+ secured_token=node_token,
365
+ ssl_cert_folder=self.cert_folder,
366
+ )
367
+
368
+ # Ensure light service port is assigned
369
+ self._ensure_light_service_port()
370
+
371
+ # Initialize the command handler with configuration
372
+ self.tracer.info("Starting Command Handler")
373
+ self.command_handler = CommandHandler(
374
+ node_client=self.node_client,
375
+ mqtt_broker_host=mqtt_broker_host,
376
+ mqtt_broker_port=mqtt_broker_port,
377
+ light_service_port=self.config.network.light_service_port or 50052,
378
+ topics=topics,
379
+ dataset_manager=self.dataset_manager,
380
+ metrics_collector=self.metrics_collector,
381
+ stop_callback=self.stop,
382
+ ssl_cert_folder=self.cert_folder,
383
+ # Pass additional configuration
384
+ ping_interval=self.config.mqtt.ping_interval,
385
+ max_concurrent_tasks=self.config.tasks.max_concurrent,
386
+ task_timeout=self.config.tasks.task_timeout,
387
+ )
388
+
389
+ # Start the server for docker communications through
390
+ # manta light with manta node
391
+ self.tracer.info(
392
+ f"Starting Light Server at {self.config.network.light_service_host}:"
393
+ f"{self.config.network.light_service_port}"
394
+ )
395
+ local_servicer = LocalServicer(
396
+ self.node_client,
397
+ self.dataset_manager,
398
+ )
399
+ world_servicer = WorldServicer(
400
+ self.node_client,
401
+ self.command_handler.tasks,
402
+ )
403
+ self.server = Server([local_servicer, world_servicer])
404
+ await self.server.start(
405
+ self.config.network.light_service_host,
406
+ self.config.network.light_service_port,
407
+ )
408
+
409
+ # Create asyncio tasks and start them
410
+ self.command_loop = asyncio.create_task(self.command_handler.main())
411
+ self.light_server_task = asyncio.create_task(self.server.wait_closed())
412
+ await asyncio.gather(self.command_loop, self.light_server_task)
413
+ except ConnectionRefusedError:
414
+ self.tracer.exception("Unable to start and connect to MQTT Message Broker")
415
+ sys.exit("[ERROR] Unable to start and connect to MQTT Message Broker")
416
+ except GRPCError:
417
+ self.tracer.exception("Unable to register to the Manager")
418
+ sys.exit("[ERROR] Unable to register to the Manager")
419
+ except OSError as os_error:
420
+ self.tracer.exception(f"OS error: {os_error}")
421
+ await self.stop()
422
+ sys.exit(f"[ERROR] OS error: {os_error}")
423
+ except (DockerException, DockerError) as docker_error:
424
+ self.tracer.exception(f"Docker is not available: {docker_error}")
425
+ await self.stop()
426
+ sys.exit(f"[ERROR] Docker is not available: {docker_error}")
427
+ except Exception as exc:
428
+ self.tracer.exception(f"Unknown error: {exc}")
429
+ await self.stop()
430
+ sys.exit(f"[ERROR] Unknown error: {exc}")
431
+
432
+ async def stop(self) -> None:
433
+ """
434
+ Stop the node
435
+ """
436
+ self.tracer.info("Stopping node")
437
+ if (
438
+ self.command_handler is not None
439
+ and self.command_loop is not None
440
+ and not self.command_loop.cancelled()
441
+ ):
442
+ self.tracer.info("Publishing node status to manager")
443
+ await self.command_handler.client.publish(
444
+ "manager/set_node_status",
445
+ NodeStatus(status=NodeStatusEnum.DISCONNECTED).SerializeToString(),
446
+ )
447
+
448
+ if self.command_handler is not None:
449
+ self.tracer.info("Cleaning up command handler")
450
+ await self.command_handler.clean()
451
+
452
+ if self.command_loop is not None:
453
+ self.tracer.info("Stopping command loop")
454
+ self.command_loop.cancel()
455
+
456
+ if self.light_server_task is not None:
457
+ self.tracer.info("Stopping light server task")
458
+ self.light_server_task.cancel()
459
+
460
+ # if self.server is not None:
461
+ # self.tracer.info("Stopping light server")
462
+ # self.server.close()