Skip to content

Commit 1de150d

Browse files
committed
inherit power client and standartize method names
Signed-off-by: Benny Zlotnik <[email protected]>
1 parent 3527396 commit 1de150d

File tree

3 files changed

+71
-55
lines changed

3 files changed

+71
-55
lines changed

packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections.abc import Generator
2+
import time
23

34
import asyncclick as click
45

@@ -13,6 +14,16 @@ def on(self) -> str:
1314
def off(self) -> str:
1415
return self.call("off")
1516

17+
def cycle(self, quiescent_period: int = 2) -> str:
18+
"""Power cycle the device"""
19+
self.logger.info("Starting power cycle sequence")
20+
self.off()
21+
self.logger.info(f"Waiting {quiescent_period} seconds...")
22+
time.sleep(quiescent_period)
23+
self.on()
24+
self.logger.info("Power cycle sequence complete")
25+
return "Power cycled"
26+
1627
def read(self) -> Generator[PowerReading, None, None]:
1728
for v in self.streamingcall("read"):
1829
yield PowerReading.model_validate(v, strict=True)
@@ -33,4 +44,10 @@ def off():
3344
"""Power off"""
3445
click.echo(self.off())
3546

47+
@base.command()
48+
@click.option('--wait', '-w', default=2, help='Wait time in seconds between off and on')
49+
def cycle(wait):
50+
"""Power cycle"""
51+
click.echo(self.cycle(wait))
52+
3653
return base
Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,34 @@
11
from dataclasses import dataclass
22

33
import asyncclick as click
4-
5-
from jumpstarter.client import DriverClient
4+
from jumpstarter_driver_power.client import PowerClient
65

76

87
@dataclass(kw_only=True)
9-
class SNMPServerClient(DriverClient):
8+
class SNMPServerClient(PowerClient):
109
"""Client interface for SNMP Power Control"""
1110

12-
def power_on(self):
11+
def on(self) -> str:
1312
"""Turn power on"""
14-
return self.call("power_on")
13+
return self.call("on")
1514

16-
def power_off(self):
15+
def off(self) -> str:
1716
"""Turn power off"""
18-
return self.call("power_off")
19-
20-
def power_cycle(self):
21-
"""Power cycle the device"""
22-
return self.call("power_cycle")
17+
return self.call("off")
2318

2419
def cli(self):
2520
@click.group()
2621
def snmp():
2722
"""SNMP power control commands"""
2823
pass
2924

30-
@snmp.command()
31-
def on():
32-
"""Turn power on"""
33-
result = self.power_on()
34-
click.echo(result)
35-
36-
@snmp.command()
37-
def off():
38-
"""Turn power off"""
39-
result = self.power_off()
40-
click.echo(result)
25+
for cmd in super().cli().commands.values():
26+
snmp.add_command(cmd)
4127

4228
@snmp.command()
4329
def cycle():
4430
"""Power cycle the device"""
45-
result = self.power_cycle()
31+
result = self.cycle()
4632
click.echo(result)
4733

4834
return snmp

packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/driver.py

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import socket
3-
import time
43
from dataclasses import dataclass, field
54
from enum import IntEnum
65

@@ -30,41 +29,70 @@ class SNMPServer(Driver):
3029
timeout: int = 3
3130
plug: int = field()
3231
oid: str = field(default="1.3.6.1.4.1.13742.6.4.1.2.1.2.1")
32+
auth_protocol: str = field(default=None) # 'MD5' or 'SHA'
33+
auth_key: str = field(default=None)
34+
priv_protocol: str = field(default=None) # 'DES' or 'AES'
35+
priv_key: str = field(default=None)
3336

3437
def __post_init__(self):
3538
if hasattr(super(), "__post_init__"):
3639
super().__post_init__()
3740

3841
try:
3942
self.ip_address = socket.gethostbyname(self.host)
40-
self.logger.info(f"Resolved {self.host} to {self.ip_address}")
43+
self.logger.debug(f"Resolved {self.host} to {self.ip_address}")
4144
except socket.gaierror as e:
4245
raise SNMPError(f"Failed to resolve hostname {self.host}: {e}") from e
4346

4447
self.full_oid = tuple(int(x) for x in self.oid.split('.')) + (self.plug,)
4548

4649
def _setup_snmp(self):
4750
try:
48-
# TODO: switch to anyio?
4951
asyncio.get_running_loop()
5052
except RuntimeError:
5153
loop = asyncio.new_event_loop()
5254
asyncio.set_event_loop(loop)
5355

