Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit ca07e4e

Browse files
authored
Merge branch 'main' into no-ti-j784s4-preflash
2 parents 527d896 + 74d3e2a commit ca07e4e

5 files changed

Lines changed: 193 additions & 82 deletions

File tree

packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ async def test_get_client(_load_kube_config_mock, get_client_mock: AsyncMock):
219219
220220
"""
221221

222+
CLIENTS_LIST_NAME = """client.jumpstarter.dev/test
223+
client.jumpstarter.dev/another
224+
"""
225+
222226
CLIENTS_LIST_EMPTY_YAML = """apiVersion: jumpstarter.dev/v1alpha1
223227
items: []
224228
kind: ClientList
@@ -259,7 +263,7 @@ async def test_get_clients(_load_kube_config_mock, list_clients_mock: AsyncMock)
259263
list_clients_mock.return_value = CLIENTS_LIST
260264
result = await runner.invoke(get, ["clients", "--output", "name"])
261265
assert result.exit_code == 0
262-
assert result.output == "client.jumpstarter.dev/test\n"
266+
assert result.output == CLIENTS_LIST_NAME
263267
list_clients_mock.reset_mock()
264268

265269
# No clients found
@@ -590,6 +594,10 @@ async def test_get_exporter_devices(_load_kube_config_mock, get_exporter_mock: A
590594
591595
"""
592596

597+
EXPORTERS_LIST_NAME = """exporter.jumpstarter.dev/test
598+
exporter.jumpstarter.dev/another
599+
"""
600+
593601

594602
@pytest.mark.anyio
595603
@patch.object(ExportersV1Alpha1Api, "list_exporters")
@@ -623,7 +631,7 @@ async def test_get_exporters(_load_kube_config_mock, list_exporters_mock: AsyncM
623631
list_exporters_mock.return_value = EXPORTERS_LIST
624632
result = await runner.invoke(get, ["exporters", "--output", "name"])
625633
assert result.exit_code == 0
626-
assert result.output == "exporter.jumpstarter.dev/test\n"
634+
assert result.output == EXPORTERS_LIST_NAME
627635
list_exporters_mock.reset_mock()
628636

629637
# No exporters found
@@ -754,6 +762,7 @@ async def test_get_exporters(_load_kube_config_mock, list_exporters_mock: AsyncM
754762
755763
"""
756764

765+
EXPORTERS_DEVICES_LIST_NAME = EXPORTERS_LIST_NAME
757766

758767
@pytest.mark.anyio
759768
@patch.object(ExportersV1Alpha1Api, "list_exporters")
@@ -790,7 +799,7 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock
790799
list_exporters_mock.return_value = EXPORTER_DEVICES_LIST
791800
result = await runner.invoke(get, ["exporters", "--devices", "--output", "name"])
792801
assert result.exit_code == 0
793-
assert result.output == "exporter.jumpstarter.dev/test\n"
802+
assert result.output == EXPORTERS_DEVICES_LIST_NAME
794803
list_exporters_mock.reset_mock()
795804

796805
# No exporters found
@@ -1128,6 +1137,9 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock):
11281137
11291138
"""
11301139

1140+
LEASES_LIST_NAME = """lease.jumpstarter.dev/82a8ac0d-d7ff-4009-8948-18a3c5c607b1
1141+
lease.jumpstarter.dev/82a8ac0d-d7ff-4009-8948-18a3c5c607b2
1142+
"""
11311143

11321144
@pytest.mark.anyio
11331145
@patch.object(LeasesV1Alpha1Api, "list_leases")
@@ -1172,7 +1184,7 @@ async def test_get_leases(_load_kube_config_mock, list_leases_mock: AsyncMock):
11721184
list_leases_mock.return_value = V1Alpha1LeaseList(items=[IN_PROGRESS_LEASE, FINISHED_LEASE])
11731185
result = await runner.invoke(get, ["leases", "--output", "name"])
11741186
assert result.exit_code == 0
1175-
assert result.output == "lease.jumpstarter.dev/82a8ac0d-d7ff-4009-8948-18a3c5c607b1\n"
1187+
assert result.output == LEASES_LIST_NAME
11761188
list_leases_mock.reset_mock()
11771189

11781190
# No leases found

packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output:
4040
elif output == OutputMode.YAML:
4141
click.echo(clients.dump_yaml())
4242
elif output == OutputMode.NAME:
43-
if len(clients.items) > 0:
44-
click.echo(f"client.jumpstarter.dev/{clients.items[0].metadata.name}")
43+
for item in clients.items:
44+
click.echo(f"client.jumpstarter.dev/{item.metadata.name}")
4545
elif len(clients.items) == 0:
4646
raise click.ClickException(f'No resources found in "{namespace}" namespace')
4747
else:
@@ -104,8 +104,8 @@ def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, d
104104
elif output == OutputMode.YAML:
105105
click.echo(exporters.dump_yaml())
106106
elif output == OutputMode.NAME:
107-
if len(exporters.items) > 0:
108-
click.echo(f"exporter.jumpstarter.dev/{exporters.items[0].metadata.name}")
107+
for item in exporters.items:
108+
click.echo(f"exporter.jumpstarter.dev/{item.metadata.name}")
109109
elif len(exporters.items) == 0:
110110
raise click.ClickException(f'No resources found in "{namespace}" namespace')
111111
elif devices:
@@ -168,8 +168,8 @@ def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: Ou
168168
elif output == OutputMode.YAML:
169169
click.echo(leases.dump_yaml())
170170
elif output == OutputMode.NAME:
171-
if len(leases.items) > 0:
172-
click.echo(f"lease.jumpstarter.dev/{leases.items[0].metadata.name}")
171+
for item in leases.items:
172+
click.echo(f"lease.jumpstarter.dev/{item.metadata.name}")
173173
elif len(leases.items) == 0:
174174
raise click.ClickException(f'No resources found in "{namespace}" namespace')
175175
else:

packages/jumpstarter-driver-dutlink/jumpstarter_driver_dutlink/driver.py

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
from __future__ import annotations
22

3-
import os
43
import time
54
from collections.abc import AsyncGenerator
65
from dataclasses import dataclass, field
76

87
import pyudev
98
import usb.core
109
import usb.util
11-
from anyio import fail_after, sleep
12-
from anyio.streams.file import FileReadStream, FileWriteStream
10+
from anyio import sleep
1311
from jumpstarter_driver_composite.driver import CompositeInterface
1412
from jumpstarter_driver_opendal.driver import StorageMuxFlasherInterface
1513
from jumpstarter_driver_power.driver import PowerInterface, PowerReading
1614
from jumpstarter_driver_pyserial.driver import PySerial
1715
from serial.serialutil import SerialException
1816

17+
from jumpstarter.common.storage import read_from_storage_device, write_to_storage_device
1918
from jumpstarter.driver import Driver, export
2019

2120

2221
@dataclass(kw_only=True)
2322
class DutlinkConfig:
2423
serial: str | None = field(default=None)
2524
timeout_s: int = field(default=20) # 20 seconds, power control sequences can block USB for a long time
25+
storage_timeout: int = field(default=10)
26+
storage_leeway: int = field(default=6)
27+
storage_fsync_timeout: int = field(default=900)
2628

2729
dev: usb.core.Device = field(init=False)
2830
itf: usb.core.Interface = field(init=False)
@@ -187,44 +189,29 @@ def dut(self):
187189
def off(self):
188190
return self.control("off")
189191

190-
async def wait_for_storage_device(self):
191-
with fail_after(20):
192-
while True:
193-
self.logger.debug(f"waiting for storage device {self.storage_device}")
194-
if os.path.exists(self.storage_device):
195-
self.logger.debug(f"storage device {self.storage_device} is ready")
196-
# https://stackoverflow.com/a/2774125
197-
fd = os.open(self.storage_device, os.O_WRONLY)
198-
try:
199-
if os.lseek(fd, 0, os.SEEK_END) > 0:
200-
break
201-
finally:
202-
os.close(fd)
203-
await sleep(1)
204-
205192
@export
206193
async def write(self, src: str):
207194
self.host()
208-
await self.wait_for_storage_device()
209-
async with await FileWriteStream.from_path(self.storage_device) as stream:
210-
async with self.resource(src) as res:
211-
total_bytes = 0
212-
next_print = 0
213-
async for chunk in res:
214-
await stream.send(chunk)
215-
if total_bytes > next_print:
216-
self.logger.debug(f"{self.storage_device} written {total_bytes / (1024 * 1024)} MB")
217-
next_print += 50 * 1024 * 1024
218-
total_bytes += len(chunk)
195+
async with self.resource(src) as res:
196+
await write_to_storage_device(
197+
self.storage_device,
198+
res,
199+
timeout=self.storage_timeout,
200+
leeway=self.storage_leeway,
201+
fsync_timeout=self.storage_fsync_timeout,
202+
logger=self.logger,
203+
)
219204

220205
@export
221206
async def read(self, dst: str):
222207
self.host()
223-
await self.wait_for_storage_device()
224-
async with await FileReadStream.from_path(self.storage_device) as stream:
225-
async with self.resource(dst) as res:
226-
async for chunk in stream:
227-
await res.send(chunk)
208+
async with self.resource(dst) as res:
209+
await read_from_storage_device(
210+
self.storage_device,
211+
res,
212+
timeout=self.storage_timeout,
213+
logger=self.logger,
214+
)
228215

229216

230217
@dataclass(kw_only=True)

packages/jumpstarter-driver-sdwire/jumpstarter_driver_sdwire/driver.py

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
from __future__ import annotations
22

3-
import os
43
from dataclasses import dataclass, field
54

65
import pyudev
76
import usb.core
87
import usb.util
9-
from anyio import fail_after, sleep
10-
from anyio.streams.file import FileReadStream, FileWriteStream
118
from jumpstarter_driver_opendal.driver import StorageMuxFlasherInterface
129

10+
from jumpstarter.common.storage import read_from_storage_device, write_to_storage_device
1311
from jumpstarter.driver import Driver, export
1412

1513

@@ -20,6 +18,9 @@ class SDWire(StorageMuxFlasherInterface, Driver):
2018
itf: usb.core.Interface = field(init=False)
2119

2220
storage_device: str | None = field(default=None)
21+
storage_timeout: int = field(default=10)
22+
storage_leeway: int = field(default=6)
23+
storage_fsync_timeout: int = field(default=900)
2324

2425
def effective_storage_device(self):
2526
if self.storage_device is None:
@@ -101,45 +102,26 @@ def dut(self):
101102
def off(self):
102103
self.host()
103104

104-
async def wait_for_storage_device(self):
105-
with fail_after(10):
106-
storage_device = self.effective_storage_device()
107-
108-
while True:
109-
# https://stackoverflow.com/a/2774125
110-
try:
111-
fd = os.open(storage_device, os.O_WRONLY)
112-
if os.lseek(fd, 0, os.SEEK_END) > 0:
113-
break
114-
except OSError as e:
115-
match e.errno:
116-
case 123: # No medium found
117-
pass
118-
case 5: # Input/output error
119-
pass
120-
case _:
121-
raise
122-
finally:
123-
if "fd" in locals():
124-
os.close(fd)
125-
await sleep(1)
126-
127-
return storage_device
128-
129105
@export
130106
async def write(self, src: str):
131107
self.host()
132-
storage_device = await self.wait_for_storage_device()
133-
async with await FileWriteStream.from_path(storage_device) as stream:
134-
async with self.resource(src) as res:
135-
async for chunk in res:
136-
await stream.send(chunk)
108+
async with self.resource(src) as res:
109+
await write_to_storage_device(
110+
self.effective_storage_device(),
111+
res,
112+
timeout=self.storage_timeout,
113+
leeway=self.storage_leeway,
114+
fsync_timeout=self.storage_fsync_timeout,
115+
logger=self.logger,
116+
)
137117

138118
@export
139119
async def read(self, dst: str):
140120
self.host()
141-
storage_device = await self.wait_for_storage_device()
142-
async with await FileReadStream.from_path(storage_device) as stream:
143-
async with self.resource(dst) as res:
144-
async for chunk in stream:
145-
await res.send(chunk)
121+
async with self.resource(dst) as res:
122+
await read_from_storage_device(
123+
self.effective_storage_device(),
124+
res,
125+
timeout=self.storage_timeout,
126+
logger=self.logger,
127+
)

0 commit comments

Comments
 (0)