primitive 0.2.68__tar.gz → 0.2.72__tar.gz

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 primitive might be problematic. Click here for more details.

Files changed (134) hide show
  1. {primitive-0.2.68 → primitive-0.2.72}/.vscode/settings.json +1 -0
  2. {primitive-0.2.68 → primitive-0.2.72}/PKG-INFO +1 -1
  3. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/__about__.py +1 -1
  4. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/agent/actions.py +8 -5
  5. primitive-0.2.72/src/primitive/agent/pxe.py +71 -0
  6. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/agent/runner.py +56 -45
  7. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/cli.py +2 -0
  8. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/client.py +2 -0
  9. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/actions.py +23 -1
  10. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/actions.py +202 -2
  11. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/commands.py +13 -2
  12. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/messaging/provider.py +1 -0
  13. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/monitor/actions.py +11 -6
  14. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/network/actions.py +89 -44
  15. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/network/commands.py +17 -6
  16. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/network/ssh.py +53 -12
  17. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/network/ui.py +9 -3
  18. primitive-0.2.72/src/primitive/operating_systems/actions.py +473 -0
  19. primitive-0.2.72/src/primitive/operating_systems/commands.py +246 -0
  20. primitive-0.2.72/src/primitive/operating_systems/graphql/mutations.py +32 -0
  21. primitive-0.2.72/src/primitive/operating_systems/graphql/queries.py +36 -0
  22. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/organizations/actions.py +6 -0
  23. primitive-0.2.72/src/primitive/reservations/graphql/__init__.py +0 -0
  24. primitive-0.2.72/src/primitive/utils/__init__.py +0 -0
  25. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/cache.py +11 -0
  26. primitive-0.2.72/src/primitive/utils/checksums.py +44 -0
  27. primitive-0.2.72/testing_containers/README.md +21 -0
  28. primitive-0.2.72/testing_containers/fedora.Containerfile +20 -0
  29. primitive-0.2.72/testing_containers/ubuntu.Containerfile +20 -0
  30. {primitive-0.2.68 → primitive-0.2.72}/.git-hooks/pre-commit +0 -0
  31. {primitive-0.2.68 → primitive-0.2.72}/.gitattributes +0 -0
  32. {primitive-0.2.68 → primitive-0.2.72}/.github/workflows/lint.yml +0 -0
  33. {primitive-0.2.68 → primitive-0.2.72}/.github/workflows/publish.yml +0 -0
  34. {primitive-0.2.68 → primitive-0.2.72}/.github/workflows/pyright.yml +0 -0
  35. {primitive-0.2.68 → primitive-0.2.72}/.gitignore +0 -0
  36. {primitive-0.2.68 → primitive-0.2.72}/.vscode/extensions.json +0 -0
  37. {primitive-0.2.68 → primitive-0.2.72}/LICENSE.txt +0 -0
  38. {primitive-0.2.68 → primitive-0.2.72}/Makefile +0 -0
  39. {primitive-0.2.68 → primitive-0.2.72}/README.md +0 -0
  40. {primitive-0.2.68 → primitive-0.2.72}/linux setup.md +0 -0
  41. {primitive-0.2.68 → primitive-0.2.72}/pyproject.toml +0 -0
  42. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/__init__.py +0 -0
  43. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/agent/__init__.py +0 -0
  44. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/agent/commands.py +0 -0
  45. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/agent/uploader.py +0 -0
  46. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/auth/__init__.py +0 -0
  47. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/auth/actions.py +0 -0
  48. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/auth/commands.py +0 -0
  49. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/auth/graphql/__init__.py +0 -0
  50. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/auth/graphql/queries.py +0 -0
  51. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/daemons/__init__.py +0 -0
  52. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/daemons/actions.py +0 -0
  53. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/daemons/commands.py +0 -0
  54. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/daemons/launch_agents.py +0 -0
  55. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/daemons/launch_service.py +0 -0
  56. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/daemons/ui.py +0 -0
  57. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/exec/__init__.py +0 -0
  58. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/exec/actions.py +0 -0
  59. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/exec/commands.py +0 -0
  60. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/exec/interactive.py +0 -0
  61. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/__init__.py +0 -0
  62. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/commands.py +0 -0
  63. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/graphql/__init__.py +0 -0
  64. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/graphql/fragments.py +0 -0
  65. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/graphql/mutations.py +0 -0
  66. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/graphql/queries.py +0 -0
  67. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/files/ui.py +0 -0
  68. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/git/__init__.py +0 -0
  69. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/git/actions.py +0 -0
  70. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/git/commands.py +0 -0
  71. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/git/graphql/__init__.py +0 -0
  72. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/git/graphql/queries.py +0 -0
  73. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/graphql/__init__.py +0 -0
  74. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/graphql/relay.py +0 -0
  75. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/graphql/sdk.py +0 -0
  76. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/graphql/utility_fragments.py +0 -0
  77. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/__init__.py +0 -0
  78. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/android.py +0 -0
  79. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/graphql/__init__.py +0 -0
  80. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/graphql/fragments.py +0 -0
  81. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/graphql/mutations.py +0 -0
  82. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/graphql/queries.py +0 -0
  83. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/hardware/ui.py +0 -0
  84. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/__init__.py +0 -0
  85. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/actions.py +0 -0
  86. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/commands.py +0 -0
  87. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/graphql/__init__.py +0 -0
  88. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/graphql/fragments.py +0 -0
  89. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/graphql/mutations.py +0 -0
  90. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/jobs/graphql/queries.py +0 -0
  91. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/messaging/__init__.py +0 -0
  92. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/monitor/commands.py +0 -0
  93. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/network/__init__.py +0 -0
  94. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/network/redfish.py +0 -0
  95. {primitive-0.2.68/src/primitive/organizations → primitive-0.2.72/src/primitive/operating_systems}/__init__.py +0 -0
  96. {primitive-0.2.68/src/primitive/organizations → primitive-0.2.72/src/primitive/operating_systems}/graphql/__init__.py +0 -0
  97. {primitive-0.2.68/src/primitive/projects → primitive-0.2.72/src/primitive/organizations}/__init__.py +0 -0
  98. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/organizations/commands.py +0 -0
  99. {primitive-0.2.68/src/primitive/projects → primitive-0.2.72/src/primitive/organizations}/graphql/__init__.py +0 -0
  100. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/organizations/graphql/fragments.py +0 -0
  101. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/organizations/graphql/mutations.py +0 -0
  102. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/organizations/graphql/queries.py +0 -0
  103. {primitive-0.2.68/src/primitive/provisioning → primitive-0.2.72/src/primitive/projects}/__init__.py +0 -0
  104. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/projects/actions.py +0 -0
  105. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/projects/commands.py +0 -0
  106. {primitive-0.2.68/src/primitive/provisioning → primitive-0.2.72/src/primitive/projects}/graphql/__init__.py +0 -0
  107. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/projects/graphql/fragments.py +0 -0
  108. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/projects/graphql/mutations.py +0 -0
  109. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/projects/graphql/queries.py +0 -0
  110. {primitive-0.2.68/src/primitive/reservations → primitive-0.2.72/src/primitive/provisioning}/__init__.py +0 -0
  111. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/provisioning/actions.py +0 -0
  112. {primitive-0.2.68/src/primitive/reservations → primitive-0.2.72/src/primitive/provisioning}/graphql/__init__.py +0 -0
  113. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/provisioning/graphql/queries.py +0 -0
  114. {primitive-0.2.68/src/primitive/utils → primitive-0.2.72/src/primitive/reservations}/__init__.py +0 -0
  115. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/reservations/actions.py +0 -0
  116. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/reservations/commands.py +0 -0
  117. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/reservations/graphql/fragments.py +0 -0
  118. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/reservations/graphql/mutations.py +0 -0
  119. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/reservations/graphql/queries.py +0 -0
  120. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/actions.py +0 -0
  121. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/auth.py +0 -0
  122. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/chunk_size.py +0 -0
  123. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/config.py +0 -0
  124. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/daemons.py +0 -0
  125. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/exceptions.py +0 -0
  126. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/logging.py +0 -0
  127. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/memory_size.py +0 -0
  128. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/printer.py +0 -0
  129. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/psutil.py +0 -0
  130. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/shell.py +0 -0
  131. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/text.py +0 -0
  132. {primitive-0.2.68 → primitive-0.2.72}/src/primitive/utils/x509.py +0 -0
  133. {primitive-0.2.68 → primitive-0.2.72}/tests/__init__.py +0 -0
  134. {primitive-0.2.68 → primitive-0.2.72}/uv.lock +0 -0
