Skip to content

Commit aa08ff2

Browse files
authored
Merge pull request #1219 from a3f/write-files
remote/client: implement udisks2-using write-files command
2 parents 6c8c6e5 + a712ee2 commit aa08ff2

File tree

8 files changed

+297
-21
lines changed

8 files changed

+297
-21
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ New Features in 24.0
1212
to the serial console during testing.
1313
- The `QEMUDriver` now has an additional ``disk_opts`` property which can be
1414
used to pass additional options for the disk directly to QEMU
15+
- labgrid-client now has a ``write-files`` subcommand to copy files onto mass
16+
storage devices.
1517

1618
Bug fixes in 24.0
1719
~~~~~~~~~~~~~~~~~

contrib/completion/labgrid-client.bash

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,40 @@ _labgrid_client_write_image()
769769
esac
770770
}
771771
772+
_labgrid_client_write_files()
773+
{
774+
local cur prev words cword
775+
_init_completion || return
776+
777+
case "$prev" in
778+
-w|--wait)
779+
;&
780+
-p|--partition)
781+
;&
782+
-t|--target-directory)
783+
;&
784+
-T)
785+
;&
786+
-n|--name)
787+
_labgrid_complete match-names "$cur"
788+
return
789+
;;
790+
esac
791+
792+
case "$cur" in
793+
-*)
794+
local options="--wait --partition --target-directory --name $_labgrid_shared_options"
795+
COMPREPLY=( $(compgen -W "$options" -- "$cur") )
796+
;;
797+
*)
798+
local args
799+
_labgrid_count_args "@(-w|--wait|-p|--partition|-t|--target-directory|-T|-n|--name)" || return
800+
801+
_filedir
802+
;;
803+
esac
804+
}
805+
772806
_labgrid_client_reserve()
773807
{
774808
_labgrid_client_generic_subcommand "--wait --shell --prio"
@@ -888,6 +922,7 @@ _labgrid_client()
888922
audio \
889923
tmc \
890924
write-image \
925+
write-files \
891926
reserve \
892927
cancel-reservation \
893928
wait \

doc/configuration.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,14 @@ device.
610610
match:
611611
ID_PATH: 'pci-0000:06:00.0-usb-0:1.3.2:1.0-scsi-0:0:0:3'
612612
613+
Writing images to disk requires installation of ``dd`` or optionally
614+
``bmaptool`` on the same system as the block device.
615+
616+
For mounting the file system and writing into it,
617+
`PyGObject <https://pygobject.readthedocs.io/>`_ must be installed.
618+
For Debian, the necessary packages are `python3-gi` and `gir1.2-udisks-2.0`.
619+
This is not required for writing images to disks.
620+
613621
Arguments:
614622
- match (dict): key and value pairs for a udev match, see `udev Matching`_
615623

@@ -622,7 +630,7 @@ A :any:`NetworkUSBMassStorage` resource describes a USB memory stick or similar
622630
device available on a remote computer.
623631

624632
The NetworkUSBMassStorage can be used in test cases by calling the
625-
``write_image()``, and ``get_size()`` functions.
633+
``write_files()``, ``write_image()``, and ``get_size()`` functions.
626634

627635
SigrokDevice
628636
~~~~~~~~~~~~

labgrid/driver/usbstoragedriver.py

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import enum
22
import os
3+
import pathlib
34
import time
45
import subprocess
56

67
import attr
78

89
from ..factory import target_factory
10+
from ..resource.remote import RemoteUSBResource
911
from ..step import step
1012
from ..util.managedfile import ManagedFile
1113
from .common import Driver
1214
from ..driver.exception import ExecutionError
1315

1416
from ..util.helper import processwrapper
17+
from ..util.agentwrapper import AgentWrapper
1518
from ..util import Timeout
1619

1720

@@ -40,12 +43,69 @@ class USBStorageDriver(Driver):
4043
default=None,
4144
validator=attr.validators.optional(attr.validators.instance_of(str))
4245
)
46+
WAIT_FOR_MEDIUM_TIMEOUT = 10.0 # s
47+
WAIT_FOR_MEDIUM_SLEEP = 0.5 # s
48+
49+
def __attrs_post_init__(self):
50+
super().__attrs_post_init__()
51+
self.wrapper = None
4352

