primitive 0.2.10__py3-none-any.whl → 0.2.12__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.
@@ -2,178 +2,238 @@ import os
2
2
  import configparser
3
3
  import subprocess
4
4
  from pathlib import Path
5
+ from loguru import logger
6
+ from ..utils.daemons import Daemon
5
7
 
6
8
  HOME_DIRECTORY = Path.home()
7
-
8
9
  PRIMITIVE_BINARY_PATH = Path(HOME_DIRECTORY / ".pyenv" / "shims" / "primitive")
9
10
 
10
- PRIMITIVE_AGENT_SERVICE = "tech.primitive.agent.service"
11
- PRIMITIVE_AGENT_LOGS = "tech.primitive.agent.log"
12
- PRIMITIVE_AGENT_SERVICE_FILEPATH = Path(
13
- HOME_DIRECTORY / ".config" / "systemd" / "user" / PRIMITIVE_AGENT_SERVICE
14
- )
15
- PRIMITIVE_AGENT_LOGS_FILEPATH = Path(
16
- HOME_DIRECTORY / ".cache" / "primitive" / PRIMITIVE_AGENT_LOGS
17
- )
18
-
19
-
20
- def stop_service():
21
- try:
22
- if is_service_active():
23
- stop_existing_service = f"systemctl --user stop {PRIMITIVE_AGENT_SERVICE}"
24
- subprocess.check_output(stop_existing_service.split(" "))
25
- return True
26
- except subprocess.CalledProcessError as exception:
27
- print("stop_service: ", exception)
28
- return False
29
-
30
-
31
- def is_service_active():
32
- try:
33
- is_service_active = (
34
- f"systemctl --user show {PRIMITIVE_AGENT_SERVICE} -p ActiveState --value"
11
+
12
+ class LaunchService(Daemon):
13
+ def __init__(self, label: str):
14
+ self.label = label
15
+ self.name = label.split(".")[-1]
16
+
17
+ @property
18
+ def service_name(self) -> str:
19
+ return f"{self.label}.service"
20
+
21
+ @property
22
+ def file_path(self) -> Path:
23
+ return Path(HOME_DIRECTORY / ".config" / "systemd" / "user" / self.service_name)
24
+
25
+ @property
26
+ def logs(self) -> Path:
27
+ return Path(HOME_DIRECTORY / ".cache" / "primitive" / f"{self.label}.log")
28
+
29
+ def stop(self) -> bool:
30
+ try:
31
+ if self.is_active():
32
+ stop_existing_service = f"systemctl --user stop {self.service_name}"
33
+ subprocess.check_output(
34
+ stop_existing_service.split(" "), stderr=subprocess.DEVNULL
35
+ )
36
+ logger.info(f":white_check_mark: {self.label} stopped successfully!")
37
+ return True
38
+ except subprocess.CalledProcessError as exception:
39
+ if exception.returncode == 4:
40
+ logger.debug(f"{self.label} is not running or does not exist.")
41
+ return True
42
+ else:
43
+ logger.error(f"Unable to stop {self.label}, {exception.returncode}")
44
+ logger.error(exception)
45
+ return False
46
+
47
+ def start(self) -> bool:
48
+ try:
49
+ start_new_service = f"systemctl --user start {self.service_name}"
50
+ subprocess.check_output(start_new_service.split(" "))
51
+ logger.info(f":white_check_mark: {self.label} started successfully!")
52
+ return True
53
+ except subprocess.CalledProcessError as exception:
54
+ logger.error(f"Unable to start {self.label}")
55
+ logger.error(exception)
56
+ return False
57
+
58
+ def disable(self) -> bool:
59
+ try:
60
+ if self.is_installed():
61
+ disable_existing_service = (
62
+ f"systemctl --user disable {self.service_name}"
63
+ )
64
+ subprocess.check_output(
65
+ disable_existing_service.split(" "), stderr=subprocess.DEVNULL
66
+ )
67
+ return True
68
+ except subprocess.CalledProcessError as exception:
69
+ logger.error(f"Unable to disable {self.label}")
70
+ logger.error(exception)
71
+ return False
72
+
73
+ def enable(self) -> bool:
74
+ try:
75
+ enable_service = f"systemctl --user enable {self.service_name}"
76
+ subprocess.check_output(
77
+ enable_service.split(" "), stderr=subprocess.DEVNULL
78
+ )
79
+ return True
80
+ except subprocess.CalledProcessError as exception:
81
+ logger.error(f"Unable to enable {self.label}")
82
+ logger.error(exception)
83
+ return False
84
+
85
+ def verify(self) -> bool:
86
+ systemctl_check = (
87
+ f"systemctl --user show {self.service_name} -p CanStart --value"
35
88
  )
36
- output = subprocess.check_output(is_service_active.split(" ")).decode().strip()
37
- return output == "active"
38
- except subprocess.CalledProcessError as exception:
39
- print("is_service_active: ", exception)
40
- return False
41
-
42
-
43
- def disable_service():
44
- try:
45
- if is_service_enabled():
46
- disable_existing_service = (
47
- f"systemctl --user disable {PRIMITIVE_AGENT_SERVICE}"
89
+ try:
90
+ output = (
91
+ subprocess.check_output(systemctl_check.split(" ")).decode().strip()
92
+ )
93
+ if output == "no":
94
+ raise Exception(f"{systemctl_check} yielded {output}")
95
+ return True
96
+ except subprocess.CalledProcessError as exception:
97
+ logger.error(f"Unable to verify {self.label}")
98
+ logger.error(exception)
99
+ return False
100
+
101
+ def view_logs(self) -> None:
102
+ follow_logs = f"tail -f -n +1 {self.logs}"
103
+ os.system(follow_logs)
104
+
105
+ def populate(self) -> bool:
106
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
107
+ self.file_path.touch()
108
+
109
+ if self.file_path.exists():
110
+ self.file_path.unlink()
111
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
112
+ self.file_path.touch()
113
+
114
+ config = configparser.ConfigParser()
115
+ config.optionxform = str # type: ignore
116
+
117
+ config["Unit"] = {
118
+ "Description": "Primitive Agent",
119
+ "After": "network.target",
120
+ }
121
+
122
+ found_primitive_binary_path = PRIMITIVE_BINARY_PATH
123
+ if not PRIMITIVE_BINARY_PATH.exists():
124
+ result = subprocess.run(["which", "primitive"], capture_output=True)
125
+ if result.returncode == 0:
126
+ found_primitive_binary_path = result.stdout.decode().rstrip("\n")
127
+ else:
128
+ print("primitive binary not found")
129
+ return False
130
+
131
+ config["Service"] = {
132
+ "ExecStart": f'/bin/sh -lc "{found_primitive_binary_path} agent"',
133
+ "Restart": "always",
134
+ "StandardError": f"append:{self.logs}",
135
+ "StandardOutput": f"append:{self.logs}",
136
+ }
137
+
138
+ config["Install"] = {
139
+ "WantedBy": "default.target",
140
+ }
141
+
142
+ try:
143
+ with open(self.file_path, "w") as service_file:
144
+ config.write(service_file)
145
+ except IOError as exception:
146
+ print(f"populate_service_file: {exception}")
147
+
148
+ self.file_path.chmod(0o644)
149
+ return self.verify()
150
+
151
+ def create_stdout_file(self) -> bool:
152
+ try:
153
+ if not self.logs.exists():
154
+ self.logs.parent.mkdir(parents=True, exist_ok=True)
155
+ self.logs.touch()
156
+
157
+ return True
158
+ except Exception as e:
159
+ logger.error(
160
+ f"Unable to create log file at {self.logs} for daemon {self.label}"
161
+ )
162
+ logger.error(e)
163
+ return False
164
+
165
+ def delete_stdout_file(self) -> bool:
166
+ try:
167
+ if self.logs.exists():
168
+ self.logs.unlink()
169
+
170
+ return True
171
+ except Exception as e:
172
+ logger.error(
173
+ f"Unable to delete log file at {self.logs} for daemon {self.label}"
48
174
  )
49
- subprocess.check_output(disable_existing_service.split(" "))
50
- return True
51
- except subprocess.CalledProcessError as exception:
52
- print("disable_service: ", exception)
53
- return False
175
+ logger.error(e)
176
+ return False
177
+
178
+ def delete_service_file(self) -> bool:
179
+ try:
180
+ if self.file_path.exists():
181
+ self.file_path.unlink()
182
+
183
+ return True
184
+ except Exception as e:
185
+ logger.error(
186
+ f"Unable to delete service file at {self.file_path} for daemon {self.label}"
187
+ )
188
+ logger.error(e)
189
+ return False
54
190
 
191
+ def install(self) -> bool:
192
+ return all(
193
+ [
194
+ self.stop(),
195
+ self.disable(),
196
+ self.populate(),
197
+ self.create_stdout_file(),
198
+ self.enable(),
199
+ self.start(),
200
+ ]
201
+ )
55
202
 
56
- def is_service_enabled():
57
- try:
58
- is_service_active = (
59
- f"systemctl --user show {PRIMITIVE_AGENT_SERVICE} -p UnitFileState --value" # noqa
203
+ def uninstall(self) -> bool:
204
+ return all(
205
+ [
206
+ self.stop(),
207
+ self.disable(),
208
+ self.delete_service_file(),
209
+ self.delete_stdout_file(),
210
+ ]
60
211
  )
61
- output = subprocess.check_output(is_service_active.split(" ")).decode().strip()
62
- return output == "enabled"
63
- except subprocess.CalledProcessError as exception:
64
- print("is_service_enabled: ", exception)
65
- return False
66
-
67
-
68
- def populate_service_file():
69
- PRIMITIVE_AGENT_LOGS_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
70
- PRIMITIVE_AGENT_LOGS_FILEPATH.touch()
71
-
72
- if PRIMITIVE_AGENT_SERVICE_FILEPATH.exists():
73
- PRIMITIVE_AGENT_SERVICE_FILEPATH.unlink()
74
- PRIMITIVE_AGENT_SERVICE_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
75
- PRIMITIVE_AGENT_SERVICE_FILEPATH.touch()
76
-
77
- config = configparser.ConfigParser()
78
- config.optionxform = str # type: ignore
79
-
80
- config["Unit"] = {
81
- "Description": "Primitive Agent",
82
- "After": "network.target",
83
- }
84
-
85
- found_primitive_binary_path = PRIMITIVE_BINARY_PATH
86
- if not PRIMITIVE_BINARY_PATH.exists():
87
- result = subprocess.run(["which", "primitive"], capture_output=True)
88
- if result.returncode == 0:
89
- found_primitive_binary_path = result.stdout.decode().rstrip("\n")
90
- else:
91
- print("primitive binary not found")
212
+
213
+ def is_active(self) -> bool:
214
+ try:
215
+ is_service_active = (
216
+ f"systemctl --user show {self.service_name} -p ActiveState --value"
217
+ )
218
+ output = (
219
+ subprocess.check_output(is_service_active.split(" ")).decode().strip()
220
+ )
221
+ return output == "active"
222
+ except subprocess.CalledProcessError as exception:
223
+ logger.error(f"Unable to check if {self.label} is active")
224
+ logger.error(exception)
92
225
  return False
93
226
 
94
- config["Service"] = {
95
- "ExecStart": f'/bin/sh -lc "{found_primitive_binary_path} agent"',
96
- "Restart": "always",
97
- "StandardError": f"append:{PRIMITIVE_AGENT_LOGS_FILEPATH}",
98
- "StandardOutput": f"append:{PRIMITIVE_AGENT_LOGS_FILEPATH}",
99
- }
100
-
101
- config["Install"] = {
102
- "WantedBy": "default.target",
103
- }
104
-
105
- try:
106
- with open(PRIMITIVE_AGENT_SERVICE_FILEPATH, "w") as service_file:
107
- config.write(service_file)
108
- except IOError as exception:
109
- print(f"populate_service_file: {exception}")
110
-
111
- PRIMITIVE_AGENT_SERVICE_FILEPATH.chmod(0o644)
112
- verify_service_file()
113
-
114
-
115
- def verify_service_file():
116
- systemctl_check = (
117
- f"systemctl --user show {PRIMITIVE_AGENT_SERVICE} -p CanStart --value"
118
- )
119
- try:
120
- output = subprocess.check_output(systemctl_check.split(" ")).decode().strip()
121
- if output == "no":
122
- raise Exception(f"{systemctl_check} yielded {output}")
123
- return True
124
- except subprocess.CalledProcessError as exception:
125
- print("verify_service_file: ", exception)
126
- return False
127
-
128
-
129
- def create_stdout_file():
130
- if not PRIMITIVE_AGENT_LOGS_FILEPATH.exists():
131
- PRIMITIVE_AGENT_LOGS_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
132
- PRIMITIVE_AGENT_LOGS_FILEPATH.touch()
133
-
134
-
135
- def delete_stdout_file():
136
- if PRIMITIVE_AGENT_LOGS_FILEPATH.exists():
137
- PRIMITIVE_AGENT_LOGS_FILEPATH.unlink()
138
-
139
-
140
- def enable_service():
141
- try:
142
- enable_service = f"systemctl --user enable {PRIMITIVE_AGENT_SERVICE}"
143
- subprocess.check_output(enable_service.split(" "))
144
- return True
145
- except subprocess.CalledProcessError as exception:
146
- print("enable_service: ", exception)
147
- return False
148
-
149
-
150
- def start_service():
151
- try:
152
- start_new_service = f"systemctl --user start {PRIMITIVE_AGENT_SERVICE}"
153
- subprocess.check_output(start_new_service.split(" "))
154
- return True
155
- except subprocess.CalledProcessError as exception:
156
- print("start_service: ", exception)
157
- return False
158
-
159
-
160
- def view_service_logs():
161
- follow_logs = f"tail -f -n +1 {PRIMITIVE_AGENT_LOGS_FILEPATH}"
162
- os.system(follow_logs)
163
-
164
-
165
- def full_service_install():
166
- stop_service()
167
- disable_service()
168
- populate_service_file()
169
- create_stdout_file()
170
- enable_service()
171
- start_service()
172
-
173
-
174
- def full_service_uninstall():
175
- stop_service()
176
- disable_service()
177
- if PRIMITIVE_AGENT_SERVICE_FILEPATH.exists():
178
- PRIMITIVE_AGENT_SERVICE_FILEPATH.unlink()
179
- delete_stdout_file()
227
+ def is_installed(self) -> bool:
228
+ try:
229
+ is_service_active = (
230
+ f"systemctl --user show {self.service_name} -p UnitFileState --value" # noqa
231
+ )
232
+ output = (
233
+ subprocess.check_output(is_service_active.split(" ")).decode().strip()
234
+ )
235
+ return output == "enabled"
236
+ except subprocess.CalledProcessError as exception:
237
+ logger.error(f"Unable to check if {self.label} is enabled")
238
+ logger.error(exception)
239
+ return False
@@ -0,0 +1,41 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+ from typing import List
4
+
5
+ from ..utils.daemons import Daemon
6
+
7
+
8
+ def render_daemon_list(daemons: List[Daemon]) -> None:
9
+ console = Console()
10
+
11
+ table = Table(show_header=True, header_style="bold #FFA800")
12
+ table.add_column("Name")
13
+ table.add_column("Label")
14
+ table.add_column("Installed")
15
+ table.add_column("Active")
16
+ table.add_column("File Path")
17
+ table.add_column("Log Path")
18
+
19
+ for daemon in daemons:
20
+ child_table = Table(show_header=False, header_style="bold #FFA800")
21
+ child_table.add_column("Name")
22
+ child_table.add_column("Label")
23
+ child_table.add_column("Installed")
24
+ child_table.add_column("Active")
25
+ child_table.add_column("File Path")
26
+ child_table.add_column("Log Path")
27
+
28
+ table.add_row(
29
+ daemon.name,
30
+ daemon.label,
31
+ "[bold green]Yes[/bold green]"
32
+ if daemon.is_installed()
33
+ else "[bold red]No[/bold red]",
34
+ "[bold green]Yes[/bold green]"
35
+ if daemon.is_active()
36
+ else "[bold red]No[/bold red]",
37
+ str(daemon.file_path),
38
+ str(daemon.logs),
39
+ )
40
+
41
+ console.print(table)
primitive/db/base.py ADDED
@@ -0,0 +1,5 @@
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
3
+
4
+ class Base(DeclarativeBase):
5
+ pass
primitive/db/models.py ADDED
@@ -0,0 +1,88 @@
1
+ from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union
2
+
3
+ from sqlalchemy import Column, Integer, String
4
+ from sqlalchemy.orm import Mapped, Query, mapped_column
5
+
6
+ from .base import Base
7
+ from .sqlite import Session
8
+
9
+ T = TypeVar("T", bound="Base")
10
+
11
+
12
+ class Manager(Generic[T]):
13
+ def __init__(self, model_cls_lambda: Callable[[], Type[T]]) -> None:
14
+ self.model_cls_lambda = model_cls_lambda
15
+ self.filters: Dict[str, Any] = {}
16
+
17
+ def create(self, **kwargs) -> T:
18
+ with Session() as session:
19
+ model = self.model_cls_lambda()
20
+ obj = model(**kwargs)
21
+ session.add(obj)
22
+ session.commit()
23
+ session.refresh(obj)
24
+ return obj
25
+
26
+ def filter_by(self, **kwargs) -> "Manager[T]":
27
+ self.filters = kwargs
28
+ return self
29
+
30
+ def exists(self) -> bool:
31
+ with Session() as session:
32
+ model = self.model_cls_lambda()
33
+ query = session.query(model)
34
+ query.filter_by(**self.filters)
35
+ self.filters.clear()
36
+ return query.count() > 0
37
+
38
+ def all(self) -> List[T]:
39
+ with Session() as session:
40
+ model = self.model_cls_lambda()
41
+ query = session.query(model)
42
+ query.filter_by(**self.filters)
43
+ self.filters.clear()
44
+ return query.all()
45
+
46
+ def first(self) -> Union[T, None]:
47
+ with Session() as session:
48
+ model = self.model_cls_lambda()
49
+ query = session.query(model)
50
+ query.filter_by(**self.filters)
51
+ self.filters.clear()
52
+ return query.first()
53
+
54
+ def update(self, update: Dict[Any, Any]) -> Query[T]:
55
+ with Session() as session:
56
+ model = self.model_cls_lambda()
57
+ query = session.query(model).filter_by(**self.filters)
58
+
59
+ if query.count() > 0:
60
+ query.update(update)
61
+ session.commit()
62
+ return query
63
+ else:
64
+ raise ValueError(f"Update failed, {model.__name__} not found")
65
+
66
+ def delete(self) -> None:
67
+ with Session() as session:
68
+ model = self.model_cls_lambda()
69
+ query = session.query(model).filter_by(**self.filters)
70
+
71
+ if query.count() > 0:
72
+ query.delete()
73
+ session.commit()
74
+ else:
75
+ raise ValueError(f"Delete failed, {model.__name__} not found")
76
+
77
+
78
+ class JobRun(Base):
79
+ __tablename__ = "JobRun"
80
+
81
+ id = Column(Integer, primary_key=True)
82
+ job_run_id: Mapped[str] = mapped_column(String, nullable=False)
83
+ pid: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
84
+
85
+ objects: Manager["JobRun"] = Manager(lambda: JobRun)
86
+
87
+ def __repr__(self):
88
+ return f"<JobRun(id={self.id} job_run_id={self.job_run_id}, pid={self.pid})>"
primitive/db/sqlite.py ADDED
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ from loguru import logger
4
+ from sqlalchemy import Engine, create_engine
5
+ from sqlalchemy.orm import Session as SQLAlchemySession
6
+
7
+ from ..utils.cache import get_cache_dir
8
+ from .base import Base
9
+
10
+
11
+ def init() -> None:
12
+ db_path: Path = get_cache_dir() / "primitive.sqlite3"
13
+
14
+ # Drop DB existing database if it exists
15
+ # if db_path.exists():
16
+ # logger.warning(f"[*] Deleting existing SQLite database at {db_path}")
17
+ # db_path.unlink()
18
+ if db_path.exists():
19
+ return
20
+
21
+ logger.info(f"[*] Initializing SQLite database at {db_path}")
22
+ engine = create_engine(f"sqlite:///{db_path}", echo=False)
23
+ Base.metadata.create_all(engine)
24
+
25
+
26
+ def engine() -> Engine:
27
+ db_path: Path = get_cache_dir() / "primitive.sqlite3"
28
+ return create_engine(f"sqlite:///{db_path}", echo=False)
29
+
30
+
31
+ def Session() -> SQLAlchemySession:
32
+ from sqlalchemy.orm import sessionmaker
33
+
34
+ return sessionmaker(bind=engine())()
primitive/exec/actions.py CHANGED
@@ -58,7 +58,6 @@ class Exec(BaseAction):
58
58
 
59
59
  reservation = reservation_result.data["reservation"]
60
60
  if reservation.get("status") != "in_progress":
61
- logger.enable("primitive")
62
61
  logger.info(
63
62
  f"Reservation {reservation.get('id')} is in status {reservation.get('status')}, cannot execute command at this time."
64
63
  )
@@ -200,7 +200,6 @@ class Files(BaseAction):
200
200
  key_prefix: str = "",
201
201
  file_id: Optional[str] = None,
202
202
  ):
203
- logger.enable("primitive")
204
203
  if path.exists() is False:
205
204
  raise Exception(f"File {path} does not exist.")
206
205
 
@@ -293,7 +293,6 @@ class Hardware(BaseAction):
293
293
  )
294
294
  if messages := result.data.get("registerHardware").get("messages"):
295
295
  for message in messages:
296
- logger.enable("primitive")
297
296
  if message.get("kind") == "ERROR":
298
297
  logger.error(message.get("message"))
299
298
  else:
@@ -327,7 +326,6 @@ class Hardware(BaseAction):
327
326
 
328
327
  if messages := result.data.get("unregisterHardware").get("messages"):
329
328
  for message in messages:
330
- logger.enable("primitive")
331
329
  if message.get("kind") == "ERROR":
332
330
  logger.error(message.get("message"))
333
331
  else:
@@ -368,11 +366,11 @@ class Hardware(BaseAction):
368
366
  mutation, variable_values=variables, get_execution_result=True
369
367
  )