@@ -3,6 +3,7 @@
3
3
  "ajob",
4
4
  "cafile",
5
5
  "Checkin",
6
+ "efibootmgr",
6
7
  "loguru",
7
8
  "machdep",
8
9
  "Mbit",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.68
3
+ Version: 0.2.72
4
4
  Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
5
5
  Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
6
6
  Project-URL: Source, https://github.com//primitivecorp/primitive-cli
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.68"
4
+ __version__ = "0.2.72"
@@ -96,11 +96,14 @@ class Agent(BaseAction):
96
96
  job_run_status = job_run_data.get("status", None)
97
97
 
98
98
  hardware_id = hardware.get("id", None) if hardware else None
99
- execution_hardware_id = (
100
- job_run_data.get("executionHardware", {}).get("id", None)
101
- if job_run_data
102
- else None
103
- )
99
+ execution_hardware_id = None
100
+ if job_run_data:
101
+ execution_hardware = job_run_data.get("executionHardware", None)
102
+ execution_hardware_id = (
103
+ execution_hardware.get("id", None)
104
+ if execution_hardware
105
+ else None
106
+ )
104
107
  target_hardware_id = None
105
108
 
106
109
  if (
@@ -0,0 +1,71 @@
1
+ from loguru import logger
2
+
3
+ from primitive.network.redfish import RedfishClient
4
+ from primitive.network.ssh import test_ssh_connection, run_command
5
+
6
+
7
+ def pxe_boot_via_redfish(target_hardware_secret: dict):
8
+ bmc_host = target_hardware_secret.get("bmcHostname", None)
9
+ bmc_username = target_hardware_secret.get("bmcUsername", None)
10
+ bmc_password = target_hardware_secret.get("bmcPassword", "")
11
+
12
+ if bmc_host is None:
13
+ logger.error(
14
+ "No BMC host found in target hardware secret for out-of-band power cycle"
15
+ )
16
+ return True
17
+ if bmc_username is None:
18
+ logger.error(
19
+ "No BMC username found in target hardware secret for out-of-band power cycle"
20
+ )
21
+ return True
22
+
23
+ redfish = RedfishClient(host=bmc_host, username=bmc_username, password=bmc_password)
24
+ redfish.update_boot_options(
25
+ system_id="1",
26
+ boot_source_override_target="Pxe",
27
+ boot_source_override_enabled="Once",
28
+ boot_source_override_mode="UEFI",
29
+ )
30
+ redfish.compute_system_reset(system_id="1", reset_type="ForceRestart")
31
+
32
+
33
+ def pxe_boot_via_efibootmgr(target_hardware_secret: dict):
34
+ run_command(
35
+ hostname=target_hardware_secret.get("hostname"),
36
+ username=target_hardware_secret.get("username"),
37
+ password=target_hardware_secret.get("password"),
38
+ command="sudo efibootmgr -n $(efibootmgr | awk '/PXE IPV4/ {print substr($1,5,4)}' | head -n1 || efibootmgr | awk '/ubuntu/ {print substr($1,5,4)}' | head -n1) && sudo reboot",
39
+ port=22,
40
+ )
41
+
42
+
43
+ def pxe_boot(target_hardware_secret: dict) -> bool:
44
+ redfish_available = False
45
+ ssh_available = False
46
+
47
+ if target_hardware_secret.get("bmcHostname", None):
48
+ redfish_available = True
49
+ else:
50
+ logger.info(
51
+ "No BMC credentials found, skipping Redfish PXE boot method. Checking efiboot method."
52
+ )
53
+
54
+ ssh_available = test_ssh_connection(
55
+ hostname=target_hardware_secret.get("hostname"),
56
+ username=target_hardware_secret.get("username"),
57
+ password=target_hardware_secret.get("password"),
58
+ port=22,
59
+ )
60
+
61
+ if redfish_available:
62
+ pxe_boot_via_redfish(target_hardware_secret=target_hardware_secret)
63
+ return True
64
+ elif ssh_available:
65
+ pxe_boot_via_efibootmgr(target_hardware_secret=target_hardware_secret)
66
+ return True
67
+ else:
68
+ logger.error(
69
+ "No available method to PXE boot target hardware. Missing BMC credentials and SSH is not available."
70
+ )
71
+ return False
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import os
3
3
  import shutil
4
+ import time
4
5
  import typing
5
6
  from enum import Enum
6
7
  from pathlib import Path, PurePath
@@ -9,6 +10,7 @@ from datetime import datetime, timezone
9
10
 
10
11
  from loguru import logger
11
12
 
13
+ from primitive.agent.pxe import pxe_boot
12
14
  from primitive.network.ssh import wait_for_ssh
13
15
  from primitive.utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
14
16
  from primitive.utils.logging import fmt, log_context
@@ -231,6 +233,11 @@ class Runner:
231
233
  commands = task["cmd"].strip().split("\n")
232
234
 
233
235
  for i, cmd in enumerate(commands):
236
+ if cmd.strip() == "":
237
+ continue
238
+ if cmd.strip().startswith("#"):
239
+ logger.debug(f"Skipping comment line: {cmd.strip()}")
240
+ continue
234
241
  if cmd == "oobpowercycle":
235
242
  logger.info("Performing out-of-band power cycle")
236
243
  from primitive.network.redfish import RedfishClient
@@ -261,37 +268,29 @@ class Runner:
261
268
  is_rebooting=True,
262
269
  start_rebooting_at=str(datetime.now(timezone.utc)),
263
270
  )
