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.
Files changed (109) hide show
  1. {primitive-0.1.88 → primitive-0.1.90}/PKG-INFO +2 -1
  2. {primitive-0.1.88 → primitive-0.1.90}/pyproject.toml +1 -0
  3. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/__about__.py +1 -1
  4. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/actions.py +6 -1
  5. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/runner.py +6 -2
  6. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/actions.py +40 -3
  7. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/actions.py +1 -3
  8. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/actions.py +119 -15
  9. primitive-0.1.90/src/primitive/hardware/android.py +75 -0
  10. primitive-0.1.90/src/primitive/hardware/commands.py +155 -0
  11. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/graphql/fragments.py +11 -0
  12. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/graphql/mutations.py +29 -0
  13. primitive-0.1.90/src/primitive/hardware/graphql/queries.py +97 -0
  14. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/actions.py +52 -6
  15. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/commands.py +5 -2
  16. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/shell.py +7 -1
  17. {primitive-0.1.88 → primitive-0.1.90}/uv.lock +305 -279
  18. primitive-0.1.88/src/primitive/hardware/commands.py +0 -63
  19. primitive-0.1.88/src/primitive/hardware/graphql/queries.py +0 -31
  20. {primitive-0.1.88 → primitive-0.1.90}/.git-hooks/pre-commit +0 -0
  21. {primitive-0.1.88 → primitive-0.1.90}/.gitattributes +0 -0
  22. {primitive-0.1.88 → primitive-0.1.90}/.github/workflows/lint.yml +0 -0
  23. {primitive-0.1.88 → primitive-0.1.90}/.github/workflows/publish.yml +0 -0
  24. {primitive-0.1.88 → primitive-0.1.90}/.gitignore +0 -0
  25. {primitive-0.1.88 → primitive-0.1.90}/.vscode/settings.json +0 -0
  26. {primitive-0.1.88 → primitive-0.1.90}/LICENSE.txt +0 -0
  27. {primitive-0.1.88 → primitive-0.1.90}/Makefile +0 -0
  28. {primitive-0.1.88 → primitive-0.1.90}/README.md +0 -0
  29. {primitive-0.1.88 → primitive-0.1.90}/linux setup.md +0 -0
  30. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/__init__.py +0 -0
  31. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/__init__.py +0 -0
  32. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/commands.py +0 -0
  33. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/process.py +0 -0
  34. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/provision.py +0 -0
  35. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/agent/uploader.py +0 -0
  36. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/__init__.py +0 -0
  37. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/actions.py +0 -0
  38. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/commands.py +0 -0
  39. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/graphql/__init__.py +0 -0
  40. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/auth/graphql/queries.py +0 -0
  41. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/cli.py +0 -0
  42. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/client.py +0 -0
  43. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/__init__.py +0 -0
  44. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/actions.py +0 -0
  45. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/commands.py +0 -0
  46. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/launch_agents.py +0 -0
  47. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/daemons/launch_service.py +0 -0
  48. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/__init__.py +0 -0
  49. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/commands.py +0 -0
  50. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/exec/interactive.py +0 -0
  51. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/__init__.py +0 -0
  52. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/actions.py +0 -0
  53. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/commands.py +0 -0
  54. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/__init__.py +0 -0
  55. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/fragments.py +0 -0
  56. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/mutations.py +0 -0
  57. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/files/graphql/queries.py +0 -0
  58. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/__init__.py +0 -0
  59. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/commands.py +0 -0
  60. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/graphql/__init__.py +0 -0
  61. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/git/graphql/queries.py +0 -0
  62. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/__init__.py +0 -0
  63. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/relay.py +0 -0
  64. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/sdk.py +0 -0
  65. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/graphql/utility_fragments.py +0 -0
  66. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/__init__.py +0 -0
  67. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/hardware/graphql/__init__.py +0 -0
  68. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/__init__.py +0 -0
  69. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/actions.py +0 -0
  70. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/commands.py +0 -0
  71. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/__init__.py +0 -0
  72. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/fragments.py +0 -0
  73. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/mutations.py +0 -0
  74. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/jobs/graphql/queries.py +0 -0
  75. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/__init__.py +0 -0
  76. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/actions.py +0 -0
  77. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/commands.py +0 -0
  78. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/__init__.py +0 -0
  79. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/fragments.py +0 -0
  80. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/mutations.py +0 -0
  81. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/organizations/graphql/queries.py +0 -0
  82. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/__init__.py +0 -0
  83. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/actions.py +0 -0
  84. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/commands.py +0 -0
  85. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/__init__.py +0 -0
  86. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/fragments.py +0 -0
  87. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/mutations.py +0 -0
  88. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/projects/graphql/queries.py +0 -0
  89. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/__init__.py +0 -0
  90. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/actions.py +0 -0
  91. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/graphql/__init__.py +0 -0
  92. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/provisioning/graphql/queries.py +0 -0
  93. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/__init__.py +0 -0
  94. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/__init__.py +0 -0
  95. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/fragments.py +0 -0
  96. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/mutations.py +0 -0
  97. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/reservations/graphql/queries.py +0 -0
  98. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/__init__.py +0 -0
  99. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/actions.py +0 -0
  100. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/auth.py +0 -0
  101. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/cache.py +0 -0
  102. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/chunk_size.py +0 -0
  103. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/config.py +0 -0
  104. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/files.py +0 -0
  105. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/git.py +0 -0
  106. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/memory_size.py +0 -0
  107. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/printer.py +0 -0
  108. {primitive-0.1.88 → primitive-0.1.90}/src/primitive/utils/verible.py +0 -0
  109. {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.88
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
 
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "primitive-pal == 0.1.4",
35
35
  "paramiko[invoke]",
36
36
  "speedtest-cli",
37
+ "rich>=13.9.4",
37
38
  ]
38
39
 
39
40
  [tool.uv]
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.1.88"
4
+ __version__ = "0.1.90"
@@ -28,7 +28,12 @@ class Agent(BaseAction):
28
28
 
29
29
  # self.primitive.hardware.update_hardware_system_info()
30
30
  try:
31
- self.primitive.hardware.check_in_http(is_available=True, is_online=True)
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(f"Process {step['name']} finished with return code {returncode}")
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(f"Step {step['name']} failed with return code {returncode}")
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
- reservation = self.primitive.reservations.wait_for_reservation_status(
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
- formatted_command = " ".join(command)
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
- print(stdout.read())
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, fingerprint: Optional[str] = None, slug: Optional[str] = None
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
- return result
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
- hardware_list_result = self.get_hardware_list(slug=hardware_identifier)
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
- hardware_list_result = self.get_hardware_list(slug=hardware_identifier)
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
  + """