primitive 0.2.66__py3-none-any.whl → 0.2.68__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 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.66"
4
+ __version__ = "0.2.68"
primitive/agent/runner.py CHANGED
@@ -9,6 +9,7 @@ from datetime import datetime, timezone
9
9
 
10
10
  from loguru import logger
11
11
 
12
+ from primitive.network.ssh import wait_for_ssh
12
13
  from primitive.utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
13
14
  from primitive.utils.logging import fmt, log_context
14
15
  from primitive.utils.psutil import kill_process_and_children
@@ -231,6 +232,7 @@ class Runner:
231
232
 
232
233
  for i, cmd in enumerate(commands):
233
234
  if cmd == "oobpowercycle":
235
+ logger.info("Performing out-of-band power cycle")
234
236
  from primitive.network.redfish import RedfishClient
235
237
 
236
238
  bmc_host = self.target_hardware_secret.get("bmcHostname", None)
@@ -253,15 +255,16 @@ class Runner:
253
255
  )
254
256
  redfish.compute_system_reset(system_id="1", reset_type="ForceRestart")
255
257
  if self.target_hardware_id:
256
- self.primitive.hardware.update_hardware(
258
+ await self.primitive.hardware.aupdate_hardware(
257
259
  hardware_id=self.target_hardware_id,
258
260
  is_online=False,
259
261
  is_rebooting=True,
260
- start_rebooting_at=datetime.now(timezone.utc),
262
+ start_rebooting_at=str(datetime.now(timezone.utc)),
261
263
  )
262
264
  continue
263
265
 
264
266
  if cmd == "pxeboot":
267
+ logger.info("Setting next boot to PXE and rebooting")
265
268
  from primitive.network.redfish import RedfishClient
266
269
 
267
270
  bmc_host = self.target_hardware_secret.get("bmcHostname", None)
@@ -290,12 +293,19 @@ class Runner:
290
293
  )
291
294
  redfish.compute_system_reset(system_id="1", reset_type="ForceRestart")
292
295
  if self.target_hardware_id:
293
- self.primitive.hardware.update_hardware(
296
+ await self.primitive.hardware.aupdate_hardware(
294
297
  hardware_id=self.target_hardware_id,
295
298
  is_online=False,
296
299
  is_rebooting=True,
297
- start_rebooting_at=datetime.now(timezone.utc),
300
+ start_rebooting_at=str(datetime.now(timezone.utc)),
298
301
  )
302
+ wait_for_ssh(
303
+ hostname=self.target_hardware_secret.get("hostname"),
304
+ username=self.target_hardware_secret.get("username"),
305
+ password=self.target_hardware_secret.get("password"),
306
+ port=22,
307
+ )
308
+ logger.info("PXE boot successful, SSH is now available")
299
309
  continue
300
310
 
301
311
  args = ["/bin/bash", "-c", cmd]
@@ -981,3 +981,39 @@ class Hardware(BaseAction):
981
981
  logger.info(message)
982
982
 
983
983
  return result
984
+
985
+ @guard
986
+ async def aupdate_hardware(
987
+ self,
988
+ hardware_id: str,
989
+ is_online: Optional[bool] = None,
990
+ is_rebooting: Optional[bool] = None,
991
+ start_rebooting_at: Optional[str] = None,
992
+ ):
993
+ new_state: dict = {
994
+ "id": hardware_id,
995
+ }
996
+ if is_online is not None:
997
+ new_state["isOnline"] = is_online
998
+ if is_rebooting is not None:
999
+ new_state["isRebooting"] = is_rebooting
1000
+ if start_rebooting_at is not None:
1001
+ new_state["startRebootingAt"] = start_rebooting_at
1002
+
1003
+ mutation = gql(hardware_update_mutation)
1004
+
1005
+ input = new_state
1006
+ variables = {"input": input}
1007
+ try:
1008
+ result = await self.primitive.session.execute_async(
1009
+ mutation, variable_values=variables, get_execution_result=True
1010
+ )
1011
+ except client_exceptions.ClientConnectorError as exception:
1012
+ message = "Failed to update hardware! "
1013
+ logger.exception(message)
1014
+ raise exception
1015
+
1016
+ message = "Updated hardware successfully! "
1017
+ logger.info(message)
1018
+
1019
+ return result
@@ -0,0 +1,94 @@
1
+ import paramiko
2
+ import socket
3
+ import time
4
+
5
+
6
+ def test_ssh_connection(hostname, username, password=None, key_filename=None, port=22):
7
+ """
8
+ Tests an SSH connection to a remote host.
9
+
10
+ Args:
11
+ hostname (str): The hostname or IP address of the remote SSH server.
12
+ username (str): The username for authentication.
13
+ password (str, optional): The password for authentication. Defaults to None.
14
+ key_filename (str, optional): Path to the private key file for authentication. Defaults to None.
15
+ port (int, optional): The SSH port. Defaults to 22.
16
+
17
+ Returns:
18
+ bool: True if the connection is successful, False otherwise.
19
+ """
20
+ ssh_client = paramiko.SSHClient()
21
+ ssh_client.set_missing_host_key_policy(
22
+ paramiko.AutoAddPolicy()
23
+ ) # Auto-add new host keys
24
+
25
+ try:
26
+ if password:
27
+ ssh_client.connect(
28
+ hostname=hostname, port=port, username=username, password=password
29
+ )
30
+ elif key_filename:
31
+ ssh_client.connect(
32
+ hostname=hostname,
33
+ port=port,
34
+ username=username,
35
+ key_filename=key_filename,
36
+ )
37
+ else:
38
+ print(
39
+ "Error: Either password or key_filename must be provided for authentication."
40
+ )
41
+ return False
42
+
43
+ print(f"Successfully connected to {hostname} as {username}")
44
+ return True
45
+ except paramiko.AuthenticationException:
46
+ print(f"Authentication failed for {username} on {hostname}")
47
+ return False
48
+ except paramiko.SSHException as e:
49
+ print(f"SSH error connecting to {hostname}: {e}")
50
+ return False
51
+ except socket.error as e:
52
+ print(f"Socket error connecting to {hostname}: {e}")
53
+ return False
54
+ except Exception as e:
55
+ print(f"An unexpected error occurred: {e}")
56
+ return False
57
+ finally:
58
+ ssh_client.close()
59
+
60
+
61
+ TEN_MINUTES = 60 * 10
62
+
63
+
64
+ def wait_for_ssh(
65
+ hostname, username, password=None, key_filename=None, port=22, timeout=TEN_MINUTES
66
+ ):
67
+ """
68
+ Waits until an SSH connection to a remote host can be established.
69
+
70
+ Args:
71
+ hostname (str): The hostname or IP address of the remote SSH server.
72
+ username (str): The username for authentication.
73
+ password (str, optional): The password for authentication. Defaults to None.
74
+ key_filename (str, optional): Path to the private key file for authentication. Defaults to None.
75
+ port (int, optional): The SSH port. Defaults to 22.
76
+ timeout (int, optional): Maximum time to wait in seconds. Defaults to 300.
77
+
78
+ Returns:
79
+ bool: True if the connection is successful within the timeout, False otherwise.
80
+ """
81
+
82
+ start_time = time.time()
83
+ while time.time() - start_time < timeout:
84
+ if test_ssh_connection(
85
+ hostname, username, password=password, key_filename=key_filename, port=port
86
+ ):
87
+ return True
88
+ print(f"Waiting for SSH to become available on {hostname}...")
89
+ time.sleep(10)
90
+
91
+ print(
92
+ f"Timeout reached: Unable to connect to {hostname} via SSH within {timeout} seconds."
93
+ )
94
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.66
3
+ Version: 0.2.68
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
@@ -1,11 +1,11 @@
1
- primitive/__about__.py,sha256=ZHOZYKO41emH20Bvp7WJ22vSZsHO5IRCfpWpII9LKbg,130
1
+ primitive/__about__.py,sha256=C8M6D49G3Ebr3FPG2sD1HtX3IjFMyfqC0icwwuBfmp8,130
2
2
  primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
3
3
  primitive/cli.py,sha256=kLjqyNijkbElbH8XhEtR6vjPc5uwOoh9mKEf2RkcfKk,2608
4
4
  primitive/client.py,sha256=n0eQAft3lQyTT_tVE1_1BWSsh8G7gJI2kHBuXLvZVEY,3836
5
5
  primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  primitive/agent/actions.py,sha256=ohY81bflsYSTsNgo2o2WP_tZr66haX4z319zf5oib-4,9362
7
7
  primitive/agent/commands.py,sha256=o847pK7v7EWQGG67tky6a33qtwoutX6LZrP2FIS_NOk,388
8
- primitive/agent/runner.py,sha256=oKMCkZpeHGENLXgLIFMgOitrjIpUGYUQmcfTWwSsqSM,15598
8
+ primitive/agent/runner.py,sha256=QoRsehNhpcdCXeidi_YO1W4REjAJ2UR8ZecyePTmc-g,16177
9
9
  primitive/agent/uploader.py,sha256=DT_Nzt5eOTm_uRcYKm1sjBBaQZzp5iNZ_uN5XktfQ30,3382
10
10
  primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  primitive/auth/actions.py,sha256=9NIEXJ1BNJutJs6AMMSjMN_ziONUAUhY_xHwojYJCLA,942
@@ -40,7 +40,7 @@ primitive/graphql/relay.py,sha256=bmij2AjdpURQ6GGVCxwWhauF-r_SxuAU2oJ4sDbLxpI,72
40
40
  primitive/graphql/sdk.py,sha256=dE4TD8KiTKw3Y0uiw5XrIcuZGqexE47eSlPaPD6jDGo,1545
41
41
  primitive/graphql/utility_fragments.py,sha256=uIjwILC4QtWNyO5vu77VjQf_p0jvP3A9q_6zRq91zqs,303
42
42
  primitive/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- primitive/hardware/actions.py,sha256=kSQ1DufDCwxY5fs3zNMj7evAunEF8vt36a7rgtjI7Uw,37323
43
+ primitive/hardware/actions.py,sha256=yCBIGV9zwkeeW3NykdoOLCXefEC2C-z4LlfQImZY_Mc,38471
44
44
  primitive/hardware/android.py,sha256=tu7pBPxWFrIwb_mm5CEdFFf1_veNDOKjOCQg13i_Lh4,2758
45
45
  primitive/hardware/commands.py,sha256=XU365RByl75ECAV_GdA4xxLrmZkwo1axlw9ZH0k3yRk,4751
46
46
  primitive/hardware/ui.py,sha256=12rucuZ2s-w5R4bKyxON5dEbrdDnVf5sbj3K_nbdo44,2473
@@ -63,6 +63,7 @@ primitive/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
63
63
  primitive/network/actions.py,sha256=qpkn9U7LTDzdoOntiFP2sBPCQYEraRjAYrkcPzhmw78,17021
64
64
  primitive/network/commands.py,sha256=F84KmwkG0DHNLIa7Em8LMm2XDOaKMCWRpcFej8olxM0,1071
65
65
  primitive/network/redfish.py,sha256=uOtAS_Dwc4I4bnWKNSTCQ_xsj5LTtRzW5v2P_fWaSJM,4248
66
+ primitive/network/ssh.py,sha256=a4LNMCQEEbYxTczkfRBiFBahGtZ_h6KKOx6rGk2PFfM,3208
66
67
  primitive/network/ui.py,sha256=_i4lJ3hhTrPc_KS5EjXPkqqKkzdLCOdxxtk5MiDWsfU,504
67
68
  primitive/organizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
69
  primitive/organizations/actions.py,sha256=kVHOhG1oS2sI5p8uldSo5L-RUZsnG36eaulVuKLyZ-M,1863
@@ -104,8 +105,8 @@ primitive/utils/psutil.py,sha256=xa7ef435UL37jyjmUPbEqCO2ayQMpCs0HCrxVEvLcuM,763
104
105
  primitive/utils/shell.py,sha256=Z4zxmOaSyGCrS0D6I436iQci-ewHLt4UxVg1CD9Serc,2171
105
106
  primitive/utils/text.py,sha256=XiESMnlhjQ534xE2hMNf08WehE1SKaYFRNih0MmnK0k,829
106
107
  primitive/utils/x509.py,sha256=HwHRPqakTHWd40ny-9O_yNknSL1Cxo50O0UCjXHFq04,3796
107
- primitive-0.2.66.dist-info/METADATA,sha256=sjynK6BDitz1RZTZyJnxC9T7Pms8jgLwHZmmdUq9Uvk,3569
108
- primitive-0.2.66.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
109
- primitive-0.2.66.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
110
- primitive-0.2.66.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
111
- primitive-0.2.66.dist-info/RECORD,,
108
+ primitive-0.2.68.dist-info/METADATA,sha256=Z16gZVIM8zx0JvTsSjpQrrbzl69hskD2JDRUtGoA6CQ,3569
109
+ primitive-0.2.68.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
110
+ primitive-0.2.68.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
111
+ primitive-0.2.68.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
112
+ primitive-0.2.68.dist-info/RECORD,,