271
+ logger.info(
272
+ "Box rebooting, waiting 30 seconds before beginning SSH connection."
273
+ )
274
+ time.sleep(30)
275
+ wait_for_ssh(
276
+ hostname=self.target_hardware_secret.get("hostname"),
277
+ username=self.target_hardware_secret.get("username"),
278
+ password=self.target_hardware_secret.get("password"),
279
+ port=22,
280
+ )
281
+ logger.info("Reboot successful, SSH is now available")
282
+ await self.primitive.hardware.aupdate_hardware(
283
+ hardware_id=self.target_hardware_id,
284
+ is_online=True,
285
+ is_rebooting=False,
286
+ )
264
287
  continue
265
288
 
266
289
  if cmd == "pxeboot":
267
290
  logger.info("Setting next boot to PXE and rebooting")
268
- from primitive.network.redfish import RedfishClient
269
291
 
270
- bmc_host = self.target_hardware_secret.get("bmcHostname", None)
271
- bmc_username = self.target_hardware_secret.get("bmcUsername", None)
272
- bmc_password = self.target_hardware_secret.get("bmcPassword", "")
273
-
274
- if bmc_host is None:
275
- logger.error(
276
- "No BMC host found in target hardware secret for out-of-band power cycle"
277
- )
278
- return True
279
- if bmc_username is None:
280
- logger.error(
281
- "No BMC username found in target hardware secret for out-of-band power cycle"
282
- )
283
- return True
292
+ pxe_boot(target_hardware_secret=self.target_hardware_secret)
284
293
 
