primitive 0.2.63__tar.gz → 0.2.66__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.
- {primitive-0.2.63 → primitive-0.2.66}/.vscode/settings.json +1 -0
- {primitive-0.2.63 → primitive-0.2.66}/Makefile +1 -1
- {primitive-0.2.63 → primitive-0.2.66}/PKG-INFO +2 -1
- {primitive-0.2.63 → primitive-0.2.66}/pyproject.toml +3 -2
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/__about__.py +1 -1
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/agent/actions.py +33 -3
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/agent/runner.py +99 -3
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/cli.py +2 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/client.py +2 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/actions.py +232 -15
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/commands.py +8 -2
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/graphql/fragments.py +10 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/graphql/mutations.py +16 -1
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/graphql/queries.py +48 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/graphql/fragments.py +4 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/messaging/provider.py +9 -4
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/monitor/actions.py +28 -6
- primitive-0.2.66/src/primitive/network/actions.py +436 -0
- primitive-0.2.66/src/primitive/network/commands.py +40 -0
- primitive-0.2.66/src/primitive/network/redfish.py +121 -0
- primitive-0.2.66/src/primitive/network/ui.py +19 -0
- primitive-0.2.66/src/primitive/utils/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/uv.lock +12 -1
- {primitive-0.2.63 → primitive-0.2.66}/.git-hooks/pre-commit +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/.gitattributes +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/.github/workflows/lint.yml +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/.github/workflows/publish.yml +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/.github/workflows/pyright.yml +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/.gitignore +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/.vscode/extensions.json +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/LICENSE.txt +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/README.md +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/linux setup.md +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/agent/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/agent/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/agent/uploader.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/auth/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/auth/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/auth/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/auth/graphql/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/auth/graphql/queries.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/daemons/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/daemons/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/daemons/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/daemons/launch_agents.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/daemons/launch_service.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/daemons/ui.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/exec/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/exec/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/exec/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/exec/interactive.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/graphql/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/graphql/fragments.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/graphql/mutations.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/graphql/queries.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/files/ui.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/git/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/git/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/git/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/git/graphql/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/git/graphql/queries.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/graphql/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/graphql/relay.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/graphql/sdk.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/graphql/utility_fragments.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/android.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/graphql/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/hardware/ui.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/commands.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/graphql/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/graphql/mutations.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/jobs/graphql/queries.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/messaging/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/monitor/commands.py +0 -0
- {primitive-0.2.63/src/primitive/organizations → primitive-0.2.66/src/primitive/network}/__init__.py +0 -0
- {primitive-0.2.63/src/primitive/organizations/graphql → primitive-0.2.66/src/primitive/organizations}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/organizations/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/organizations/commands.py +0 -0
- {primitive-0.2.63/src/primitive/projects → primitive-0.2.66/src/primitive/organizations/graphql}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/organizations/graphql/fragments.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/organizations/graphql/mutations.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/organizations/graphql/queries.py +0 -0
- {primitive-0.2.63/src/primitive/projects/graphql → primitive-0.2.66/src/primitive/projects}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/projects/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/projects/commands.py +0 -0
- {primitive-0.2.63/src/primitive/provisioning → primitive-0.2.66/src/primitive/projects/graphql}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/projects/graphql/fragments.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/projects/graphql/mutations.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/projects/graphql/queries.py +0 -0
- {primitive-0.2.63/src/primitive/provisioning/graphql → primitive-0.2.66/src/primitive/provisioning}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/provisioning/actions.py +0 -0
- {primitive-0.2.63/src/primitive/reservations → primitive-0.2.66/src/primitive/provisioning/graphql}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/provisioning/graphql/queries.py +0 -0
- {primitive-0.2.63/src/primitive/reservations/graphql → primitive-0.2.66/src/primitive/reservations}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/reservations/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/reservations/commands.py +0 -0
- {primitive-0.2.63/src/primitive/utils → primitive-0.2.66/src/primitive/reservations/graphql}/__init__.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/reservations/graphql/fragments.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/reservations/graphql/mutations.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/reservations/graphql/queries.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/actions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/auth.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/cache.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/chunk_size.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/config.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/daemons.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/exceptions.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/logging.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/memory_size.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/printer.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/psutil.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/shell.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/text.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/src/primitive/utils/x509.py +0 -0
- {primitive-0.2.63 → primitive-0.2.66}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: primitive
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.66
|
|
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
|
|
@@ -24,6 +24,7 @@ Requires-Dist: loguru
|
|
|
24
24
|
Requires-Dist: paramiko[invoke]
|
|
25
25
|
Requires-Dist: pika>=1.3.2
|
|
26
26
|
Requires-Dist: psutil>=7.0.0
|
|
27
|
+
Requires-Dist: pyserial>=3.5
|
|
27
28
|
Requires-Dist: rich>=13.9.4
|
|
28
29
|
Requires-Dist: speedtest-cli
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
@@ -36,10 +36,11 @@ dependencies = [
|
|
|
36
36
|
"rich>=13.9.4",
|
|
37
37
|
"psutil>=7.0.0",
|
|
38
38
|
"pika>=1.3.2",
|
|
39
|
+
"pyserial>=3.5",
|
|
39
40
|
]
|
|
40
41
|
|
|
41
|
-
[
|
|
42
|
-
dev
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = ["ipdb", "ruff", "pyright", "isort"]
|
|
43
44
|
|
|
44
45
|
[project.urls]
|
|
45
46
|
Documentation = "https://github.com//primitivecorp/primitive-cli#readme"
|
|
@@ -53,7 +53,9 @@ class Agent(BaseAction):
|
|
|
53
53
|
# - then wait for the JobRun to be in_progress from the API
|
|
54
54
|
|
|
55
55
|
active_reservation_id = None
|
|
56
|
+
hardware = None
|
|
56
57
|
job_run_data: dict = {}
|
|
58
|
+
active_reservation_data: dict = {}
|
|
57
59
|
|
|
58
60
|
if RUNNING_IN_CONTAINER and job_run_id:
|
|
59
61
|
job_run_result = self.primitive.jobs.get_job_run(id=job_run_id)
|
|
@@ -81,16 +83,43 @@ class Agent(BaseAction):
|
|
|
81
83
|
if RUNNING_IN_CONTAINER:
|
|
82
84
|
logger.info("Running in container, exiting due to no JobRun.")
|
|
83
85
|
break
|
|
84
|
-
logger.debug("No pending Job Run found
|
|
86
|
+
logger.debug("No pending Job Run found. [sleeping 5 seconds]")
|
|
85
87
|
sleep(5)
|
|
86
88
|
continue
|
|
87
89
|
|
|
88
90
|
logger.debug("Found pending Job Run")
|
|
89
|
-
logger.debug(f"Job Run ID: {job_run_data.get('id')}")
|
|
90
|
-
logger.debug(
|
|
91
|
+
logger.debug(f"Job Run ID: {job_run_data.get('id', 'No Job ID Found')}")
|
|
92
|
+
logger.debug(
|
|
93
|
+
f"Job Name: {job_run_data.get('job', {}).get('name', 'No Job Name Found')}"
|
|
94
|
+
)
|
|
91
95
|
|
|
92
96
|
job_run_status = job_run_data.get("status", None)
|
|
93
97
|
|
|
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
|
+
)
|
|
104
|
+
target_hardware_id = None
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
hardware_id is not None
|
|
108
|
+
and execution_hardware_id is not None
|
|
109
|
+
and (hardware_id != execution_hardware_id)
|
|
110
|
+
):
|
|
111
|
+
logger.info(
|
|
112
|
+
f"Job Run {job_run_id} is being executed by the controller. Agent may stop. [sleeping 5 seconds]"
|
|
113
|
+
)
|
|
114
|
+
sleep(5)
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
if active_reservation_data and active_reservation_id:
|
|
118
|
+
for hardware in active_reservation_data.get("hardware", []):
|
|
119
|
+
if hardware.get("id", None) != execution_hardware_id:
|
|
120
|
+
target_hardware_id = hardware.get("id", None)
|
|
121
|
+
break
|
|
122
|
+
|
|
94
123
|
while True:
|
|
95
124
|
if job_run_status == "pending":
|
|
96
125
|
# we are setting to request_in_progress here which puts a started_at time on the JobRun in the API's database
|
|
@@ -135,6 +164,7 @@ class Agent(BaseAction):
|
|
|
135
164
|
runner = Runner(
|
|
136
165
|
primitive=self.primitive,
|
|
137
166
|
job_run=job_run_data,
|
|
167
|
+
target_hardware_id=target_hardware_id,
|
|
138
168
|
)
|
|
139
169
|
runner.setup()
|
|
140
170
|
except Exception as exception:
|
|
@@ -5,6 +5,7 @@ import typing
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from pathlib import Path, PurePath
|
|
7
7
|
from typing import Dict, List, TypedDict
|
|
8
|
+
from datetime import datetime, timezone
|
|
8
9
|
|
|
9
10
|
from loguru import logger
|
|
10
11
|
|
|
@@ -42,12 +43,14 @@ class Runner:
|
|
|
42
43
|
self,
|
|
43
44
|
primitive: "primitive.client.Primitive",
|
|
44
45
|
job_run: Dict,
|
|
46
|
+
target_hardware_id: str | None = None,
|
|
45
47
|
max_log_size: int = 10 * 1024 * 1024, # 10MB
|
|
46
48
|
) -> None:
|
|
47
49
|
self.primitive = primitive
|
|
48
50
|
self.job = job_run["job"]
|
|
49
51
|
self.job_run = job_run
|
|
50
52
|
self.job_settings = job_run["jobSettings"]
|
|
53
|
+
self.target_hardware_id = target_hardware_id
|
|
51
54
|
self.config = job_run["jobSettings"]["config"]
|
|
52
55
|
self.initial_env = {}
|
|
53
56
|
self.modified_env = {}
|
|
@@ -64,6 +67,14 @@ class Runner:
|
|
|
64
67
|
backtrace=True,
|
|
65
68
|
)
|
|
66
69
|
|
|
70
|
+
if self.target_hardware_id is not None:
|
|
71
|
+
target_hardware_secret = self.primitive.hardware.get_hardware_secret(
|
|
72
|
+
hardware_id=self.target_hardware_id
|
|
73
|
+
)
|
|
74
|
+
self.target_hardware_secret = {
|
|
75
|
+
k: v for k, v in target_hardware_secret.items() if v is not None
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
@log_context(label="setup")
|
|
68
79
|
def setup(self) -> None:
|
|
69
80
|
# Attempt to download the job source code
|
|
@@ -217,11 +228,96 @@ class Runner:
|
|
|
217
228
|
async def run_task(self, task: Task) -> bool:
|
|
218
229
|
logger.info(f"Running step '{task['label']}'")
|
|
219
230
|
commands = task["cmd"].strip().split("\n")
|
|
231
|
+
|
|
220
232
|
for i, cmd in enumerate(commands):
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
233
|
+
if cmd == "oobpowercycle":
|
|
234
|
+
from primitive.network.redfish import RedfishClient
|
|
235
|
+
|
|
236
|
+
bmc_host = self.target_hardware_secret.get("bmcHostname", None)
|
|
237
|
+
bmc_username = self.target_hardware_secret.get("bmcUsername", None)
|
|
238
|
+
bmc_password = self.target_hardware_secret.get("bmcPassword", "")
|
|
239
|
+
|
|
240
|
+
if bmc_host is None:
|
|
241
|
+
logger.error(
|
|
242
|
+
"No BMC host found in target hardware secret for out-of-band power cycle"
|
|
243
|
+
)
|
|
244
|
+
return True
|
|
245
|
+
if bmc_username is None:
|
|
246
|
+
logger.error(
|
|
247
|
+
"No BMC username found in target hardware secret for out-of-band power cycle"
|
|
248
|
+
)
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
redfish = RedfishClient(
|
|
252
|
+
host=bmc_host, username=bmc_username, password=bmc_password
|
|
253
|
+
)
|
|
254
|
+
redfish.compute_system_reset(system_id="1", reset_type="ForceRestart")
|
|
255
|
+
if self.target_hardware_id:
|
|
256
|
+
self.primitive.hardware.update_hardware(
|
|
257
|
+
hardware_id=self.target_hardware_id,
|
|
258
|
+
is_online=False,
|
|
259
|
+
is_rebooting=True,
|
|
260
|
+
start_rebooting_at=datetime.now(timezone.utc),
|
|
261
|
+
)
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if cmd == "pxeboot":
|
|
265
|
+
from primitive.network.redfish import RedfishClient
|
|
266
|
+
|
|
267
|
+
bmc_host = self.target_hardware_secret.get("bmcHostname", None)
|
|
268
|
+
bmc_username = self.target_hardware_secret.get("bmcUsername", None)
|
|
269
|
+
bmc_password = self.target_hardware_secret.get("bmcPassword", "")
|
|
270
|
+
|
|
271
|
+
if bmc_host is None:
|
|
272
|
+
logger.error(
|
|
273
|
+
"No BMC host found in target hardware secret for out-of-band power cycle"
|
|
274
|
+
)
|
|
275
|
+
return True
|
|
276
|
+
if bmc_username is None:
|
|
277
|
+
logger.error(
|
|
278
|
+
"No BMC username found in target hardware secret for out-of-band power cycle"
|
|
279
|
+
)
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
redfish = RedfishClient(
|
|
283
|
+
host=bmc_host, username=bmc_username, password=bmc_password
|
|
284
|
+
)
|
|
285
|
+
redfish.update_boot_options(
|
|
286
|
+
system_id="1",
|
|
287
|
+
boot_source_override_target="Pxe",
|
|
288
|
+
boot_source_override_enabled="Once",
|
|
289
|
+
boot_source_override_mode="UEFI",
|
|
290
|
+
)
|
|
291
|
+
redfish.compute_system_reset(system_id="1", reset_type="ForceRestart")
|
|
292
|
+
if self.target_hardware_id:
|
|
293
|
+
self.primitive.hardware.update_hardware(
|
|
294
|
+
hardware_id=self.target_hardware_id,
|
|
295
|
+
is_online=False,
|
|
296
|
+
is_rebooting=True,
|
|
297
|
+
start_rebooting_at=datetime.now(timezone.utc),
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
|
|
224
301
|
args = ["/bin/bash", "-c", cmd]
|
|
302
|
+
if self.target_hardware_secret:
|
|
303
|
+
username = self.target_hardware_secret.get("username")
|
|
304
|
+
password = self.target_hardware_secret.get("password")
|
|
305
|
+
hostname = self.target_hardware_secret.get("hostname")
|
|
306
|
+
args = [
|
|
307
|
+
"sshpass",
|
|
308
|
+
"-p",
|
|
309
|
+
password,
|
|
310
|
+
"ssh",
|
|
311
|
+
"-o",
|
|
312
|
+
"StrictHostKeyChecking=no",
|
|
313
|
+
"-o",
|
|
314
|
+
"UserKnownHostsFile=/dev/null",
|
|
315
|
+
"-o",
|
|
316
|
+
"IdentitiesOnly=yes",
|
|
317
|
+
f"{username}@{hostname}",
|
|
318
|
+
"--",
|
|
319
|
+
f'/bin/bash -c "cd {task.get("workdir", "~/")} && {cmd}"',
|
|
320
|
+
]
|
|
225
321
|
|
|
226
322
|
logger.info(
|
|
227
323
|
f"Executing command {i + 1}/{len(commands)}: {cmd} at {self.source_dir / task.get('workdir', '')}"
|
|
@@ -17,6 +17,7 @@ from .organizations.commands import cli as organizations_commands
|
|
|
17
17
|
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
|
+
from .network.commands import cli as network_commands
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@click.group()
|
|
@@ -73,6 +74,7 @@ cli.add_command(projects_commands, "projects")
|
|
|
73
74
|
cli.add_command(reservations_commands, "reservations")
|
|
74
75
|
cli.add_command(exec_commands, "exec")
|
|
75
76
|
cli.add_command(monitor_commands, "monitor")
|
|
77
|
+
cli.add_command(network_commands, "network")
|
|
76
78
|
|
|
77
79
|
if __name__ == "__main__":
|
|
78
80
|
cli(obj={})
|
|
@@ -16,6 +16,7 @@ from .git.actions import Git
|
|
|
16
16
|
from .hardware.actions import Hardware
|
|
17
17
|
from .jobs.actions import Jobs
|
|
18
18
|
from .monitor.actions import Monitor
|
|
19
|
+
from .network.actions import Network
|
|
19
20
|
from .organizations.actions import Organizations
|
|
20
21
|
from .projects.actions import Projects
|
|
21
22
|
from .provisioning.actions import Provisioning
|
|
@@ -97,6 +98,7 @@ class Primitive:
|
|
|
97
98
|
self.exec: Exec = Exec(self)
|
|
98
99
|
self.provisioning: Provisioning = Provisioning(self)
|
|
99
100
|
self.monitor: Monitor = Monitor(self)
|
|
101
|
+
self.network: Network = Network(self)
|
|
100
102
|
|
|
101
103
|
def get_host_config(self):
|
|
102
104
|
self.full_config = read_config_file()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import csv
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
import io
|
|
3
4
|
import json
|
|
4
5
|
import platform
|
|
@@ -10,6 +11,7 @@ from typing import Dict, List, Optional
|
|
|
10
11
|
|
|
11
12
|
import click
|
|
12
13
|
import psutil
|
|
14
|
+
from socket import AddressFamily
|
|
13
15
|
from aiohttp import client_exceptions
|
|
14
16
|
from gql import gql
|
|
15
17
|
from loguru import logger
|
|
@@ -22,20 +24,22 @@ from ..utils.auth import guard
|
|
|
22
24
|
from ..utils.config import update_config_file
|
|
23
25
|
from ..utils.exceptions import P_CLI_100
|
|
24
26
|
from .graphql.mutations import (
|
|
27
|
+
hardware_certificate_create_mutation,
|
|
25
28
|
hardware_checkin_mutation,
|
|
26
29
|
hardware_update_mutation,
|
|
30
|
+
hardware_update_system_info_mutation,
|
|
27
31
|
register_child_hardware_mutation,
|
|
28
32
|
register_hardware_mutation,
|
|
29
33
|
unregister_hardware_mutation,
|
|
30
|
-
hardware_certificate_create_mutation,
|
|
31
34
|
)
|
|
32
35
|
from .graphql.queries import (
|
|
33
36
|
hardware_details,
|
|
34
37
|
hardware_list,
|
|
35
38
|
nested_children_hardware_list,
|
|
39
|
+
hardware_secret,
|
|
40
|
+
hardware_with_parent_list,
|
|
36
41
|
)
|
|
37
42
|
|
|
38
|
-
|
|
39
43
|
if typing.TYPE_CHECKING:
|
|
40
44
|
pass
|
|
41
45
|
|
|
@@ -199,6 +203,34 @@ class Hardware(BaseAction):
|
|
|
199
203
|
|
|
200
204
|
return gpu_config
|
|
201
205
|
|
|
206
|
+
def _get_network_interfaces(self) -> Dict[str, str]:
|
|
207
|
+
os_family = platform.system()
|
|
208
|
+
network_interfaces = {}
|
|
209
|
+
for interface, addresses in psutil.net_if_addrs().items():
|
|
210
|
+
if (
|
|
211
|
+
interface in ["lo", "lo0", "awdl0", "llw0"]
|
|
212
|
+
or interface.startswith("utun")
|
|
213
|
+
or interface.startswith("bridge")
|
|
214
|
+
):
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
network_interfaces[interface] = {}
|
|
218
|
+
for address in addresses:
|
|
219
|
+
# get linux's mac address
|
|
220
|
+
if os_family == "Linux" and address.family == AddressFamily.AF_PACKET:
|
|
221
|
+
mac_address = address.address
|
|
222
|
+
if mac_address and mac_address != "00:00:00:00:00:00":
|
|
223
|
+
network_interfaces[interface]["mac_address"] = mac_address
|
|
224
|
+
if os_family == "Darwin" and address.family == AddressFamily.AF_LINK:
|
|
225
|
+
mac_address = address.address
|
|
226
|
+
if mac_address and mac_address != "00:00:00:00:00:00":
|
|
227
|
+
network_interfaces[interface]["mac_address"] = mac_address
|
|
228
|
+
elif address.family == AddressFamily.AF_INET:
|
|
229
|
+
ip_address = address.address
|
|
230
|
+
if ip_address and ip_address != "127.0.0.1":
|
|
231
|
+
network_interfaces[interface]["ip_address"] = ip_address
|
|
232
|
+
return network_interfaces
|
|
233
|
+
|
|
202
234
|
def _get_windows_computer_service_product_values(self) -> Dict[str, str]:
|
|
203
235
|
windows_computer_service_product_csv_command = (
|
|
204
236
|
"cmd.exe /C wmic csproduct get Name, Vendor, Version, UUID /format:csv"
|
|
@@ -289,6 +321,8 @@ class Hardware(BaseAction):
|
|
|
289
321
|
system_info["architecture"] = platform.architecture()[0]
|
|
290
322
|
system_info["cpu_cores"] = str(platform.os.cpu_count()) # type: ignore exits
|
|
291
323
|
system_info["gpu_config"] = self._get_gpu_config()
|
|
324
|
+
system_info["network_interfaces"] = self._get_network_interfaces()
|
|
325
|
+
|
|
292
326
|
return system_info
|
|
293
327
|
|
|
294
328
|
@guard
|
|
@@ -410,7 +444,7 @@ class Hardware(BaseAction):
|
|
|
410
444
|
"systemInfo": system_info,
|
|
411
445
|
}
|
|
412
446
|
|
|
413
|
-
mutation = gql(
|
|
447
|
+
mutation = gql(hardware_update_system_info_mutation)
|
|
414
448
|
|
|
415
449
|
input = new_state
|
|
416
450
|
variables = {"input": input}
|
|
@@ -435,16 +469,26 @@ class Hardware(BaseAction):
|
|
|
435
469
|
is_available: bool = False,
|
|
436
470
|
is_online: bool = True,
|
|
437
471
|
stopping_agent: Optional[bool] = False,
|
|
472
|
+
http: bool = False,
|
|
438
473
|
):
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
474
|
+
if not http and self.primitive.messaging.ready:
|
|
475
|
+
message = {
|
|
476
|
+
"is_healthy": is_healthy,
|
|
477
|
+
"is_quarantined": is_quarantined,
|
|
478
|
+
"is_available": is_available,
|
|
479
|
+
"is_online": is_online,
|
|
480
|
+
}
|
|
481
|
+
self.primitive.messaging.send_message(
|
|
482
|
+
message_type=MESSAGE_TYPES.CHECK_IN, message=message
|
|
483
|
+
)
|
|
484
|
+
else:
|
|
485
|
+
self.check_in_http(
|
|
486
|
+
is_healthy=is_healthy,
|
|
487
|
+
is_quarantined=is_quarantined,
|
|
488
|
+
is_available=is_available,
|
|
489
|
+
is_online=is_online,
|
|
490
|
+
stopping_agent=stopping_agent,
|
|
491
|
+
)
|
|
448
492
|
|
|
449
493
|
@guard
|
|
450
494
|
def check_in_http(
|
|
@@ -544,8 +588,13 @@ class Hardware(BaseAction):
|
|
|
544
588
|
id: Optional[str] = None,
|
|
545
589
|
slug: Optional[str] = None,
|
|
546
590
|
nested_children: Optional[bool] = False,
|
|
591
|
+
parent: Optional[bool] = False,
|
|
547
592
|
):
|
|
548
|
-
query = gql(
|
|
593
|
+
query = gql(hardware_list)
|
|
594
|
+
if parent:
|
|
595
|
+
query = gql(hardware_with_parent_list)
|
|
596
|
+
if nested_children:
|
|
597
|
+
query = gql(nested_children_hardware_list)
|
|
549
598
|
|
|
550
599
|
filters = {
|
|
551
600
|
"isRegistered": {"exact": True},
|
|
@@ -556,8 +605,8 @@ class Hardware(BaseAction):
|
|
|
556
605
|
filters["slug"] = {"exact": slug}
|
|
557
606
|
if id is not None:
|
|
558
607
|
filters["id"] = {"exact": id}
|
|
559
|
-
if nested_children is True:
|
|
560
|
-
|
|
608
|
+
# if nested_children is True:
|
|
609
|
+
# filters["hasParent"] = {"exact": False}
|
|
561
610
|
|
|
562
611
|
variables = {
|
|
563
612
|
"filters": filters,
|
|
@@ -565,6 +614,43 @@ class Hardware(BaseAction):
|
|
|
565
614
|
result = self.primitive.session.execute(
|
|
566
615
|
query, variable_values=variables, get_execution_result=True
|
|
567
616
|
)
|
|
617
|
+
|
|
618
|
+
return result
|
|
619
|
+
|
|
620
|
+
@guard
|
|
621
|
+
async def aget_hardware_list(
|
|
622
|
+
self,
|
|
623
|
+
fingerprint: Optional[str] = None,
|
|
624
|
+
id: Optional[str] = None,
|
|
625
|
+
slug: Optional[str] = None,
|
|
626
|
+
nested_children: Optional[bool] = False,
|
|
627
|
+
parent: Optional[bool] = False,
|
|
628
|
+
):
|
|
629
|
+
query = gql(hardware_list)
|
|
630
|
+
if parent:
|
|
631
|
+
query = gql(hardware_with_parent_list)
|
|
632
|
+
if nested_children:
|
|
633
|
+
query = gql(nested_children_hardware_list)
|
|
634
|
+
|
|
635
|
+
filters = {
|
|
636
|
+
"isRegistered": {"exact": True},
|
|
637
|
+
}
|
|
638
|
+
if fingerprint is not None:
|
|
639
|
+
filters["fingerprint"] = {"exact": fingerprint}
|
|
640
|
+
if slug is not None:
|
|
641
|
+
filters["slug"] = {"exact": slug}
|
|
642
|
+
if id is not None:
|
|
643
|
+
filters["id"] = {"exact": id}
|
|
644
|
+
# if nested_children is True:
|
|
645
|
+
# filters["hasParent"] = {"exact": False}
|
|
646
|
+
|
|
647
|
+
variables = {
|
|
648
|
+
"filters": filters,
|
|
649
|
+
}
|
|
650
|
+
result = await self.primitive.session.execute_async(
|
|
651
|
+
query, variable_values=variables, get_execution_result=True
|
|
652
|
+
)
|
|
653
|
+
|
|
568
654
|
return result
|
|
569
655
|
|
|
570
656
|
@guard
|
|
@@ -601,11 +687,56 @@ class Hardware(BaseAction):
|
|
|
601
687
|
fingerprint=self.primitive.host_config.get("fingerprint"),
|
|
602
688
|
nested_children=True,
|
|
603
689
|
)
|
|
690
|
+
if len(hardware_list_result.data.get("hardwareList").get("edges", [])) == 0:
|
|
691
|
+
raise Exception(
|
|
692
|
+
"No hardware found with fingerprint: "
|
|
693
|
+
f"{self.primitive.host_config.get('fingerprint')}. "
|
|
694
|
+
"Please register: primitive hardware register"
|
|
695
|
+
)
|
|
696
|
+
|
|
604
697
|
hardware = (
|
|
605
698
|
hardware_list_result.data.get("hardwareList").get("edges")[0].get("node")
|
|
606
699
|
)
|
|
607
700
|
return hardware
|
|
608
701
|
|
|
702
|
+
def get_parent_hardware_details(self):
|
|
703
|
+
hardware_list_result = self.get_hardware_list(
|
|
704
|
+
fingerprint=self.primitive.host_config.get("fingerprint"), parent=True
|
|
705
|
+
)
|
|
706
|
+
if len(hardware_list_result.data.get("hardwareList").get("edges", [])) == 0:
|
|
707
|
+
logger.warning(
|
|
708
|
+
"No hardware found with fingerprint: "
|
|
709
|
+
f"{self.primitive.host_config.get('fingerprint')}. "
|
|
710
|
+
"Please register: primitive hardware register"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
hardware = (
|
|
714
|
+
hardware_list_result.data.get("hardwareList").get("edges")[0].get("node")
|
|
715
|
+
)
|
|
716
|
+
parent = hardware.get("parent", None)
|
|
717
|
+
if not parent:
|
|
718
|
+
logger.warning("No parent network device found.")
|
|
719
|
+
return parent
|
|
720
|
+
|
|
721
|
+
async def aget_parent_hardware_details(self):
|
|
722
|
+
hardware_list_result = await self.aget_hardware_list(
|
|
723
|
+
fingerprint=self.primitive.host_config.get("fingerprint"), parent=True
|
|
724
|
+
)
|
|
725
|
+
if len(hardware_list_result.data.get("hardwareList").get("edges", [])) == 0:
|
|
726
|
+
logger.warning(
|
|
727
|
+
"No hardware found with fingerprint: "
|
|
728
|
+
f"{self.primitive.host_config.get('fingerprint')}. "
|
|
729
|
+
"Please register: primitive hardware register"
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
hardware = (
|
|
733
|
+
hardware_list_result.data.get("hardwareList").get("edges")[0].get("node")
|
|
734
|
+
)
|
|
735
|
+
parent = hardware.get("parent", None)
|
|
736
|
+
if not parent:
|
|
737
|
+
logger.warning("No parent network device found.")
|
|
738
|
+
return parent
|
|
739
|
+
|
|
609
740
|
def get_hardware_from_slug_or_id(self, hardware_identifier: str):
|
|
610
741
|
is_id = False
|
|
611
742
|
is_slug = False
|
|
@@ -634,6 +765,56 @@ class Hardware(BaseAction):
|
|
|
634
765
|
|
|
635
766
|
return hardware
|
|
636
767
|
|
|
768
|
+
@guard
|
|
769
|
+
def get_hardware_secret(self, hardware_id: str):
|
|
770
|
+
query = gql(hardware_secret)
|
|
771
|
+
variables = {"hardwareId": hardware_id}
|
|
772
|
+
result = self.primitive.session.execute(
|
|
773
|
+
query, variable_values=variables, get_execution_result=True
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
secret = result.data.get("hardwareSecret", {})
|
|
777
|
+
return secret
|
|
778
|
+
|
|
779
|
+
@guard
|
|
780
|
+
async def aget_hardware_secret(self, hardware_id: str):
|
|
781
|
+
query = gql(hardware_secret)
|
|
782
|
+
variables = {"hardwareId": hardware_id}
|
|
783
|
+
result = await self.primitive.session.execute_async(
|
|
784
|
+
query, variable_values=variables, get_execution_result=True
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
secret = result.data.get("hardwareSecret", {})
|
|
788
|
+
return secret
|
|
789
|
+
|
|
790
|
+
@guard
|
|
791
|
+
def get_and_set_switch_info(self):
|
|
792
|
+
parent = self.get_parent_hardware_details()
|
|
793
|
+
if not parent:
|
|
794
|
+
return
|
|
795
|
+
|
|
796
|
+
parent_secret = self.get_hardware_secret(hardware_id=parent.get("id"))
|
|
797
|
+
self.primitive.network.switch_connection_info = {
|
|
798
|
+
"vendor": parent.get("manufacturer", {}).get("slug"),
|
|
799
|
+
"hostname": parent_secret.get("hostname"),
|
|
800
|
+
"username": parent_secret.get("username"),
|
|
801
|
+
"password": parent_secret.get("password"),
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
@guard
|
|
805
|
+
async def aget_and_set_switch_info(self):
|
|
806
|
+
parent = await self.aget_parent_hardware_details()
|
|
807
|
+
if not parent:
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
parent_secret = await self.aget_hardware_secret(hardware_id=parent.get("id"))
|
|
811
|
+
self.primitive.network.switch_connection_info = {
|
|
812
|
+
"vendor": parent.get("manufacturer", {}).get("slug"),
|
|
813
|
+
"hostname": parent_secret.get("hostname"),
|
|
814
|
+
"username": parent_secret.get("username"),
|
|
815
|
+
"password": parent_secret.get("password"),
|
|
816
|
+
}
|
|
817
|
+
|
|
637
818
|
@guard
|
|
638
819
|
def register_child(self, child: AndroidDevice):
|
|
639
820
|
system_info = child.system_info
|
|
@@ -764,3 +945,39 @@ class Hardware(BaseAction):
|
|
|
764
945
|
"disk_free": disk_usage.free,
|
|
765
946
|
}
|
|
766
947
|
return metrics
|
|
948
|
+
|
|
949
|
+
@guard
|
|
950
|
+
def update_hardware(
|
|
951
|
+
self,
|
|
952
|
+
hardware_id: str,
|
|
953
|
+
is_online: Optional[bool] = None,
|
|
954
|
+
is_rebooting: Optional[bool] = None,
|
|
955
|
+
start_rebooting_at: Optional[datetime] = None,
|
|
956
|
+
):
|
|
957
|
+
new_state: dict = {
|
|
958
|
+
"id": hardware_id,
|
|
959
|
+
}
|
|
960
|
+
if is_online is not None:
|
|
961
|
+
new_state["isOnline"] = is_online
|
|
962
|
+
if is_rebooting is not None:
|
|
963
|
+
new_state["isRebooting"] = is_rebooting
|
|
964
|
+
if start_rebooting_at is not None:
|
|
965
|
+
new_state["startRebootingAt"] = start_rebooting_at
|
|
966
|
+
|
|
967
|
+
mutation = gql(hardware_update_mutation)
|
|
968
|
+
|
|
969
|
+
input = new_state
|
|
970
|
+
variables = {"input": input}
|
|
971
|
+
try:
|
|
972
|
+
result = self.primitive.session.execute(
|
|
973
|
+
mutation, variable_values=variables, get_execution_result=True
|
|
974
|
+
)
|
|
975
|
+
except client_exceptions.ClientConnectorError as exception:
|
|
976
|
+
message = "Failed to update hardware! "
|
|
977
|
+
logger.exception(message)
|
|
978
|
+
raise exception
|
|
979
|
+
|
|
980
|
+
message = "Updated hardware successfully! "
|
|
981
|
+
logger.info(message)
|
|
982
|
+
|
|
983
|
+
return result
|
|
@@ -101,11 +101,17 @@ def unregister_command(context):
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
@cli.command("checkin")
|
|
104
|
+
@click.option(
|
|
105
|
+
"--http",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
default=False,
|
|
108
|
+
help="Use HTTP instead of amqp for check-in",
|
|
109
|
+
)
|
|
104
110
|
@click.pass_context
|
|
105
|
-
def checkin_command(context):
|
|
111
|
+
def checkin_command(context, http: bool = False):
|
|
106
112
|
"""Checkin Hardware with Primitive"""
|
|
107
113
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
|
108
|
-
primitive.hardware.check_in()
|
|
114
|
+
primitive.hardware.check_in(http=http)
|
|
109
115
|
|
|
110
116
|
|
|
111
117
|
@cli.command("list")
|