primitive 0.2.68__py3-none-any.whl → 0.2.72__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.
Potentially problematic release.
This version of primitive might be problematic. Click here for more details.
- primitive/__about__.py +1 -1
- primitive/agent/actions.py +8 -5
- primitive/agent/pxe.py +71 -0
- primitive/agent/runner.py +56 -45
- primitive/cli.py +2 -0
- primitive/client.py +2 -0
- primitive/files/actions.py +23 -1
- primitive/hardware/actions.py +202 -2
- primitive/hardware/commands.py +13 -2
- primitive/messaging/provider.py +1 -0
- primitive/monitor/actions.py +11 -6
- primitive/network/actions.py +89 -44
- primitive/network/commands.py +17 -6
- primitive/network/ssh.py +53 -12
- primitive/network/ui.py +9 -3
- primitive/operating_systems/__init__.py +0 -0
- primitive/operating_systems/actions.py +473 -0
- primitive/operating_systems/commands.py +246 -0
- primitive/operating_systems/graphql/__init__.py +0 -0
- primitive/operating_systems/graphql/mutations.py +32 -0
- primitive/operating_systems/graphql/queries.py +36 -0
- primitive/organizations/actions.py +6 -0
- primitive/utils/cache.py +11 -0
- primitive/utils/checksums.py +44 -0
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/METADATA +1 -1
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/RECORD +29 -21
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/WHEEL +0 -0
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/licenses/LICENSE.txt +0 -0
primitive/__about__.py
CHANGED
primitive/agent/actions.py
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
primitive/agent/pxe.py
ADDED
|
@@ -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
|
primitive/agent/runner.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
primitive/cli.py
CHANGED
|
@@ -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={})
|
primitive/client.py
CHANGED
|
@@ -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()
|
primitive/files/actions.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
primitive/hardware/actions.py
CHANGED
|
@@ -283,7 +283,180 @@ class Hardware(BaseAction):
|
|
|
283
283
|
return {"linux_machine_id": machine_id}
|
|
284
284
|
return {}
|
|
285
285
|
|
|
286
|
-
def
|
|
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"] =
|
|
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
|
+
)
|
primitive/hardware/commands.py
CHANGED
|
@@ -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(
|
|
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
|
|
primitive/messaging/provider.py
CHANGED
|
@@ -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"
|