285
- redfish = RedfishClient(
286
- host=bmc_host, username=bmc_username, password=bmc_password
287
- )
288
- redfish.update_boot_options(
289
- system_id="1",
290
- boot_source_override_target="Pxe",
291
- boot_source_override_enabled="Once",
292
- boot_source_override_mode="UEFI",
293
- )
294
- redfish.compute_system_reset(system_id="1", reset_type="ForceRestart")
295
294
  if self.target_hardware_id:
296
295
  await self.primitive.hardware.aupdate_hardware(
297
296
  hardware_id=self.target_hardware_id,
@@ -299,21 +298,29 @@ class Runner:
299
298
  is_rebooting=True,
300
299
  start_rebooting_at=str(datetime.now(timezone.utc)),
301
300
  )
302
- wait_for_ssh(
303
- hostname=self.target_hardware_secret.get("hostname"),
304
- username=self.target_hardware_secret.get("username"),
305
- password=self.target_hardware_secret.get("password"),
306
- port=22,
307
- )
308
- logger.info("PXE boot successful, SSH is now available")
301
+ logger.info(
302
+ "Box rebooting, waiting 30 seconds before beginning SSH connection."
303
+ )
304
+ time.sleep(30)
305
+ wait_for_ssh(
306
+ hostname=self.target_hardware_secret.get("hostname"),
307
+ username=self.target_hardware_secret.get("username"),
308
+ password=self.target_hardware_secret.get("password"),
309
+ port=22,
310
+ )
311
+ logger.info("PXE boot successful, SSH is now available")
312
+ await self.primitive.hardware.aupdate_hardware(
313
+ hardware_id=self.target_hardware_id,
314
+ is_online=True,
315
+ is_rebooting=False,
316
+ )
309
317
  continue
