11import enum
22import os
3+ import pathlib
34import time
45import subprocess
56
67import attr
78
89from ..factory import target_factory
10+ from ..resource .remote import RemoteUSBResource
911from ..step import step
1012from ..util .managedfile import ManagedFile
1113from .common import Driver
1214from ..driver .exception import ExecutionError
1315
1416from ..util .helper import processwrapper
17+ from ..util .agentwrapper import AgentWrapper
1518from ..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
0 commit comments