primitive 0.2.63__py3-none-any.whl → 0.2.64__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.
primitive/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.63"
4
+ __version__ = "0.2.64"
@@ -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, sleeping...")
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(f"Job Name: {job_run_data.get('job').get('name')}")
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:
primitive/agent/runner.py CHANGED
@@ -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
- # Adding an additional echo and utilizing stdbuf to force line buffering
222
- # This ensures that the environment variables and starting delimiter are
223
- # always in a new chunk, vastly simplifying our parsing logic
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', '')}"
primitive/cli.py CHANGED
@@ -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={})
primitive/client.py CHANGED
@@ -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(hardware_update_mutation)
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
- message = {
440
- "is_healthy": is_healthy,
441
- "is_quarantined": is_quarantined,
442
- "is_available": is_available,
443
- "is_online": is_online,
444
- }
445
- self.primitive.messaging.send_message(
446
- message_type=MESSAGE_TYPES.CHECK_IN, message=message
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(nested_children_hardware_list if nested_children else hardware_list)
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
- filters["hasParent"] = {"exact": False}
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")
@@ -10,9 +10,16 @@ fragment HardwareFragment on Hardware {
10
10
  isOnline
11
11
  isQuarantined
12
12
  isHealthy
13
+ isController
13
14
  systemInfo
14
15
  hostname
15
16
  sshUsername
17
+ manufacturer {
18
+ id
19
+ pk
20
+ name
21
+ slug
22
+ }
16
23
  organization {
17
24
  id
18
25
  pk
@@ -25,6 +32,9 @@ fragment HardwareFragment on Hardware {
25
32
  status
26
33
  reservationNumber
27
34
  reason
35
+ hardware {
36
+ id
37
+ }
28
38
  createdBy {
29
39
  username
30
40
  }
@@ -1,4 +1,5 @@
1
1
  from primitive.graphql.utility_fragments import operation_info_fragment
2
+ from primitive.hardware.graphql.fragments import hardware_fragment
2
3
 
3
4
  hardware_certificate_create_mutation = (
4
5
  operation_info_fragment
@@ -60,14 +61,28 @@ mutation unregisterHardware($input: UnregisterHardwareInput!) {
60
61
  """
61
62
  )
62
63
 
64
+ hardware_update_system_info_mutation = (
65
+ operation_info_fragment
66
+ + """
67
+ mutation hardwareUpdate($input: HardwareUpdateInput!) {
68
+ hardwareUpdate(input: $input) {
69
+ ... on Hardware {
70
+ systemInfo
71
+ }
72
+ ...OperationInfoFragment
73
+ }
74
+ }
75
+ """
76
+ )
63
77
 
64
78
  hardware_update_mutation = (
65
79
  operation_info_fragment
80
+ + hardware_fragment
66
81
  + """
67
82
  mutation hardwareUpdate($input: HardwareUpdateInput!) {
68
83
  hardwareUpdate(input: $input) {
69
84
  ... on Hardware {
70
- systemInfo
85
+ ...HardwareFragment
71
86
  }
72
87
  ...OperationInfoFragment
73
88
  }