310
318
 
311
- args = ["/bin/bash", "-c", cmd]
312
319
  if self.target_hardware_secret:
313
320
  username = self.target_hardware_secret.get("username")
314
321
  password = self.target_hardware_secret.get("password")
315
322
  hostname = self.target_hardware_secret.get("hostname")
316
- args = [
323
+ command_args = [
317
324
  "sshpass",
318
325
  "-p",
319
326
  password,
@@ -326,15 +333,18 @@ class Runner:
326
333
  "IdentitiesOnly=yes",
327
334
  f"{username}@{hostname}",
328
335
  "--",
329
- f'/bin/bash -c "cd {task.get("workdir", "~/")} && {cmd}"',
336
+ f"{cmd}",
330
337
  ]
338
+ print(" ".join(command_args))
339
+ else:
340
+ command_args = ["/bin/bash", "--login", "-c", cmd]
331
341
 
332
342
  logger.info(
333
343
  f"Executing command {i + 1}/{len(commands)}: {cmd} at {self.source_dir / task.get('workdir', '')}"
334
344
  )
335
345
 
336
346
  process = await asyncio.create_subprocess_exec(
337
- *args,
347
+ *command_args,
338
348
  env=self.modified_env,
339
349
  cwd=str(Path(self.source_dir / task.get("workdir", ""))),
340
350
  stdout=asyncio.subprocess.PIPE,
@@ -398,15 +408,16 @@ class Runner:
398
408
 
399
409
  @log_context(label="cleanup")
400
410
  def cleanup(self) -> None:
401
- for glob in self.config.get("stores", []):
402
- # Glob relative to the source directory
403
- matches = self.source_dir.rglob(glob)
404
-
405
- for match in matches:
406
- relative_path = PurePath(match).relative_to(self.source_dir)
407
- dest = Path(get_artifacts_cache(self.job_run["id"]) / relative_path)
408
- dest.parent.mkdir(parents=True, exist_ok=True)
409
- Path(match).replace(dest)
411
+ if stores := self.config.get("stores"):
412
+ for glob in stores:
413
+ # Glob relative to the source directory
414
+ matches = self.source_dir.rglob(glob)
415
+
416
+ for match in matches:
417
+ relative_path = PurePath(match).relative_to(self.source_dir)
418
+ dest = Path(get_artifacts_cache(self.job_run["id"]) / relative_path)
419
+ dest.parent.mkdir(parents=True, exist_ok=True)
420
+ Path(match).replace(dest)
410
421
 
411
422
  shutil.rmtree(path=self.source_dir)
412
423
 
@@ -18,6 +18,7 @@ from .projects.commands import cli as projects_commands
18
18
  from .reservations.commands import cli as reservations_commands
19
19
  from .monitor.commands import cli as monitor_commands
20
20
  from .network.commands import cli as network_commands
21
+ from .operating_systems.commands import cli as operating_system_commands
21
22
 
22
23
 
23
24
  @click.group()
@@ -75,6 +76,7 @@ cli.add_command(reservations_commands, "reservations")
75
76
  cli.add_command(exec_commands, "exec")
76
77
  cli.add_command(monitor_commands, "monitor")
77
78
  cli.add_command(network_commands, "network")
79
+ cli.add_command(operating_system_commands, "operating-systems")
78
80
 
79
81
  if __name__ == "__main__":
80
82
  cli(obj={})
@@ -17,6 +17,7 @@ from .hardware.actions import Hardware
17
17
  from .jobs.actions import Jobs
18
18
  from .monitor.actions import Monitor
19
19
  from .network.actions import Network
20
+ from .operating_systems.actions import OperatingSystems
20
21
  from .organizations.actions import Organizations
21
22
  from .projects.actions import Projects
22
23
  from .provisioning.actions import Provisioning
@@ -99,6 +100,7 @@ class Primitive:
99
100
  self.provisioning: Provisioning = Provisioning(self)
100
101
  self.monitor: Monitor = Monitor(self)
101
102
  self.network: Network = Network(self)
103
+ self.operating_systems: OperatingSystems = OperatingSystems(self)
102
104
 
103
105
  def get_host_config(self):
104
106
  self.full_config = read_config_file()
@@ -62,6 +62,7 @@ class Files(BaseAction):
62
62
  chunk_size: int,
63
63
  number_of_parts: int,
64
64
  is_public: bool = False,
65
+ organization_id: Optional[str] = None,
65
66
  ):
66
67
  mutation = gql(pending_file_create_mutation)
67
68
  input = {
@@ -74,6 +75,8 @@ class Files(BaseAction):
74
75
  "chunkSize": chunk_size,
75
76
  "numberOfParts": number_of_parts,
76
77
  }
78
+ if organization_id:
79
+ input["organizationId"] = organization_id
77
80
  variables = {"input": input}
78
81
  result = self.primitive.session.execute(
79
82
  mutation, variable_values=variables, get_execution_result=True
@@ -223,6 +226,7 @@ class Files(BaseAction):
223
226
  is_public: bool = False,
224
227
  key_prefix: str = "",
225
228
  file_id: Optional[str] = None,
229
+ organization_id: Optional[str] = None,
226
230
  ):
227
231
  if path.exists() is False:
228
232
  raise Exception(f"File {path} does not exist.")
@@ -262,6 +266,7 @@ class Files(BaseAction):
262
266
  is_public=is_public,
263
267
  chunk_size=chunk_size,
264
268
  number_of_parts=number_of_parts,
269
+ organization_id=organization_id,
265
270
  )