4453
def on_activate(self):
45-
pass
54+
host = self.storage.host if isinstance(self.storage, RemoteUSBResource) else None
55+
self.wrapper = AgentWrapper(host)
56+
self.proxy = self.wrapper.load('udisks2')
4657

4758
def on_deactivate(self):
48-
pass
59+
self.wrapper.close()
60+
self.wrapper = None
61+
self.proxy = None
62+
63+
@Driver.check_active
64+
@step(args=['sources', 'target', 'partition', 'target_is_directory'])
65+
def write_files(self, sources, target, partition, target_is_directory=True):
66+
"""
67+
Write the file(s) specified by filename(s) to the
68+
bound USB storage partition.
69+
70+
Args:
71+
sources (List[str]): path(s) to the file(s) to be copied to the bound USB storage
72+
partition.
73+
target (str): target directory or file to copy to
74+
partition (int): mount the specified partition or None to mount the whole disk
75+
target_is_directory (bool): Whether target is a directory
76+
"""
77+
78+
self.devpath = self._get_devpath(partition)
79+
mount_path = self.proxy.mount(self.devpath)
80+
81+
try:
82+
# (pathlib.PurePath(...) / "/") == "/", so we turn absolute paths into relative
83+
# paths with respect to the mount point here
84+
target_rel = target.relative_to(target.root) if target.root is not None else target
85+
target_path = str(pathlib.PurePath(mount_path) / target_rel)
86+
87+
copied_sources = []
88+
89+
for f in sources:
90+
mf = ManagedFile(f, self.storage)
91+
mf.sync_to_resource()
92+
copied_sources.append(mf.get_remote_path())
93+
94+
if target_is_directory:
95+
args = ["cp", "-t", target_path] + copied_sources
96+
else:
97+
if len(sources) != 1:
98+
raise ValueError("single source argument required when target_is_directory=False")
99+
100+
args = ["cp", "-T", copied_sources[0], target_path]
101+
102+
processwrapper.check_output(self.storage.command_prefix + args)
103+
self.proxy.unmount(self.devpath)
104+
except:
105+
# We are going to die with an exception anyway, so no point in waiting
106+
# to make sure everything has been written before continuing
107+
self.proxy.unmount(self.devpath, lazy=True)
108+
raise
49109

50110
@Driver.check_active
51111
@step(args=['filename'])
@@ -68,22 +128,10 @@ def write_image(self, filename=None, mode=Mode.DD, partition=None, skip=0, seek=
68128
mf = ManagedFile(filename, self.storage)
69129
mf.sync_to_resource()
70130

71-
# wait for medium
72-
timeout = Timeout(10.0)
73-
while not timeout.expired:
74-
try:
75-
if self.get_size() > 0:
76-
break
77-
time.sleep(0.5)
78-
except ValueError:
79-
# when the medium gets ready the sysfs attribute is empty for a short time span
80-
continue
81-
else:
82-
raise ExecutionError("Timeout while waiting for medium")
131+
self._wait_for_medium(partition)
83132

84-
partition = "" if partition is None else partition
133+
target = self._get_devpath(partition)
85134
remote_path = mf.get_remote_path()
86-
target = f"{self.storage.path}{partition}"
87135

88136
if mode == Mode.DD:
89137
self.logger.info('Writing %s to %s using dd.', remote_path, target)
@@ -139,12 +187,41 @@ def write_image(self, filename=None, mode=Mode.DD, partition=None, skip=0, seek=
139187
print_on_silent_log=True
140188
)
141189

190+
def _get_devpath(self, partition):
191+
partition = "" if partition is None else partition
192+
# simple concatenation is sufficient for USB mass storage
193+
return f"{self.storage.path}{partition}"
194+
142195
@Driver.check_active
143-
@step(result=True)
144-
def get_size(self):
145-
args = ["cat", f"/sys/class/block/{self.storage.path[5:]}/size"]
196+
def _wait_for_medium(self, partition):
197+
timeout = Timeout(self.WAIT_FOR_MEDIUM_TIMEOUT)
198+
while not timeout.expired:
199+
if self.get_size(partition) > 0:
200+
break
201+
time.sleep(self.WAIT_FOR_MEDIUM_SLEEP)
202+
else:
203+
raise ExecutionError("Timeout while waiting for medium")
204+
205+
@Driver.check_active
206+
@step(args=['partition'], result=True)
207+
def get_size(self, partition=None):
208+
"""
209+
Get the size of the bound USB storage root device or partition.
210+
211+
Args:
212+
partition (int or None): optional, get size of the specified partition or None for
213+
getting the size of the root device (defaults to None)
214+
215+
Returns:
216+
int: size in bytes
217+
"""
218+
args = ["cat", f"/sys/class/block/{self._get_devpath(partition)[5:]}/size"]
146219
size = subprocess.check_output(self.storage.command_prefix + args)
147-
return int(size)*512
220+
try:
221+
return int(size) * 512
222+
except ValueError:
223+
# when the medium gets ready the sysfs attribute is empty for a short time span
224+
return 0
148225

149226

150227
@target_factory.reg_driver

labgrid/remote/client.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import contextlib
66
import enum
77
import os
8+
import pathlib
89
import subprocess
910
import traceback
1011
import logging
@@ -1214,6 +1215,32 @@ def tmc_channel(self):
12141215
for k, v in sorted(data.items()):
12151216
print(f"{k:<16s} {str(v):<10s}")
12161217

1218+
def write_files(self):
1219+
place = self.get_acquired_place()
1220+
target = self._get_target(place)
1221+
name = self.args.name
1222+
drv = self._get_driver_or_new(target, "USBStorageDriver", activate=False, name=name)
1223+
drv.storage.timeout = self.args.wait
1224+
target.activate(drv)
1225+
1226+
try:
1227+
if self.args.partition == 0:
1228+
self.args.partition = None
1229+
1230+
if self.args.rename:
1231+
if len(self.args.SOURCE) != 2:
1232+
self.args.parser.error("the following arguments are required: SOURCE DEST")
1233+
1234+
drv.write_files([self.args.SOURCE[0]], self.args.SOURCE[1],
1235+
self.args.partition, target_is_directory=False)
1236+
else:
1237+
drv.write_files(self.args.SOURCE, self.args.target_directory,
1238+
self.args.partition, target_is_directory=True)
1239+
except subprocess.CalledProcessError as e:
1240+
raise UserError(f"could not copy files to network usb storage: {e}")
1241+
except FileNotFoundError as e:
1242+
raise UserError(e)
1243+
12171244
def write_image(self):
12181245
place = self.get_acquired_place()
12191246
target = self._get_target(place)
@@ -1761,6 +1788,27 @@ def main():
17611788
tmc_subparser.add_argument('action', choices=['info', 'values'])
17621789
tmc_subparser.set_defaults(func=ClientSession.tmc_channel)
17631790

1791+
subparser = subparsers.add_parser('write-files', help="copy files onto mass storage device",
1792+
usage="%(prog)s [OPTION]... -T SOURCE DEST\n" +
1793+
" %(prog)s [OPTION]... [-t DIRECTORY] SOURCE...")
1794+
subparser.add_argument('-w', '--wait', type=float, default=10.0,
1795+
help='storage poll timeout in seconds')
1796+
subparser.add_argument('-p', '--partition', type=int, choices=range(0, 256),
1797+
metavar='0-255', default=1,
1798+
help='partition number to mount or 0 to mount whole disk (default: %(default)s)')
1799+
group = subparser.add_mutually_exclusive_group()
1800+
group.add_argument('-t', '--target-directory', type=pathlib.PurePath, metavar='DIRECTORY',
1801+
default=pathlib.PurePath("/"),
1802+
help='copy all SOURCE files into DIRECTORY (default: partition root)')
1803+
group.add_argument('-T', action='store_true', dest='rename',
1804+
help='copy SOURCE file and rename to DEST')
1805+
subparser.add_argument('--name', '-n', help="optional resource name")
1806+
subparser.add_argument('SOURCE', type=pathlib.PurePath, nargs='+',
1807+
help='source file(s) to copy')
1808+
subparser.add_argument('DEST', type=pathlib.PurePath, nargs='?',
1809+
help='destination file name for SOURCE')
1810+
subparser.set_defaults(func=ClientSession.write_files, parser=subparser)
1811+
17641812
subparser = subparsers.add_parser('write-image', help="write an image onto mass storage")
17651813
subparser.add_argument('-w', '--wait', type=float, default=10.0)
17661814
subparser.add_argument('-p', '--partition', type=int, help="partition number to write to")

0 commit comments

Comments
 (0)