370
368
  except client_exceptions.ClientConnectorError as exception:
371
- message = " [*] Failed to update hardware system info! "
369
+ message = "[*] Failed to update hardware system info! "
372
370
  logger.exception(message)
373
371
  raise exception
374
372
 
375
- message = " [*] Updated hardware system info successfully! "
373
+ message = "[*] Updated hardware system info successfully! "
376
374
  logger.info(message)
377
375
 
378
376
  return result
@@ -416,7 +414,6 @@ class Hardware(BaseAction):
416
414
  checkin_success = result.data.get("checkIn").get("lastCheckIn")
417
415
  if messages := result.data.get("checkIn").get("messages"):
418
416
  for message in messages:
419
- logger.enable("primitive")
420
417
  if message.get("kind") == "ERROR":
421
418
  logger.error(message.get("message"))
422
419
  else:
@@ -435,7 +432,7 @@ class Hardware(BaseAction):
435
432
  }
436
433
  self.status_cache[fingerprint] = new_state.copy()
437
434
 
438
- message = f" [*] Checked in successfully for {fingerprint}: "
435
+ message = f"[*] Checked in successfully for {fingerprint}: "
439
436
  is_new_status = False
440
437
  for key, value in new_state.items():
441
438
  if value != previous_status.get(key, None):