266
271
  file_id = pending_file_create.get("id")
267
272
  parts_details = pending_file_create.get("partsDetails")
@@ -332,6 +337,7 @@ class Files(BaseAction):
332
337
  path: Path,
333
338
  is_public: bool = False,
334
339
  key_prefix: str = "",
340
+ organization_id: Optional[str] = None,
335
341
  ):
336
342
  """
337
343
  This method uploads a file via the Primitive API.
@@ -347,6 +353,11 @@ class Files(BaseAction):
347
353
  + file_path
348
354
  + """\", "keyPrefix": \""""
349
355
  + key_prefix
356
+ + (
357
+ f'", "organizationId": "{organization_id}'
358
+ if organization_id
359
+ else ""
360
+ )
350
361
  + """\" } } }"""
351
362
  ) # noqa
352
363
 
@@ -356,6 +367,11 @@ class Files(BaseAction):
356
367
  + file_path
357
368
  + """\", "keyPrefix": \""""
358
369
  + key_prefix
370
+ + (
371
+ f'", "organizationId": "{organization_id}'
372
+ if organization_id
373
+ else ""
374
+ )
359
375
  + """\" } } }"""
360
376
  ) # noqa
361
377
  body = {
@@ -385,6 +401,7 @@ class Files(BaseAction):
385
401
  output_path: Path = Path().cwd(),
386
402
  ) -> Path:
387
403
  file_pk = None
404
+ file_size = None
388
405
 