54-
5556
snmp_engine = engine.SnmpEngine()
5657

57-
config.add_v3_user(
58-
snmp_engine,
59-
self.user,
60-
config.USM_AUTH_NONE,
61-
None
62-
)
58+
if self.auth_protocol and self.auth_key:
59+
if self.priv_protocol and self.priv_key:
60+
security_level = 'authPriv'
61+
auth_protocol = getattr(config, f'usmHMAC{self.auth_protocol}AuthProtocol')
62+
priv_protocol = getattr(config, f'usmPriv{self.priv_protocol}Protocol')
63+
64+
config.add_v3_user(
65+
snmp_engine,
66+
self.user,
67+
auth_protocol,
68+
self.auth_key,
69+
priv_protocol,
70+
self.priv_key
71+
)
72+
else:
73+
security_level = 'authNoPriv'
74+
auth_protocol = getattr(config, f'usmHMAC{self.auth_protocol}AuthProtocol')
75+
76+
config.add_v3_user(
77+
snmp_engine,
78+
self.user,
79+
auth_protocol,
80+
self.auth_key
81+
)
82+
else:
83+
security_level = 'noAuthNoPriv'
84+
config.add_v3_user(
85+
snmp_engine,
86+
self.user,
87+
config.USM_AUTH_NONE,
88+
None
89+
)
90+
6391
config.add_target_parameters(
6492
snmp_engine,
6593
"my-creds",
6694
self.user,
67-
"noAuthNoPriv"
95+
security_level
6896
)
6997

7098
config.add_transport(
@@ -92,12 +120,12 @@ def _snmp_set(self, state: PowerState):
92120

93121
def callback(snmpEngine, sendRequestHandle, errorIndication,
94122
errorStatus, errorIndex, varBinds, cbCtx):
95-
self.logger.info(f"Callback {errorIndication} {errorStatus} {errorIndex} {varBinds}")
123+
self.logger.debug(f"Callback {errorIndication} {errorStatus} {errorIndex} {varBinds}")
96124
if errorIndication:
97-
self.logger.info(f"SNMP error: {errorIndication}")
125+
self.logger.error(f"SNMP error: {errorIndication}")
98126
result["error"] = f"SNMP error: {errorIndication}"
99127
elif errorStatus:
100-
self.logger.info(f"SNMP status: {errorStatus}")
128+
self.logger.error(f"SNMP status: {errorStatus}")
101129
result["error"] = (
102130
f"SNMP error: {errorStatus.prettyPrint()} at "
103131
f"{varBinds[int(errorIndex) - 1][0] if errorIndex else '?'}"
@@ -106,7 +134,7 @@ def callback(snmpEngine, sendRequestHandle, errorIndication,
106134
result["success"] = True
107135
for oid, val in varBinds:
108136
self.logger.debug(f"{oid.prettyPrint()} = {val.prettyPrint()}")
109-
self.logger.info(f"SNMP set result: {result}")
137+
self.logger.debug(f"SNMP set result: {result}")
110138

111139
try:
112140
self.logger.info(f"Sending power {state.name} command to {self.host}")
@@ -136,30 +164,15 @@ def callback(snmpEngine, sendRequestHandle, errorIndication,
136164
raise SNMPError(error_msg) from e
137165

138166
@export
139-
def power_on(self):
167+
def on(self):
140168
"""Turn power on"""
141169
return self._snmp_set(PowerState.ON)
142170

143171
@export
144-
def power_off(self):
172+
def off(self):
145173
"""Turn power off"""
146174
return self._snmp_set(PowerState.OFF)
147175

148-
@export
149-
def power_cycle(self):
150-
"""Power cycle the device"""
151-
try:
152-
self.logger.info("Starting power cycle sequence")
153-
self.power_off()
154-
self.logger.info(f"Waiting {self.quiescent_period} seconds...")
155-
time.sleep(self.quiescent_period)
156-
self.power_on()
157-
return "Power cycle completed successfully"
158-
except Exception as e:
159-
error_msg = f"Power cycle failed: {str(e)}"
160-
self.logger.error(error_msg)
161-
raise SNMPError(error_msg) from e
162-
163176
def close(self):
164177
"""No cleanup needed since engines are created per operation"""
165178
if hasattr(super(), "close"):

0 commit comments

Comments
 (0)