@@ -463,7 +460,7 @@ class Hardware(BaseAction):
463
460
  return result
464
461
  except client_exceptions.ClientConnectorError as exception:
465
462
  if not stopping_agent:
466
- message = " [*] Failed to check in! "
463
+ message = "[*] Failed to check in! "
467
464
  logger.error(message)
468
465
  raise exception
469
466
  else:
@@ -578,7 +575,6 @@ class Hardware(BaseAction):
578
575
 
579
576
  if messages := result.data.get("registerChildHardware").get("messages"):
580
577
  for message in messages:
581
- logger.enable("primitive")
582
578
  if message.get("kind") == "ERROR":
583
579
  logger.error(message.get("message"))
584
580
  else:
@@ -596,12 +592,17 @@ class Hardware(BaseAction):
596
592
  pass
597
593
 
598
594
  @guard
599
- def _sync_children(self):
595
+ def _sync_children(self, hardware: Optional[Dict[str, str]] = None):
600
596
  # get the existing children if any from the hardware details
601
597
  # get the latest children from the node
602
598
  # compare the two and update the node with the latest children
603
599
  # remove any children from remote that are not in the latest children
604
- hardware = self.primitive.hardware.get_own_hardware_details()
600
+ if not hardware:
601
+ hardware = self.primitive.hardware.get_own_hardware_details()
602
+ if not hardware:
603
+ logger.error("No hardware found.")
604
+ return
605
+
605
606
  remote_children = hardware.get("children", [])
606
607
  local_children = self.primitive.hardware._list_local_children()
607
608