389
406
  files_result = self.primitive.files.files(
390
407
  file_id=file_id,
@@ -396,6 +413,7 @@ class Files(BaseAction):
396
413
  file = files_data["files"]["edges"][0]["node"]
397
414
  file_pk = file["pk"]
398
415
  file_name = file["fileName"]
416
+ file_size = int(file["fileSize"])
399
417
 
400
418
  if not file_pk:
401
419
  raise Exception(
@@ -404,7 +422,11 @@ class Files(BaseAction):
404
422
 
405
423
  session = create_requests_session(host_config=self.primitive.host_config)
406
424
  transport = self.primitive.host_config.get("transport")
407
- url = f"{transport}://{self.primitive.host}/files/{file_pk}/stream/"
425
+
426
+ if file_size and file_size < 5 * 1024 * 1024:
427
+ url = f"{transport}://{self.primitive.host}/files/{file_pk}/stream/"
428
+ else:
429
+ url = f"{transport}://{self.primitive.host}/files/{file_pk}/presigned-url/"
408
430
 
409
431
  downloaded_file = output_path / file_name
410
432
 
@@ -283,7 +283,180 @@ class Hardware(BaseAction):
283
283
  return {"linux_machine_id": machine_id}
284
284
  return {}
285
285
 
286
- def get_system_info(self):
286
+ def _get_homebrew_installed(self):
287
+ try:
288
+ raw = json.loads(
289
+ subprocess.check_output(["brew", "info", "--json=v2", "--installed"])
290
+ )
291
+
292
+ if isinstance(raw, Dict):
293
+ raw_formulae = raw.get("formulae", [])
294
+ raw_casks = raw.get("casks", [])
295
+
296
+ formulae = []
297
+ casks = []
298
+
299
+ if isinstance(raw_formulae, List):
300
+ for raw_formula in raw_formulae:
301
+ try:
302
+ formulae.append(
303
+ {
304
+ "full_name": raw_formula["full_name"],
305
+ "tap": raw_formula["tap"],
306
+ "version": raw_formula["versions"]["stable"],
307
+ }
308
+ )
309
+ except Exception:
310
+ pass
311
+
312
+ if isinstance(raw_casks, List):
313
+ for raw_cask in raw_casks:
314
+ try:
315
+ casks.append(
316
+ {
317
+ "full_token": raw_cask["full_token"],
318
+ "name": raw_cask["name"][0],
319
+ "version": raw_cask["installed"],
320
+ }
321
+ )
322
+ except Exception:
323
+ pass
324
+
325
+ return {
326
+ "formulae": formulae,
327
+ "casks": casks,
328
+ }
329
+ except Exception:
330
+ pass
331
+
332
+ def _get_darwin_md_application(self, application_path: str):
333
+ lines = (
334
+ subprocess.check_output(
335
+ [
336
+ "mdls",
337
+ "-name",
338
+ "kMDItemDisplayName",
339
+ "-name",
340
+ "kMDItemVersion",
341
+ "-name",
342
+ "kMDItemCFBundleIdentifier",
343
+ application_path,
344
+ ]
345
+ )
346
+ .strip()
347
+ .decode("utf-8")
348
+ .split("\n")
349
+ )
350
+
351
+ item = {
352
+ "path": application_path,
353
+ }
354
+
355
+ for line in lines:
356
+ raw_key, raw_value = line.split(" = ")
357
+
358
+ key = raw_key.strip()
359
+
360
+ if raw_value == "(null)":
361
+ item[key] = None
362
+ elif raw_value.startswith('"') and raw_value.endswith('"'):
363
+ item[key] = raw_value[1:-1]
364
+
365
+ return item
366
+
367
+ def _get_darwin_md_applications(self):
368
+ import concurrent.futures
369
+
370
+ try:
371
+ application_paths = (
372
+ subprocess.check_output(["mdfind", "kMDItemKind == 'Application'"])
373
+ .strip()
374
+ .decode("utf-8")
375
+ .split("\n")
376
+ )
377
+
378
+ with concurrent.futures.ThreadPoolExecutor() as executor:
379
+ return list(
380
+ executor.map(self._get_darwin_md_application, application_paths)
381
+ )
382
+
383
+ except Exception:
384
+ pass
385
+
386
+ def _get_fedora_installed_packages(self):
387
+ try:
388
+ lines = (
389
+ subprocess.check_output(
390
+ [
391
+ "dnf",
392
+ "repoquery",
393
+ "--installed",
394
+ "--qf",
395
+ "%{name} , %{version}\n",
396
+ ]
397
+ )
398
+ .decode("utf-8")
399
+ .strip()
400
+ .split("\n")
401
+ )
402
+
403
+ items = []
404
+
405
+ for line in lines:
406
+ try:
407
+ name, version = line.split(" , ")
408
+
409
+ items.append(
410
+ {
411
+ "name": name,
412
+ "version": version,
413
+ }
414
+ )
415
+ except Exception:
416
+ pass
417
+ return items
418
+ except Exception:
419
+ pass
420
+
421
+ def _get_ubuntu_installed_packages(self):
422
+ try:
423
+ lines = (
424
+ subprocess.check_output(
425
+ ["apt", "list", "--installed"],
426
+ stderr=subprocess.DEVNULL,
427
+ )
428
+ .decode("utf-8")
429
+ .strip()
430
+ .split("\n")
431
+ )
432
+
433
+ lines.pop()
434
+
435
+ items = []
436
+
437
+ for line in lines:
438
+ try:
439
+ columns = line.split()
440
+
441
+ if len(columns) < 2:
442
+ continue
443
+
444
+ name, version, *_ = columns
445
+ items.append(
446
+ {
447
+ "name": name.split("/")[0],
448
+ "version": version,
449
+ }
450
+ )
451
+ except Exception:
452
+ pass
453
+
454
+ return items
455
+
456
+ except Exception:
457
+ pass
458
+
459
+ def get_system_info(self, with_installed_applications: bool = False):
287
460
  os_family = platform.system()
288
461
  system_info = {}
289
462
  if os_family == "Darwin":
@@ -294,6 +467,13 @@ class Hardware(BaseAction):
294
467
  .decode("utf-8")
295
468
  )
296
469
  system_info["apple_mac_os_version"] = platform.mac_ver()[0]
470
+ system_info["homebrew_installed"] = self._get_homebrew_installed()
471
+
472
+ if with_installed_applications:
473
+ system_info["darwin_md_applications"] = (
474
+ self._get_darwin_md_applications()
475
+ )
476
+
297
477
  elif os_family == "Linux":
298
478
  # Support for Linux-based VMs in Windows
299
479
  if "WSL2" in platform.platform():
@@ -304,6 +484,18 @@ class Hardware(BaseAction):
304
484
  }
305
485
  else:
306
486
  system_info = {**system_info, **self._get_ubuntu_values()}
487
+ system_info["linux_release"] = platform.freedesktop_os_release()
488
+
489
+ match system_info["linux_release"]["ID"]:
490
+ case "ubuntu":
491
+ system_info["ubuntu_installed_packages"] = (
492
+ self._get_ubuntu_installed_packages()
493
+ )
494
+ case "fedora":
495
+ system_info["fedora_installed_packages"] = (
496
+ self._get_fedora_installed_packages()
497
+ )
498
+
307
499
  elif os_family == "Windows":
308
500
  system_info = {
309
501
  **system_info,
@@ -604,7 +796,7 @@ class Hardware(BaseAction):
604
796
  if slug is not None:
605
797
  filters["slug"] = {"exact": slug}
606
798
  if id is not None:
607
- filters["id"] = {"exact": id}
799
+ filters["id"] = id
608
800
  # if nested_children is True:
609
801
  # filters["hasParent"] = {"exact": False}
610
802
 
@@ -1017,3 +1209,11 @@ class Hardware(BaseAction):
1017
1209
  logger.info(message)
1018
1210
 
1019
1211
  return result
1212
+
1213
+ def push_own_system_info(self):
1214
+ if self.primitive.messaging.ready:
1215
+ message = {"system_info": self.primitive.hardware.get_system_info()}
1216
+ self.primitive.messaging.send_message(
1217
+ message_type=MESSAGE_TYPES.SYSTEM_INFO,
1218
+ message=message,
1219
+ )
@@ -22,11 +22,22 @@ def cli(context):
22
22
 
23
23
 
24
24
  @cli.command("systeminfo")
25
+ @click.option(
26
+ "--with-installed-applications",
27
+ is_flag=True,
28
+ default=False,
29
+ help="Only for MacOS, list installed applications using metadata commands",
30
+ )
25
31
  @click.pass_context
26
- def systeminfo_command(context):
32
+ def systeminfo_command(
33
+ context,
34
+ with_installed_applications: bool = False,
35
+ ):
27
36
  """Get System Info"""
28
37
  primitive: Primitive = context.obj.get("PRIMITIVE")
29
- message = primitive.hardware.get_system_info()
38
+ message = primitive.hardware.get_system_info(
39
+ with_installed_applications=with_installed_applications
40
+ )
30
41
  print_result(message=message, context=context)
31
42
 
32
43
 
@@ -27,6 +27,7 @@ CELERY_TASK_NAME = "hardware.tasks.task_receive_hardware_message"
27
27
 
28
28
  class MESSAGE_TYPES(enum.Enum):
29
29
  CHECK_IN = "CHECK_IN"
30
+ SYSTEM_INFO = "SYSTEM_INFO"
30
31
  METRICS = "METRICS"
31
32
  SWITCH_AND_INTERFACES_INFO = "SWITCH_AND_INTERFACES_INFO"
32
33
  OWN_NETWORK_INTERFACES = "OWN_NETWORK_INTERFACES"