primitive 0.2.10__py3-none-any.whl → 0.2.11__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,78 @@
1
+ from sqlalchemy import Column, Integer, String
2
+ from sqlalchemy.orm import Mapped, mapped_column, Query
3
+ from typing import Optional, Generic, TypeVar, Callable, Type, List, Union, Dict, Any
4
+ from .sqlite import Session
5
+ from .base import Base
6
+
7
+ T = TypeVar("T", bound="Base")
8
+
9
+
10
+ class Manager(Generic[T]):
11
+ def __init__(self, model_cls_lambda: Callable[[], Type[T]]) -> None:
12
+ self.model_cls_lambda = model_cls_lambda
13
+ self.filters: Dict[str, Any] = {}
14
+
15
+ def create(self, **kwargs) -> T:
16
+ with Session() as session:
17
+ model = self.model_cls_lambda()
18
+ obj = model(**kwargs)
19
+ session.add(obj)
20
+ session.commit()
21
+ session.refresh(obj)
22
+ return obj
23
+
24
+ def filter_by(self, **kwargs) -> "Manager[T]":
25
+ self.filters = kwargs
26
+ return self
27
+
28
+ def all(self) -> List[T]:
29
+ with Session() as session:
30
+ model = self.model_cls_lambda()
31
+ query = session.query(model)
32
+ query.filter_by(**self.filters)
33
+ self.filters.clear()
34
+ return query.all()
35
+
36
+ def first(self) -> Union[T, None]:
37
+ with Session() as session:
38
+ model = self.model_cls_lambda()
39
+ query = session.query(model)
40
+ query.filter_by(**self.filters)
41
+ self.filters.clear()
42
+ return query.first()
43
+
44
+ def update(self, update: Dict[Any, Any]) -> Query[T]:
45
+ with Session() as session:
46
+ model = self.model_cls_lambda()
47
+ query = session.query(model).filter_by(**self.filters)
48
+
49
+ if query.count() > 0:
50
+ query.update(update)
51
+ session.commit()
52
+ return query
53
+ else:
54
+ raise ValueError(f"{model.__name__} not found")
55
+
56
+ def delete(self) -> None:
57
+ with Session() as session:
58
+ model = self.model_cls_lambda()
59
+ query = session.query(model).filter_by(**self.filters)
60
+
61
+ if query.count() > 0:
62
+ query.delete()
63
+ session.commit()
64
+ else:
65
+ raise ValueError(f"{model.__name__} not found")
66
+
67
+
68
+ class JobRun(Base):
69
+ __tablename__ = "JobRun"
70
+
71
+ id = Column(Integer, primary_key=True)
72
+ job_run_id: Mapped[str] = mapped_column(String, nullable=False)
73
+ pid: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
74
+
75
+ objects: Manager["JobRun"] = Manager(lambda: JobRun)
76
+
77
+ def __repr__(self):
78
+ return f"<JobRun(id={self.id} job_run_id={self.job_run_id}, pid={self.pid})>"
primitive/db/sqlite.py ADDED
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+ from sqlalchemy import create_engine, Engine
3
+ from sqlalchemy.orm import Session as SQLAlchemySession
4
+ from ..utils.cache import get_cache_dir
5
+ from .base import Base
6
+
7
+
8
+ def init() -> None:
9
+ db_path: Path = get_cache_dir() / "primitive.sqlite3"
10
+
11
+ # Drop DB existing database if it exists
12
+ if db_path.exists():
13
+ db_path.unlink()
14
+
15
+ engine = create_engine(f"sqlite:///{db_path}", echo=False)
16
+ Base.metadata.create_all(engine)
17
+
18
+
19
+ def engine() -> Engine:
20
+ db_path: Path = get_cache_dir() / "primitive.sqlite3"
21
+ return create_engine(f"sqlite:///{db_path}", echo=False)
22
+
23
+
24
+ def Session() -> SQLAlchemySession:
25
+ from sqlalchemy.orm import sessionmaker
26
+
27
+ 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: