primitive 0.1.88__tar.gz → 0.1.90__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.
- {primitive-0.1.88 → primitive-0.1.90}/PKG-INFO +2 -1
- {primitive-0.1.88 → primitive-0.1.90}/pyproject.toml +1 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/__about__.py +1 -1
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/actions.py +6 -1
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/runner.py +6 -2
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/actions.py +40 -3
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/actions.py +1 -3
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/actions.py +119 -15
- primitive-0.1.90/src/primitive/hardware/android.py +75 -0
- primitive-0.1.90/src/primitive/hardware/commands.py +155 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/graphql/fragments.py +11 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/graphql/mutations.py +29 -0
- primitive-0.1.90/src/primitive/hardware/graphql/queries.py +97 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/actions.py +52 -6
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/commands.py +5 -2
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/shell.py +7 -1
- {primitive-0.1.88 → primitive-0.1.90}/uv.lock +305 -279
- primitive-0.1.88/src/primitive/hardware/commands.py +0 -63
- primitive-0.1.88/src/primitive/hardware/graphql/queries.py +0 -31
- {primitive-0.1.88 → primitive-0.1.90}/.git-hooks/pre-commit +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/.gitattributes +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/.github/workflows/lint.yml +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/.github/workflows/publish.yml +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/.gitignore +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/.vscode/settings.json +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/LICENSE.txt +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/Makefile +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/README.md +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/linux setup.md +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/process.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/provision.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/uploader.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/cli.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/client.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/launch_agents.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/launch_service.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/interactive.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/fragments.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/mutations.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/relay.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/sdk.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/utility_fragments.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/fragments.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/mutations.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/fragments.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/mutations.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/commands.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/fragments.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/mutations.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/fragments.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/mutations.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/queries.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/__init__.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/actions.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/auth.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/cache.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/chunk_size.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/config.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/files.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/git.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/memory_size.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/printer.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/verible.py +0 -0
- {primitive-0.1.88 → primitive-0.1.90}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: primitive
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.90
|
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: primitive-pal==0.1.4
|
26
26
|
Requires-Dist: pyyaml
|
27
|
+
Requires-Dist: rich>=13.9.4
|
27
28
|
Requires-Dist: speedtest-cli
|
28
29
|
Description-Content-Type: text/markdown
|
29
30
|
|
@@ -28,7 +28,12 @@ class Agent(BaseAction):
|
|
28
28
|
|
29
29
|
# self.primitive.hardware.update_hardware_system_info()
|
30
30
|
try:
|
31
|
-
|
31
|
+
# hey stupid:
|
32
|
+
# do not set is_available to True here, it will mess up the reservation logic
|
33
|
+
# only set is_available after we've checked that no active reservation is present
|
34
|
+
# setting is_available of the parent also effects the children,
|
35
|
+
# which may have active reservations as well
|
36
|
+
self.primitive.hardware.check_in_http(is_online=True)
|
32
37
|
except Exception as ex:
|
33
38
|
logger.error(f"Error checking in hardware: {ex}")
|
34
39
|
sys.exit(1)
|
@@ -193,7 +193,9 @@ class AgentRunner:
|
|
193
193
|
|
194
194
|
returncode = proc.wait()
|
195
195
|
|
196
|
-
logger.debug(
|
196
|
+
logger.debug(
|
197
|
+
f"Process {step['name']} finished with return code {returncode}"
|
198
|
+
)
|
197
199
|
|
198
200
|
if proc.errors > 0 and self.failure_level >= FailureLevel.ERROR:
|
199
201
|
fail_level_detected = True
|
@@ -212,7 +214,9 @@ class AgentRunner:
|
|
212
214
|
self.primitive.jobs.job_run_update(
|
213
215
|
self.job_id, status="request_completed", conclusion="failure"
|
214
216
|
)
|
215
|
-
logger.error(
|
217
|
+
logger.error(
|
218
|
+
f"Step {step['name']} failed with return code {returncode}"
|
219
|
+
)
|
216
220
|
return
|
217
221
|
|
218
222
|
if fail_level_detected and self.parse_logs:
|
@@ -6,6 +6,7 @@ if typing.TYPE_CHECKING:
|
|
6
6
|
pass
|
7
7
|
|
8
8
|
|
9
|
+
from loguru import logger
|
9
10
|
from paramiko import SSHClient
|
10
11
|
|
11
12
|
from primitive.utils.actions import BaseAction
|
@@ -16,6 +17,15 @@ class Exec(BaseAction):
|
|
16
17
|
super().__init__(*args, **kwargs)
|
17
18
|
|
18
19
|
def execute_command(self, hardware_identifier: str, command: str) -> None:
|
20
|
+
child_hardware = None
|
21
|
+
if "." in hardware_identifier:
|
22
|
+
hardware_slugs = hardware_identifier.split(".")
|
23
|
+
hardware_identifier = hardware_slugs[0]
|
24
|
+
child_hardware_identifier = hardware_slugs[-1]
|
25
|
+
child_hardware = self.primitive.hardware.get_hardware_from_slug_or_id(
|
26
|
+
hardware_identifier=child_hardware_identifier
|
27
|
+
)
|
28
|
+
|
19
29
|
hardware = self.primitive.hardware.get_hardware_from_slug_or_id(
|
20
30
|
hardware_identifier=hardware_identifier
|
21
31
|
)
|
@@ -37,14 +47,23 @@ class Exec(BaseAction):
|
|
37
47
|
reservation_result = self.primitive.reservations.create_reservation(
|
38
48
|
requested_hardware_ids=[hardware["id"]],
|
39
49
|
reason="Executing command from Primitive CLI",
|
50
|
+
wait=False,
|
40
51
|
)
|
41
52
|
reservation = reservation_result.data["reservationCreate"]
|
42
53
|
created_reservation_on_behalf_of_user = True
|
43
54
|
|
44
|
-
|
55
|
+
reservation_result = self.primitive.reservations.wait_for_reservation_status(
|
45
56
|
reservation_id=reservation["id"], desired_status="in_progress"
|
46
57
|
)
|
47
58
|
|
59
|
+
reservation = reservation_result.data["reservation"]
|
60
|
+
if reservation.get("status") != "in_progress":
|
61
|
+
logger.enable("primitive")
|
62
|
+
logger.info(
|
63
|
+
f"Reservation {reservation.get('id')} is in status {reservation.get('status')}, cannot execute command at this time."
|
64
|
+
)
|
65
|
+
return
|
66
|
+
|
48
67
|
ssh_hostname = hardware["hostname"]
|
49
68
|
ssh_username = hardware["sshUsername"]
|
50
69
|
|
@@ -57,11 +76,29 @@ class Exec(BaseAction):
|
|
57
76
|
)
|
58
77
|
|
59
78
|
if command:
|
60
|
-
|
79
|
+
if child_hardware:
|
80
|
+
# if the child hardware has ssh credentials, format the proxy command
|
81
|
+
if child_hardware.get("systemInfo").get("os_family"):
|
82
|
+
formatted_command = (
|
83
|
+
f"adb -s {child_hardware.get('slug')} shell {command}"
|
84
|
+
)
|
85
|
+
else:
|
86
|
+
# happy path!
|
87
|
+
formatted_command = " ".join(command)
|
88
|
+
|
61
89
|
stdin, stdout, stderr = ssh_client.exec_command(formatted_command)
|
62
|
-
|
90
|
+
|
91
|
+
stdout_string = stdout.read().decode("utf-8").rstrip("\n")
|
92
|
+
stderr_string = stderr.read().decode("utf-8").rstrip("\n")
|
93
|
+
if stdout_string != b"":
|
94
|
+
print(stdout_string)
|
95
|
+
if stderr.read() != b"":
|
96
|
+
print(stderr_string)
|
97
|
+
|
63
98
|
ssh_client.close()
|
64
99
|
else:
|
100
|
+
# if the child hardware has ssh credentials, format the proxy jump
|
101
|
+
|
65
102
|
channel = ssh_client.get_transport().open_session()
|
66
103
|
channel.get_pty()
|
67
104
|
channel.invoke_shell()
|
@@ -38,9 +38,7 @@ class Git(BaseAction):
|
|
38
38
|
source_dir = Path(destination).joinpath(git_repo_full_name.split("/")[-1])
|
39
39
|
|
40
40
|
try:
|
41
|
-
run(
|
42
|
-
["git", "clone", url, source_dir, "--no-checkout"], check=True
|
43
|
-
)
|
41
|
+
run(["git", "clone", url, source_dir, "--no-checkout"], check=True)
|
44
42
|
except CalledProcessError:
|
45
43
|
raise Exception("Failed to download repository")
|
46
44
|
|
@@ -20,15 +20,23 @@ from ..utils.config import update_config_file
|
|
20
20
|
from .graphql.mutations import (
|
21
21
|
hardware_checkin_mutation,
|
22
22
|
hardware_update_mutation,
|
23
|
+
register_child_hardware_mutation,
|
23
24
|
register_hardware_mutation,
|
25
|
+
unregister_hardware_mutation,
|
26
|
+
)
|
27
|
+
from .graphql.queries import (
|
28
|
+
hardware_details,
|
29
|
+
hardware_list,
|
30
|
+
nested_children_hardware_list,
|
24
31
|
)
|
25
|
-
from .graphql.queries import hardware_list
|
26
32
|
|
27
33
|
if typing.TYPE_CHECKING:
|
28
34
|
pass
|
29
35
|
|
30
36
|
|
37
|
+
from primitive.hardware.android import AndroidDevice, list_devices
|
31
38
|
from primitive.utils.actions import BaseAction
|
39
|
+
from primitive.utils.shell import does_executable_exist
|
32
40
|
|
33
41
|
|
34
42
|
class Hardware(BaseAction):
|
@@ -40,6 +48,7 @@ class Hardware(BaseAction):
|
|
40
48
|
"isAvailable": False,
|
41
49
|
"isOnline": False,
|
42
50
|
}
|
51
|
+
self.children = []
|
43
52
|
|
44
53
|
def _get_darwin_system_profiler_values(self) -> Dict[str, str]:
|
45
54
|
system_profiler_hardware_data_type = subprocess.check_output(
|
@@ -276,10 +285,12 @@ class Hardware(BaseAction):
|
|
276
285
|
return system_info
|
277
286
|
|
278
287
|
@guard
|
279
|
-
def register(self):
|
288
|
+
def register(self, organization_id: Optional[str] = None):
|
280
289
|
system_info = self.get_system_info()
|
281
290
|
mutation = gql(register_hardware_mutation)
|
282
291
|
input = {"systemInfo": system_info}
|
292
|
+
if organization_id:
|
293
|
+
input["organizationId"] = organization_id
|
283
294
|
variables = {"input": input}
|
284
295
|
result = self.primitive.session.execute(
|
285
296
|
mutation, variable_values=variables, get_execution_result=True
|
@@ -303,6 +314,30 @@ class Hardware(BaseAction):
|
|
303
314
|
# and headers are set correctly
|
304
315
|
self.primitive.get_host_config()
|
305
316
|
self.check_in_http(is_healthy=True)
|
317
|
+
for child in self._list_local_children():
|
318
|
+
self.register_child(child=child)
|
319
|
+
return result
|
320
|
+
|
321
|
+
@guard
|
322
|
+
def unregister(self, organization_id: Optional[str] = None):
|
323
|
+
mutation = gql(unregister_hardware_mutation)
|
324
|
+
input = {
|
325
|
+
"fingerprint": self.primitive.host_config.get("fingerprint"),
|
326
|
+
}
|
327
|
+
variables = {"input": input}
|
328
|
+
result = self.primitive.session.execute(
|
329
|
+
mutation, variable_values=variables, get_execution_result=True
|
330
|
+
)
|
331
|
+
|
332
|
+
if messages := result.data.get("unregisterHardware").get("messages"):
|
333
|
+
for message in messages:
|
334
|
+
logger.enable("primitive")
|
335
|
+
if message.get("kind") == "ERROR":
|
336
|
+
logger.error(message.get("message"))
|
337
|
+
else:
|
338
|
+
logger.debug(message.get("message"))
|
339
|
+
return False
|
340
|
+
|
306
341
|
return result
|
307
342
|
|
308
343
|
@guard
|
@@ -355,6 +390,7 @@ class Hardware(BaseAction):
|
|
355
390
|
is_online: bool = True,
|
356
391
|
):
|
357
392
|
fingerprint = self.primitive.host_config.get("fingerprint", None)
|
393
|
+
|
358
394
|
if not fingerprint:
|
359
395
|
message = (
|
360
396
|
"No fingerprint found. Please register: primitive hardware register"
|
@@ -417,15 +453,50 @@ class Hardware(BaseAction):
|
|
417
453
|
|
418
454
|
@guard
|
419
455
|
def get_hardware_list(
|
420
|
-
self,
|
456
|
+
self,
|
457
|
+
fingerprint: Optional[str] = None,
|
458
|
+
id: Optional[str] = None,
|
459
|
+
slug: Optional[str] = None,
|
460
|
+
nested_children: Optional[bool] = False,
|
421
461
|
):
|
422
|
-
query = gql(hardware_list)
|
462
|
+
query = gql(nested_children_hardware_list if nested_children else hardware_list)
|
463
|
+
|
464
|
+
filters = {
|
465
|
+
"isRegistered": {"exact": True},
|
466
|
+
}
|
467
|
+
if fingerprint:
|
468
|
+
filters["fingerprint"] = {"exact": fingerprint}
|
469
|
+
if slug:
|
470
|
+
filters["slug"] = {"exact": slug}
|
471
|
+
if id:
|
472
|
+
filters["id"] = {"exact": id}
|
473
|
+
if nested_children:
|
474
|
+
filters["hasParent"] = {"exact": False}
|
475
|
+
|
476
|
+
variables = {
|
477
|
+
"filters": filters,
|
478
|
+
}
|
479
|
+
result = self.primitive.session.execute(
|
480
|
+
query, variable_values=variables, get_execution_result=True
|
481
|
+
)
|
482
|
+
return result
|
483
|
+
|
484
|
+
@guard
|
485
|
+
def get_hardware_details(
|
486
|
+
self,
|
487
|
+
fingerprint: Optional[str] = None,
|
488
|
+
id: Optional[str] = None,
|
489
|
+
slug: Optional[str] = None,
|
490
|
+
):
|
491
|
+
query = gql(hardware_details)
|
423
492
|
|
424
493
|
filters = {}
|
425
494
|
if fingerprint:
|
426
495
|
filters["fingerprint"] = {"exact": fingerprint}
|
427
496
|
if slug:
|
428
497
|
filters["slug"] = {"exact": slug}
|
498
|
+
if id:
|
499
|
+
filters["id"] = {"exact": id}
|
429
500
|
|
430
501
|
variables = {
|
431
502
|
"first": 1,
|
@@ -434,7 +505,10 @@ class Hardware(BaseAction):
|
|
434
505
|
result = self.primitive.session.execute(
|
435
506
|
query, variable_values=variables, get_execution_result=True
|
436
507
|
)
|
437
|
-
|
508
|
+
if edges := result.data["hardwareList"]["edges"]:
|
509
|
+
return edges[0]["node"]
|
510
|
+
else:
|
511
|
+
raise Exception(f"No hardware found with {filters}")
|
438
512
|
|
439
513
|
def get_own_hardware_details(self):
|
440
514
|
hardware_list_result = self.get_hardware_list(
|
@@ -448,6 +522,7 @@ class Hardware(BaseAction):
|
|
448
522
|
def get_hardware_from_slug_or_id(self, hardware_identifier: str):
|
449
523
|
is_id = False
|
450
524
|
is_slug = False
|
525
|
+
id = None
|
451
526
|
# first check if the hardware_identifier is a slug or ID
|
452
527
|
try:
|
453
528
|
type_name, id = from_base64(hardware_identifier)
|
@@ -466,16 +541,45 @@ class Hardware(BaseAction):
|
|
466
541
|
hardware = None
|
467
542
|
|
468
543
|
if is_id and is_hardware_type:
|
469
|
-
|
470
|
-
if edges := hardware_list_result.data["hardwareList"]["edges"]:
|
471
|
-
hardware = edges[0]["node"]
|
472
|
-
else:
|
473
|
-
raise Exception(f"No hardware found with slug {hardware_identifier}")
|
544
|
+
hardware = self.get_hardware_details(id=id)
|
474
545
|
elif is_slug:
|
475
|
-
|
476
|
-
if edges := hardware_list_result.data["hardwareList"]["edges"]:
|
477
|
-
hardware = edges[0]["node"]
|
478
|
-
else:
|
479
|
-
raise Exception(f"No hardware found with slug {hardware_identifier}")
|
546
|
+
hardware = self.get_hardware_details(slug=hardware_identifier)
|
480
547
|
|
481
548
|
return hardware
|
549
|
+
|
550
|
+
@guard
|
551
|
+
def register_child(self, child: AndroidDevice):
|
552
|
+
system_info = child.system_info
|
553
|
+
mutation = gql(register_child_hardware_mutation)
|
554
|
+
input = {"childSystemInfo": system_info}
|
555
|
+
variables = {"input": input}
|
556
|
+
result = self.primitive.session.execute(
|
557
|
+
mutation, variable_values=variables, get_execution_result=True
|
558
|
+
)
|
559
|
+
|
560
|
+
if messages := result.data.get("registerChildHardware").get("messages"):
|
561
|
+
for message in messages:
|
562
|
+
logger.enable("primitive")
|
563
|
+
if message.get("kind") == "ERROR":
|
564
|
+
logger.error(message.get("message"))
|
565
|
+
else:
|
566
|
+
logger.debug(message.get("message"))
|
567
|
+
return False
|
568
|
+
return result
|
569
|
+
|
570
|
+
def _list_local_children(self) -> List[AndroidDevice]:
|
571
|
+
if does_executable_exist("adb"):
|
572
|
+
self.children: List[AndroidDevice] = list_devices()
|
573
|
+
return self.children
|
574
|
+
|
575
|
+
@guard
|
576
|
+
def _remove_child(self):
|
577
|
+
pass
|
578
|
+
|
579
|
+
@guard
|
580
|
+
def _sync_children(self):
|
581
|
+
# get the existing children if any from the hardware details
|
582
|
+
# get the latest children from the node
|
583
|
+
# compare the two and update the node with the latest children
|
584
|
+
# remove any children from remote that are not in the latest children
|
585
|
+
pass
|
@@ -0,0 +1,75 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from subprocess import PIPE, Popen
|
3
|
+
from typing import Dict
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class AndroidDevice:
|
8
|
+
serial: str
|
9
|
+
usb: str
|
10
|
+
product: str
|
11
|
+
model: str
|
12
|
+
device: str
|
13
|
+
transport_id: str
|
14
|
+
system_info: Dict = field(default_factory=lambda: {})
|
15
|
+
|
16
|
+
def _get_android_values(self):
|
17
|
+
"""Get the values of the Android device."""
|
18
|
+
|
19
|
+
# example line:
|
20
|
+
# System name | Network (domain) name | Kernel Release number | Kernel Version | Machine (hardware) name
|
21
|
+
# Linux localhost 6.1.75-android14-11-g48b922851ac5-ab12039954 #1 SMP PREEMPT Tue Jul 2 09:33:34 UTC 2024 aarch64 Toybox
|
22
|
+
uname_output = execute_command(serial=self.serial, command="uname -a")
|
23
|
+
self.system_info["name"] = self.serial
|
24
|
+
self.system_info["os_family"] = "Android"
|
25
|
+
self.system_info["os_release"] = uname_output[2]
|
26
|
+
self.system_info["os_version"] = uname_output[3]
|
27
|
+
self.system_info["platform"] = ""
|
28
|
+
self.system_info["processor"] = ""
|
29
|
+
self.system_info["machine"] = uname_output[13]
|
30
|
+
self.system_info["architecture"] = "64bit"
|
31
|
+
return self.system_info
|
32
|
+
|
33
|
+
|
34
|
+
def list_devices():
|
35
|
+
"""List all connected Android devices."""
|
36
|
+
devices = []
|
37
|
+
|
38
|
+
with Popen(["adb", "devices", "-l"], stdout=PIPE) as process:
|
39
|
+
for line in process.stdout.read().decode("utf-8").split("\n"):
|
40
|
+
if line == "":
|
41
|
+
continue
|
42
|
+
|
43
|
+
if "List of devices attached" in line:
|
44
|
+
continue
|
45
|
+
|
46
|
+
device_details_array = [
|
47
|
+
detail for detail in line.split(" ") if detail != ""
|
48
|
+
]
|
49
|
+
|
50
|
+
android_device = AndroidDevice(
|
51
|
+
serial=device_details_array[0],
|
52
|
+
usb=device_details_array[1],
|
53
|
+
product=device_details_array[2],
|
54
|
+
model=device_details_array[3],
|
55
|
+
device=device_details_array[4],
|
56
|
+
transport_id=device_details_array[5],
|
57
|
+
)
|
58
|
+
android_device._get_android_values()
|
59
|
+
devices.append(android_device)
|
60
|
+
|
61
|
+
return devices
|
62
|
+
|
63
|
+
|
64
|
+
def execute_command(serial: str, command: str):
|
65
|
+
"""Execute a command on an Android device."""
|
66
|
+
with Popen(
|
67
|
+
["adb", "-s", serial, "shell", *command.split(" ")], stdin=PIPE, stdout=PIPE
|
68
|
+
) as process:
|
69
|
+
return process.stdout.read().decode("utf-8")
|
70
|
+
|
71
|
+
|
72
|
+
def create_interactive_shell(serial: str, command: str):
|
73
|
+
"""Create an interactive shell to an Android device."""
|
74
|
+
with Popen(["adb", "-s", serial, "shell"], stdin=PIPE, stdout=PIPE) as process:
|
75
|
+
return process.stdout.read().decode("utf-8")
|
@@ -0,0 +1,155 @@
|
|
1
|
+
import typing
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from ..utils.printer import print_result
|
6
|
+
|
7
|
+
if typing.TYPE_CHECKING:
|
8
|
+
from ..client import Primitive
|
9
|
+
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.table import Table
|
12
|
+
|
13
|
+
|
14
|
+
@click.group()
|
15
|
+
@click.pass_context
|
16
|
+
def cli(context):
|
17
|
+
"""Hardware"""
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
@cli.command("systeminfo")
|
22
|
+
@click.pass_context
|
23
|
+
def systeminfo_command(context):
|
24
|
+
"""Get System Info"""
|
25
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
26
|
+
message = primitive.hardware.get_system_info()
|
27
|
+
print_result(message=message, context=context)
|
28
|
+
|
29
|
+
|
30
|
+
@cli.command("register")
|
31
|
+
@click.pass_context
|
32
|
+
def register_command(context):
|
33
|
+
"""Register Hardware with Primitive"""
|
34
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
35
|
+
result = primitive.hardware.register()
|
36
|
+
color = "green" if result else "red"
|
37
|
+
if result.data.get("registerHardware"):
|
38
|
+
message = "Hardware registered successfully"
|
39
|
+
else:
|
40
|
+
message = (
|
41
|
+
"There was an error registering this device. Please review the above logs."
|
42
|
+
)
|
43
|
+
print_result(message=message, context=context, fg=color)
|
44
|
+
|
45
|
+
|
46
|
+
@cli.command("unregister")
|
47
|
+
@click.pass_context
|
48
|
+
def unregister_command(context):
|
49
|
+
"""Unregister Hardware with Primitive"""
|
50
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
51
|
+
result = primitive.hardware.unregister()
|
52
|
+
color = "green" if result else "red"
|
53
|
+
if not result:
|
54
|
+
message = "There was an error unregistering this device. Please review the above logs."
|
55
|
+
return
|
56
|
+
elif result.data.get("unregisterHardware"):
|
57
|
+
message = "Hardware unregistered successfully"
|
58
|
+
print_result(message=message, context=context, fg=color)
|
59
|
+
|
60
|
+
|
61
|
+
@cli.command("checkin")
|
62
|
+
@click.pass_context
|
63
|
+
def checkin_command(context):
|
64
|
+
"""Checkin Hardware with Primitive"""
|
65
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
66
|
+
check_in_http_result = primitive.hardware.check_in_http()
|
67
|
+
if messages := check_in_http_result.data.get("checkIn").get("messages"):
|
68
|
+
print_result(message=messages, context=context, fg="yellow")
|
69
|
+
else:
|
70
|
+
message = "Hardware checked in successfully"
|
71
|
+
print_result(message=message, context=context, fg="green")
|
72
|
+
|
73
|
+
|
74
|
+
def hardware_status_string(hardware):
|
75
|
+
if activeReservation := hardware.get("activeReservation"):
|
76
|
+
if activeReservation.get("status", None) == "in_progress":
|
77
|
+
return "Reserved"
|
78
|
+
if hardware.get("isQuarantined"):
|
79
|
+
return "Quarantined"
|
80
|
+
if not hardware.get("isOnline"):
|
81
|
+
return "Offline"
|
82
|
+
if not hardware.get("isHealthy"):
|
83
|
+
return "Not healthy"
|
84
|
+
if not hardware.get("isAvailable"):
|
85
|
+
return "Not available"
|
86
|
+
else:
|
87
|
+
return "Available"
|
88
|
+
|
89
|
+
|
90
|
+
@cli.command("list")
|
91
|
+
@click.pass_context
|
92
|
+
def list_command(context):
|
93
|
+
"""List Hardware"""
|
94
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
95
|
+
get_hardware_list_result = primitive.hardware.get_hardware_list(
|
96
|
+
nested_children=True
|
97
|
+
)
|
98
|
+
message = get_hardware_list_result.data
|
99
|
+
|
100
|
+
hardware_list = [
|
101
|
+
hardware.get("node")
|
102
|
+
for hardware in get_hardware_list_result.data.get("hardwareList").get("edges")
|
103
|
+
]
|
104
|
+
|
105
|
+
message = hardware_list
|
106
|
+
if context.obj["JSON"]:
|
107
|
+
print_result(message=message, context=context)
|
108
|
+
return
|
109
|
+
|
110
|
+
console = Console()
|
111
|
+
|
112
|
+
table = Table(show_header=True, header_style="bold magenta")
|
113
|
+
table.add_column("Organization")
|
114
|
+
table.add_column("Name | Slug")
|
115
|
+
table.add_column("Status")
|
116
|
+
table.add_column("Reservation")
|
117
|
+
|
118
|
+
for hardware in hardware_list:
|
119
|
+
name = hardware.get("name")
|
120
|
+
slug = hardware.get("slug")
|
121
|
+
print_name = name
|
122
|
+
if name != slug:
|
123
|
+
print_name = f"{name} | {slug}"
|
124
|
+
child_table = Table(show_header=False, header_style="bold magenta")
|
125
|
+
child_table.add_column("Organization")
|
126
|
+
child_table.add_column("Name | Slug")
|
127
|
+
child_table.add_column("Status")
|
128
|
+
child_table.add_column("Reservation", justify="right")
|
129
|
+
|
130
|
+
table.add_row(
|
131
|
+
hardware.get("organization").get("name"),
|
132
|
+
print_name,
|
133
|
+
hardware_status_string(hardware),
|
134
|
+
f"{hardware.get('activeReservation').get('createdBy').get('username')} | {hardware.get('activeReservation').get('status')}"
|
135
|
+
if hardware.get("activeReservation", None)
|
136
|
+
else "",
|
137
|
+
)
|
138
|
+
|
139
|
+
if len(hardware.get("children")) > 0:
|
140
|
+
for child in hardware.get("children"):
|
141
|
+
name = child.get("name")
|
142
|
+
slug = child.get("slug")
|
143
|
+
print_name = name
|
144
|
+
if name != slug:
|
145
|
+
print_name = f"└── {name} | {slug}"
|
146
|
+
table.add_row(
|
147
|
+
hardware.get("organization").get("name"),
|
148
|
+
print_name,
|
149
|
+
hardware_status_string(hardware),
|
150
|
+
f"{hardware.get('activeReservation').get('createdBy').get('username')} | {hardware.get('activeReservation').get('status')}"
|
151
|
+
if hardware.get("activeReservation", None)
|
152
|
+
else "",
|
153
|
+
)
|
154
|
+
|
155
|
+
console.print(table)
|
@@ -10,15 +10,26 @@ fragment HardwareFragment on Hardware {
|
|
10
10
|
isOnline
|
11
11
|
isQuarantined
|
12
12
|
isHealthy
|
13
|
+
systemInfo
|
13
14
|
hostname
|
14
15
|
sshUsername
|
15
16
|
capabilities {
|
16
17
|
id
|
17
18
|
pk
|
18
19
|
}
|
20
|
+
organization {
|
21
|
+
id
|
22
|
+
pk
|
23
|
+
name
|
24
|
+
slug
|
25
|
+
}
|
19
26
|
activeReservation {
|
20
27
|
id
|
21
28
|
pk
|
29
|
+
status
|
30
|
+
createdBy {
|
31
|
+
username
|
32
|
+
}
|
22
33
|
}
|
23
34
|
}
|
24
35
|
"""
|
@@ -14,6 +14,35 @@ mutation registerHardware($input: RegisterHardwareInput!) {
|
|
14
14
|
"""
|
15
15
|
)
|
16
16
|
|
17
|
+
register_child_hardware_mutation = (
|
18
|
+
operation_info_fragment
|
19
|
+
+ """
|
20
|
+
mutation registerChildHardware($input: RegisterChildHardwareInput!) {
|
21
|
+
registerChildHardware(input: $input) {
|
22
|
+
... on Hardware {
|
23
|
+
fingerprint
|
24
|
+
}
|
25
|
+
...OperationInfoFragment
|
26
|
+
}
|
27
|
+
}
|
28
|
+
"""
|
29
|
+
)
|
30
|
+
|
31
|
+
unregister_hardware_mutation = (
|
32
|
+
operation_info_fragment
|
33
|
+
+ """
|
34
|
+
mutation unregisterHardware($input: UnregisterHardwareInput!) {
|
35
|
+
unregisterHardware(input: $input) {
|
36
|
+
... on Hardware {
|
37
|
+
fingerprint
|
38
|
+
}
|
39
|
+
...OperationInfoFragment
|
40
|
+
}
|
41
|
+
}
|
42
|
+
"""
|
43
|
+
)
|
44
|
+
|
45
|
+
|
17
46
|
hardware_update_mutation = (
|
18
47
|
operation_info_fragment
|
19
48
|
+ """
|