Package CedarBackup2 :: Package writers :: Module cdwriter
[hide private]
[frames] | no frames]

Source Code for Module CedarBackup2.writers.cdwriter

   1  # -*- coding: iso-8859-1 -*- 
   2  # vim: set ft=python ts=3 sw=3 expandtab: 
   3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
   4  # 
   5  #              C E D A R 
   6  #          S O L U T I O N S       "Software done right." 
   7  #           S O F T W A R E 
   8  # 
   9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  10  # 
  11  # Copyright (c) 2004-2008 Kenneth J. Pronovici. 
  12  # All rights reserved. 
  13  # 
  14  # This program is free software; you can redistribute it and/or 
  15  # modify it under the terms of the GNU General Public License, 
  16  # Version 2, as published by the Free Software Foundation. 
  17  # 
  18  # This program is distributed in the hope that it will be useful, 
  19  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
  21  # 
  22  # Copies of the GNU General Public License are available from 
  23  # the Free Software Foundation website, http://www.gnu.org/. 
  24  # 
  25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  26  # 
  27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org> 
  28  # Language : Python (>= 2.3) 
  29  # Project  : Cedar Backup, release 2 
  30  # Revision : $Id: cdwriter.py 972 2010-05-22 16:51:20Z pronovic $ 
  31  # Purpose  : Provides functionality related to CD writer devices. 
  32  # 
  33  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
  34   
  35  ######################################################################## 
  36  # Module documentation 
  37  ######################################################################## 
  38   
  39  """ 
  40  Provides functionality related to CD writer devices. 
  41   
  42  @sort: MediaDefinition, MediaCapacity, CdWriter, 
  43         MEDIA_CDRW_74, MEDIA_CDR_74, MEDIA_CDRW_80, MEDIA_CDR_80 
  44   
  45  @var MEDIA_CDRW_74: Constant representing 74-minute CD-RW media. 
  46  @var MEDIA_CDR_74: Constant representing 74-minute CD-R media. 
  47  @var MEDIA_CDRW_80: Constant representing 80-minute CD-RW media. 
  48  @var MEDIA_CDR_80: Constant representing 80-minute CD-R media. 
  49   
  50  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
  51  """ 
  52   
  53  ######################################################################## 
  54  # Imported modules 
  55  ######################################################################## 
  56   
  57  # System modules 
  58  import os 
  59  import re 
  60  import logging 
  61  import tempfile 
  62   
  63  # Cedar Backup modules 
  64  from CedarBackup2.filesystem import FilesystemList 
  65  from CedarBackup2.util import resolveCommand, executeCommand 
  66  from CedarBackup2.util import convertSize, displayBytes, encodePath 
  67  from CedarBackup2.util import UNIT_SECTORS, UNIT_BYTES, UNIT_KBYTES, UNIT_MBYTES 
  68  from CedarBackup2.writers.util import validateDevice, validateScsiId, validateDriveSpeed 
  69  from CedarBackup2.writers.util import IsoImage 
  70   
  71   
  72  ######################################################################## 
  73  # Module-wide constants and variables 
  74  ######################################################################## 
  75   
  76  logger = logging.getLogger("CedarBackup2.log.writers.cdwriter") 
  77   
  78  MEDIA_CDRW_74  = 1 
  79  MEDIA_CDR_74   = 2 
  80  MEDIA_CDRW_80  = 3 
  81  MEDIA_CDR_80   = 4 
  82   
  83  CDRECORD_COMMAND = [ "cdrecord", ] 
  84  EJECT_COMMAND    = [ "eject", ] 
  85  MKISOFS_COMMAND  = [ "mkisofs", ] 
  86   
  87   
  88  ######################################################################## 
  89  # MediaDefinition class definition 
  90  ######################################################################## 
  91   
92 -class MediaDefinition(object):
93 94 """ 95 Class encapsulating information about CD media definitions. 96 97 The following media types are accepted: 98 99 - C{MEDIA_CDR_74}: 74-minute CD-R media (650 MB capacity) 100 - C{MEDIA_CDRW_74}: 74-minute CD-RW media (650 MB capacity) 101 - C{MEDIA_CDR_80}: 80-minute CD-R media (700 MB capacity) 102 - C{MEDIA_CDRW_80}: 80-minute CD-RW media (700 MB capacity) 103 104 Note that all of the capacities associated with a media definition are in 105 terms of ISO sectors (C{util.ISO_SECTOR_SIZE)}. 106 107 @sort: __init__, mediaType, rewritable, initialLeadIn, leadIn, capacity 108 """ 109
110 - def __init__(self, mediaType):
111 """ 112 Creates a media definition for the indicated media type. 113 @param mediaType: Type of the media, as discussed above. 114 @raise ValueError: If the media type is unknown or unsupported. 115 """ 116 self._mediaType = None 117 self._rewritable = False 118 self._initialLeadIn = 0. 119 self._leadIn = 0.0 120 self._capacity = 0.0 121 self._setValues(mediaType)
122
123 - def _setValues(self, mediaType):
124 """ 125 Sets values based on media type. 126 @param mediaType: Type of the media, as discussed above. 127 @raise ValueError: If the media type is unknown or unsupported. 128 """ 129 if mediaType not in [MEDIA_CDR_74, MEDIA_CDRW_74, MEDIA_CDR_80, MEDIA_CDRW_80]: 130 raise ValueError("Invalid media type %d." % mediaType) 131 self._mediaType = mediaType 132 self._initialLeadIn = 11400.0 # per cdrecord's documentation 133 self._leadIn = 6900.0 # per cdrecord's documentation 134 if self._mediaType == MEDIA_CDR_74: 135 self._rewritable = False 136 self._capacity = convertSize(650.0, UNIT_MBYTES, UNIT_SECTORS) 137 elif self._mediaType == MEDIA_CDRW_74: 138 self._rewritable = True 139 self._capacity = convertSize(650.0, UNIT_MBYTES, UNIT_SECTORS) 140 elif self._mediaType == MEDIA_CDR_80: 141 self._rewritable = False 142 self._capacity = convertSize(700.0, UNIT_MBYTES, UNIT_SECTORS) 143 elif self._mediaType == MEDIA_CDRW_80: 144 self._rewritable = True 145 self._capacity = convertSize(700.0, UNIT_MBYTES, UNIT_SECTORS)
146
147 - def _getMediaType(self):
148 """ 149 Property target used to get the media type value. 150 """ 151 return self._mediaType
152
153 - def _getRewritable(self):
154 """ 155 Property target used to get the rewritable flag value. 156 """ 157 return self._rewritable
158
159 - def _getInitialLeadIn(self):
160 """ 161 Property target used to get the initial lead-in value. 162 """ 163 return self._initialLeadIn
164
165 - def _getLeadIn(self):
166 """ 167 Property target used to get the lead-in value. 168 """ 169 return self._leadIn
170
171 - def _getCapacity(self):
172 """ 173 Property target used to get the capacity value. 174 """ 175 return self._capacity
176 177 mediaType = property(_getMediaType, None, None, doc="Configured media type.") 178 rewritable = property(_getRewritable, None, None, doc="Boolean indicating whether the media is rewritable.") 179 initialLeadIn = property(_getInitialLeadIn, None, None, doc="Initial lead-in required for first image written to media.") 180 leadIn = property(_getLeadIn, None, None, doc="Lead-in required on successive images written to media.") 181 capacity = property(_getCapacity, None, None, doc="Total capacity of the media before any required lead-in.")
182 183 184 ######################################################################## 185 # MediaCapacity class definition 186 ######################################################################## 187
188 -class MediaCapacity(object):
189 190 """ 191 Class encapsulating information about CD media capacity. 192 193 Space used includes the required media lead-in (unless the disk is unused). 194 Space available attempts to provide a picture of how many bytes are 195 available for data storage, including any required lead-in. 196 197 The boundaries value is either C{None} (if multisession discs are not 198 supported or if the disc has no boundaries) or in exactly the form provided 199 by C{cdrecord -msinfo}. It can be passed as-is to the C{IsoImage} class. 200 201 @sort: __init__, bytesUsed, bytesAvailable, boundaries, totalCapacity, utilized 202 """ 203
204 - def __init__(self, bytesUsed, bytesAvailable, boundaries):
205 """ 206 Initializes a capacity object. 207 @raise IndexError: If the boundaries tuple does not have enough elements. 208 @raise ValueError: If the boundaries values are not integers. 209 @raise ValueError: If the bytes used and available values are not floats. 210 """ 211 self._bytesUsed = float(bytesUsed) 212 self._bytesAvailable = float(bytesAvailable) 213 if boundaries is None: 214 self._boundaries = None 215 else: 216 self._boundaries = (int(boundaries[0]), int(boundaries[1]))
217
218 - def __str__(self):
219 """ 220 Informal string representation for class instance. 221 """ 222 return "utilized %s of %s (%.2f%%)" % (displayBytes(self.bytesUsed), displayBytes(self.totalCapacity), self.utilized)
223
224 - def _getBytesUsed(self):
225 """ 226 Property target to get the bytes-used value. 227 """ 228 return self._bytesUsed
229
230 - def _getBytesAvailable(self):
231 """ 232 Property target to get the bytes-available value. 233 """ 234 return self._bytesAvailable
235
236 - def _getBoundaries(self):
237 """ 238 Property target to get the boundaries tuple. 239 """ 240 return self._boundaries
241
242 - def _getTotalCapacity(self):
243 """ 244 Property target to get the total capacity (used + available). 245 """ 246 return self.bytesUsed + self.bytesAvailable
247
248 - def _getUtilized(self):
249 """ 250 Property target to get the percent of capacity which is utilized. 251 """ 252 if self.bytesAvailable <= 0.0: 253 return 100.0 254 elif self.bytesUsed <= 0.0: 255 return 0.0 256 return (self.bytesUsed / self.totalCapacity) * 100.0
257 258 bytesUsed = property(_getBytesUsed, None, None, doc="Space used on disc, in bytes.") 259 bytesAvailable = property(_getBytesAvailable, None, None, doc="Space available on disc, in bytes.") 260 boundaries = property(_getBoundaries, None, None, doc="Session disc boundaries, in terms of ISO sectors.") 261 totalCapacity = property(_getTotalCapacity, None, None, doc="Total capacity of the disc, in bytes.") 262 utilized = property(_getUtilized, None, None, "Percentage of the total capacity which is utilized.")
263 264 265 ######################################################################## 266 # _ImageProperties class definition 267 ######################################################################## 268
269 -class _ImageProperties(object):
270 """ 271 Simple value object to hold image properties for C{DvdWriter}. 272 """
273 - def __init__(self):
274 self.newDisc = False 275 self.tmpdir = None 276 self.mediaLabel = None 277 self.entries = None # dict mapping path to graft point
278 279 280 ######################################################################## 281 # CdWriter class definition 282 ######################################################################## 283
284 -class CdWriter(object):
285 286 ###################### 287 # Class documentation 288 ###################### 289 290 """ 291 Class representing a device that knows how to write CD media. 292 293 Summary 294 ======= 295 296 This is a class representing a device that knows how to write CD media. It 297 provides common operations for the device, such as ejecting the media, 298 writing an ISO image to the media, or checking for the current media 299 capacity. It also provides a place to store device attributes, such as 300 whether the device supports writing multisession discs, etc. 301 302 This class is implemented in terms of the C{eject} and C{cdrecord} 303 programs, both of which should be available on most UN*X platforms. 304 305 Image Writer Interface 306 ====================== 307 308 The following methods make up the "image writer" interface shared 309 with other kinds of writers (such as DVD writers):: 310 311 __init__ 312 initializeImage() 313 addImageEntry() 314 writeImage() 315 setImageNewDisc() 316 retrieveCapacity() 317 getEstimatedImageSize() 318 319 Only these methods will be used by other Cedar Backup functionality 320 that expects a compatible image writer. 321 322 The media attribute is also assumed to be available. 323 324 Media Types 325 =========== 326 327 This class knows how to write to two different kinds of media, represented 328 by the following constants: 329 330 - C{MEDIA_CDR_74}: 74-minute CD-R media (650 MB capacity) 331 - C{MEDIA_CDRW_74}: 74-minute CD-RW media (650 MB capacity) 332 - C{MEDIA_CDR_80}: 80-minute CD-R media (700 MB capacity) 333 - C{MEDIA_CDRW_80}: 80-minute CD-RW media (700 MB capacity) 334 335 Most hardware can read and write both 74-minute and 80-minute CD-R and 336 CD-RW media. Some older drives may only be able to write CD-R media. 337 The difference between the two is that CD-RW media can be rewritten 338 (erased), while CD-R media cannot be. 339 340 I do not support any other configurations for a couple of reasons. The 341 first is that I've never tested any other kind of media. The second is 342 that anything other than 74 or 80 minute is apparently non-standard. 343 344 Device Attributes vs. Media Attributes 345 ====================================== 346 347 A given writer instance has two different kinds of attributes associated 348 with it, which I call device attributes and media attributes. Device 349 attributes are things which can be determined without looking at the 350 media, such as whether the drive supports writing multisession disks or 351 has a tray. Media attributes are attributes which vary depending on the 352 state of the media, such as the remaining capacity on a disc. In 353 general, device attributes are available via instance variables and are 354 constant over the life of an object, while media attributes can be 355 retrieved through method calls. 356 357 Talking to Hardware 358 =================== 359 360 This class needs to talk to CD writer hardware in two different ways: 361 through cdrecord to actually write to the media, and through the 362 filesystem to do things like open and close the tray. 363 364 Historically, CdWriter has interacted with cdrecord using the scsiId 365 attribute, and with most other utilities using the device attribute. 366 This changed somewhat in Cedar Backup 2.9.0. 367 368 When Cedar Backup was first written, the only way to interact with 369 cdrecord was by using a SCSI device id. IDE devices were mapped to 370 pseudo-SCSI devices through the kernel. Later, extended SCSI "methods" 371 arrived, and it became common to see C{ATA:1,0,0} or C{ATAPI:0,0,0} as a 372 way to address IDE hardware. By late 2006, C{ATA} and C{ATAPI} had 373 apparently been deprecated in favor of just addressing the IDE device 374 directly by name, i.e. C{/dev/cdrw}. 375 376 Because of this latest development, it no longer makes sense to require a 377 CdWriter to be created with a SCSI id -- there might not be one. So, the 378 passed-in SCSI id is now optional. Also, there is now a hardwareId 379 attribute. This attribute is filled in with either the SCSI id (if 380 provided) or the device (otherwise). The hardware id is the value that 381 will be passed to cdrecord in the C{dev=} argument. 382 383 Testing 384 ======= 385 386 It's rather difficult to test this code in an automated fashion, even if 387 you have access to a physical CD writer drive. It's even more difficult 388 to test it if you are running on some build daemon (think of a Debian 389 autobuilder) which can't be expected to have any hardware or any media 390 that you could write to. 391 392 Because of this, much of the implementation below is in terms of static 393 methods that are supposed to take defined actions based on their 394 arguments. Public methods are then implemented in terms of a series of 395 calls to simplistic static methods. This way, we can test as much as 396 possible of the functionality via testing the static methods, while 397 hoping that if the static methods are called appropriately, things will 398 work properly. It's not perfect, but it's much better than no testing at 399 all. 400 401 @sort: __init__, isRewritable, _retrieveProperties, retrieveCapacity, _getBoundaries, 402 _calculateCapacity, openTray, closeTray, refreshMedia, writeImage, 403 _blankMedia, _parsePropertiesOutput, _parseBoundariesOutput, 404 _buildOpenTrayArgs, _buildCloseTrayArgs, _buildPropertiesArgs, 405 _buildBoundariesArgs, _buildBlankArgs, _buildWriteArgs, 406 device, scsiId, hardwareId, driveSpeed, media, deviceType, deviceVendor, 407 deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, deviceCanEject, 408 initializeImage, addImageEntry, writeImage, setImageNewDisc, getEstimatedImageSize 409 """ 410 411 ############## 412 # Constructor 413 ############## 414
415 - def __init__(self, device, scsiId=None, driveSpeed=None, 416 mediaType=MEDIA_CDRW_74, noEject=False, 417 refreshMediaDelay=0, unittest=False):
418 """ 419 Initializes a CD writer object. 420 421 The current user must have write access to the device at the time the 422 object is instantiated, or an exception will be thrown. However, no 423 media-related validation is done, and in fact there is no need for any 424 media to be in the drive until one of the other media attribute-related 425 methods is called. 426 427 The various instance variables such as C{deviceType}, C{deviceVendor}, 428 etc. might be C{None}, if we're unable to parse this specific information 429 from the C{cdrecord} output. This information is just for reference. 430 431 The SCSI id is optional, but the device path is required. If the SCSI id 432 is passed in, then the hardware id attribute will be taken from the SCSI 433 id. Otherwise, the hardware id will be taken from the device. 434 435 If cdrecord improperly detects whether your writer device has a tray and 436 can be safely opened and closed, then pass in C{noEject=False}. This 437 will override the properties and the device will never be ejected. 438 439 @note: The C{unittest} parameter should never be set to C{True} 440 outside of Cedar Backup code. It is intended for use in unit testing 441 Cedar Backup internals and has no other sensible purpose. 442 443 @param device: Filesystem device associated with this writer. 444 @type device: Absolute path to a filesystem device, i.e. C{/dev/cdrw} 445 446 @param scsiId: SCSI id for the device (optional). 447 @type scsiId: If provided, SCSI id in the form C{[<method>:]scsibus,target,lun} 448 449 @param driveSpeed: Speed at which the drive writes. 450 @type driveSpeed: Use C{2} for 2x device, etc. or C{None} to use device default. 451 452 @param mediaType: Type of the media that is assumed to be in the drive. 453 @type mediaType: One of the valid media type as discussed above. 454 455 @param noEject: Overrides properties to indicate that the device does not support eject. 456 @type noEject: Boolean true/false 457 458 @param refreshMediaDelay: Refresh media delay to use, if any 459 @type refreshMediaDelay: Number of seconds, an integer >= 0 460 461 @param unittest: Turns off certain validations, for use in unit testing. 462 @type unittest: Boolean true/false 463 464 @raise ValueError: If the device is not valid for some reason. 465 @raise ValueError: If the SCSI id is not in a valid form. 466 @raise ValueError: If the drive speed is not an integer >= 1. 467 @raise IOError: If device properties could not be read for some reason. 468 """ 469 self._image = None # optionally filled in by initializeImage() 470 self._device = validateDevice(device, unittest) 471 self._scsiId = validateScsiId(scsiId) 472 self._driveSpeed = validateDriveSpeed(driveSpeed) 473 self._media = MediaDefinition(mediaType) 474 self._noEject = noEject 475 self._refreshMediaDelay = refreshMediaDelay 476 if not unittest: 477 (self._deviceType, 478 self._deviceVendor, 479 self._deviceId, 480 self._deviceBufferSize, 481 self._deviceSupportsMulti, 482 self._deviceHasTray, 483 self._deviceCanEject) = self._retrieveProperties()
484 485 486 ############# 487 # Properties 488 ############# 489
490 - def _getDevice(self):
491 """ 492 Property target used to get the device value. 493 """ 494 return self._device
495
496 - def _getScsiId(self):
497 """ 498 Property target used to get the SCSI id value. 499 """ 500 return self._scsiId
501
502 - def _getHardwareId(self):
503 """ 504 Property target used to get the hardware id value. 505 """ 506 if self._scsiId is None: 507 return self._device 508 return self._scsiId
509
510 - def _getDriveSpeed(self):
511 """ 512 Property target used to get the drive speed. 513 """ 514 return self._driveSpeed
515
516 - def _getMedia(self):
517 """ 518 Property target used to get the media description. 519 """ 520 return self._media
521
522 - def _getDeviceType(self):
523 """ 524 Property target used to get the device type. 525 """ 526 return self._deviceType
527
528 - def _getDeviceVendor(self):
529 """ 530 Property target used to get the device vendor. 531 """ 532 return self._deviceVendor
533
534 - def _getDeviceId(self):
535 """ 536 Property target used to get the device id. 537 """ 538 return self._deviceId
539
540 - def _getDeviceBufferSize(self):
541 """ 542 Property target used to get the device buffer size. 543 """ 544 return self._deviceBufferSize
545
546 - def _getDeviceSupportsMulti(self):
547 """ 548 Property target used to get the device-support-multi flag. 549 """ 550 return self._deviceSupportsMulti
551
552 - def _getDeviceHasTray(self):
553 """ 554 Property target used to get the device-has-tray flag. 555 """ 556 return self._deviceHasTray
557
558 - def _getDeviceCanEject(self):
559 """ 560 Property target used to get the device-can-eject flag. 561 """ 562 return self._deviceCanEject
563
564 - def _getRefreshMediaDelay(self):
565 """ 566 Property target used to get the configured refresh media delay, in seconds. 567 """ 568 return self._refreshMediaDelay
569 570 device = property(_getDevice, None, None, doc="Filesystem device name for this writer.") 571 scsiId = property(_getScsiId, None, None, doc="SCSI id for the device, in the form C{[<method>:]scsibus,target,lun}.") 572 hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer, either SCSI id or device path."); 573 driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.") 574 media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.") 575 deviceType = property(_getDeviceType, None, None, doc="Type of the device, as returned from C{cdrecord -prcap}.") 576 deviceVendor = property(_getDeviceVendor, None, None, doc="Vendor of the device, as returned from C{cdrecord -prcap}.") 577 deviceId = property(_getDeviceId, None, None, doc="Device identification, as returned from C{cdrecord -prcap}.") 578 deviceBufferSize = property(_getDeviceBufferSize, None, None, doc="Size of the device's write buffer, in bytes.") 579 deviceSupportsMulti = property(_getDeviceSupportsMulti, None, None, doc="Indicates whether device supports multisession discs.") 580 deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.") 581 deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.") 582 refreshMediaDelay = property(_getRefreshMediaDelay, None, None, doc="Refresh media delay, in seconds.") 583 584 585 ################################################# 586 # Methods related to device and media attributes 587 ################################################# 588
589 - def isRewritable(self):
590 """Indicates whether the media is rewritable per configuration.""" 591 return self._media.rewritable
592
593 - def _retrieveProperties(self):
594 """ 595 Retrieves properties for a device from C{cdrecord}. 596 597 The results are returned as a tuple of the object device attributes as 598 returned from L{_parsePropertiesOutput}: C{(deviceType, deviceVendor, 599 deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, 600 deviceCanEject)}. 601 602 @return: Results tuple as described above. 603 @raise IOError: If there is a problem talking to the device. 604 """ 605 args = CdWriter._buildPropertiesArgs(self.hardwareId) 606 command = resolveCommand(CDRECORD_COMMAND) 607 (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True) 608 if result != 0: 609 raise IOError("Error (%d) executing cdrecord command to get properties." % result) 610 return CdWriter._parsePropertiesOutput(output)
611
612 - def retrieveCapacity(self, entireDisc=False, useMulti=True):
613 """ 614 Retrieves capacity for the current media in terms of a C{MediaCapacity} 615 object. 616 617 If C{entireDisc} is passed in as C{True} the capacity will be for the 618 entire disc, as if it were to be rewritten from scratch. If the drive 619 does not support writing multisession discs or if C{useMulti} is passed 620 in as C{False}, the capacity will also be as if the disc were to be 621 rewritten from scratch, but the indicated boundaries value will be 622 C{None}. The same will happen if the disc cannot be read for some 623 reason. Otherwise, the capacity (including the boundaries) will 624 represent whatever space remains on the disc to be filled by future 625 sessions. 626 627 @param entireDisc: Indicates whether to return capacity for entire disc. 628 @type entireDisc: Boolean true/false 629 630 @param useMulti: Indicates whether a multisession disc should be assumed, if possible. 631 @type useMulti: Boolean true/false 632 633 @return: C{MediaCapacity} object describing the capacity of the media. 634 @raise IOError: If the media could not be read for some reason. 635 """ 636 boundaries = self._getBoundaries(entireDisc, useMulti) 637 return CdWriter._calculateCapacity(self._media, boundaries)
638
639 - def _getBoundaries(self, entireDisc=False, useMulti=True):
640 """ 641 Gets the ISO boundaries for the media. 642 643 If C{entireDisc} is passed in as C{True} the boundaries will be C{None}, 644 as if the disc were to be rewritten from scratch. If the drive does not 645 support writing multisession discs, the returned value will be C{None}. 646 The same will happen if the disc can't be read for some reason. 647 Otherwise, the returned value will be represent the boundaries of the 648 disc's current contents. 649 650 The results are returned as a tuple of (lower, upper) as needed by the 651 C{IsoImage} class. Note that these values are in terms of ISO sectors, 652 not bytes. Clients should generally consider the boundaries value 653 opaque, however. 654 655 @param entireDisc: Indicates whether to return capacity for entire disc. 656 @type entireDisc: Boolean true/false 657 658 @param useMulti: Indicates whether a multisession disc should be assumed, if possible. 659 @type useMulti: Boolean true/false 660 661 @return: Boundaries tuple or C{None}, as described above. 662 @raise IOError: If the media could not be read for some reason. 663 """ 664 if not self._deviceSupportsMulti: 665 logger.debug("Device does not support multisession discs; returning boundaries None.") 666 return None 667 elif not useMulti: 668 logger.debug("Use multisession flag is False; returning boundaries None.") 669 return None 670 elif entireDisc: 671 logger.debug("Entire disc flag is True; returning boundaries None.") 672 return None 673 else: 674 args = CdWriter._buildBoundariesArgs(self.hardwareId) 675 command = resolveCommand(CDRECORD_COMMAND) 676 (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True) 677 if result != 0: 678 logger.debug("Error (%d) executing cdrecord command to get capacity." % result) 679 logger.warn("Unable to read disc (might not be initialized); returning boundaries of None.") 680 return None 681 boundaries = CdWriter._parseBoundariesOutput(output) 682 if boundaries is None: 683 logger.debug("Returning disc boundaries: None") 684 else: 685 logger.debug("Returning disc boundaries: (%d, %d)" % (boundaries[0], boundaries[1])) 686 return boundaries
687
688 - def _calculateCapacity(media, boundaries):
689 """ 690 Calculates capacity for the media in terms of boundaries. 691 692 If C{boundaries} is C{None} or the lower bound is 0 (zero), then the 693 capacity will be for the entire disc minus the initial lead in. 694 Otherwise, capacity will be as if the caller wanted to add an additional 695 session to the end of the existing data on the disc. 696 697 @param media: MediaDescription object describing the media capacity. 698 @param boundaries: Session boundaries as returned from L{_getBoundaries}. 699 700 @return: C{MediaCapacity} object describing the capacity of the media. 701 """ 702 if boundaries is None or boundaries[1] == 0: 703 logger.debug("Capacity calculations are based on a complete disc rewrite.") 704 sectorsAvailable = media.capacity - media.initialLeadIn 705 if sectorsAvailable < 0: sectorsAvailable = 0 706 bytesUsed = 0 707 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 708 else: 709 logger.debug("Capacity calculations are based on a new ISO session.") 710 sectorsAvailable = media.capacity - boundaries[1] - media.leadIn 711 if sectorsAvailable < 0: sectorsAvailable = 0 712 bytesUsed = convertSize(boundaries[1], UNIT_SECTORS, UNIT_BYTES) 713 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 714 logger.debug("Used [%s], available [%s]." % (displayBytes(bytesUsed), displayBytes(bytesAvailable))) 715 return MediaCapacity(bytesUsed, bytesAvailable, boundaries)
716 _calculateCapacity = staticmethod(_calculateCapacity) 717 718 719 ####################################################### 720 # Methods used for working with the internal ISO image 721 ####################################################### 722
723 - def initializeImage(self, newDisc, tmpdir, mediaLabel=None):
724 """ 725 Initializes the writer's associated ISO image. 726 727 This method initializes the C{image} instance variable so that the caller 728 can use the C{addImageEntry} method. Once entries have been added, the 729 C{writeImage} method can be called with no arguments. 730 731 @param newDisc: Indicates whether the disc should be re-initialized 732 @type newDisc: Boolean true/false. 733 734 @param tmpdir: Temporary directory to use if needed 735 @type tmpdir: String representing a directory path on disk 736 737 @param mediaLabel: Media label to be applied to the image, if any 738 @type mediaLabel: String, no more than 25 characters long 739 """ 740 self._image = _ImageProperties() 741 self._image.newDisc = newDisc 742 self._image.tmpdir = encodePath(tmpdir) 743 self._image.mediaLabel = mediaLabel 744 self._image.entries = {} # mapping from path to graft point (if any)
745
746 - def addImageEntry(self, path, graftPoint):
747 """ 748 Adds a filepath entry to the writer's associated ISO image. 749 750 The contents of the filepath -- but not the path itself -- will be added 751 to the image at the indicated graft point. If you don't want to use a 752 graft point, just pass C{None}. 753 754 @note: Before calling this method, you must call L{initializeImage}. 755 756 @param path: File or directory to be added to the image 757 @type path: String representing a path on disk 758 759 @param graftPoint: Graft point to be used when adding this entry 760 @type graftPoint: String representing a graft point path, as described above 761 762 @raise ValueError: If initializeImage() was not previously called 763 """ 764 if self._image is None: 765 raise ValueError("Must call initializeImage() before using this method.") 766 if not os.path.exists(path): 767 raise ValueError("Path [%s] does not exist." % path) 768 self._image.entries[path] = graftPoint
769
770 - def setImageNewDisc(self, newDisc):
771 """ 772 Resets (overrides) the newDisc flag on the internal image. 773 @param newDisc: New disc flag to set 774 @raise ValueError: If initializeImage() was not previously called 775 """ 776 if self._image is None: 777 raise ValueError("Must call initializeImage() before using this method.") 778 self._image.newDisc = newDisc
779
780 - def getEstimatedImageSize(self):
781 """ 782 Gets the estimated size of the image associated with the writer. 783 @return: Estimated size of the image, in bytes. 784 @raise IOError: If there is a problem calling C{mkisofs}. 785 @raise ValueError: If initializeImage() was not previously called 786 """ 787 if self._image is None: 788 raise ValueError("Must call initializeImage() before using this method.") 789 image = IsoImage() 790 for path in self._image.entries.keys(): 791 image.addEntry(path, self._image.entries[path], override=False, contentsOnly=True) 792 return image.getEstimatedSize()
793 794 795 ###################################### 796 # Methods which expose device actions 797 ###################################### 798
799 - def openTray(self):
800 """ 801 Opens the device's tray and leaves it open. 802 803 This only works if the device has a tray and supports ejecting its media. 804 We have no way to know if the tray is currently open or closed, so we 805 just send the appropriate command and hope for the best. If the device 806 does not have a tray or does not support ejecting its media, then we do 807 nothing. 808 809 If the writer was constructed with C{noEject=True}, then this is a no-op. 810 811 @raise IOError: If there is an error talking to the device. 812 """ 813 if not self._noEject: 814 if self._deviceHasTray and self._deviceCanEject: 815 args = CdWriter._buildOpenTrayArgs(self._device) 816 command = resolveCommand(EJECT_COMMAND) 817 result = executeCommand(command, args)[0] 818 if result != 0: 819 raise IOError("Error (%d) executing eject command to open tray." % result)
820
821 - def closeTray(self):
822 """ 823 Closes the device's tray. 824 825 This only works if the device has a tray and supports ejecting its media. 826 We have no way to know if the tray is currently open or closed, so we 827 just send the appropriate command and hope for the best. If the device 828 does not have a tray or does not support ejecting its media, then we do 829 nothing. 830 831 If the writer was constructed with C{noEject=True}, then this is a no-op. 832 833 @raise IOError: If there is an error talking to the device. 834 """ 835 if not self._noEject: 836 if self._deviceHasTray and self._deviceCanEject: 837 args = CdWriter._buildCloseTrayArgs(self._device) 838 command = resolveCommand(EJECT_COMMAND) 839 result = executeCommand(command, args)[0] 840 if result != 0: 841 raise IOError("Error (%d) executing eject command to close tray." % result)
842
843 - def refreshMedia(self):
844 """ 845 Opens and then immediately closes the device's tray, to refresh the 846 device's idea of the media. 847 848 Sometimes, a device gets confused about the state of its media. Often, 849 all it takes to solve the problem is to eject the media and then 850 immediately reload it. (There is also a configurable refresh media delay 851 which can be applied after the tray is closed, for situations where this 852 makes a difference.) 853 854 This only works if the device has a tray and supports ejecting its media. 855 We have no way to know if the tray is currently open or closed, so we 856 just send the appropriate command and hope for the best. If the device 857 does not have a tray or does not support ejecting its media, then we do 858 nothing. The configured delay still applies, though. 859 860 @raise IOError: If there is an error talking to the device. 861 """ 862 self.openTray() 863 self.closeTray() 864 if self.refreshMediaDelay is not None: 865 logger.debug("Per configuration, sleeping %d seconds to stabilize media state." % self.refreshMediaDelay) 866 time.sleep(self.refreshMediaDelay) 867 logger.debug("Sleep is complete; hopefully media state is stable now.")
868
869 - def writeImage(self, imagePath=None, newDisc=False, writeMulti=True):
870 """ 871 Writes an ISO image to the media in the device. 872 873 If C{newDisc} is passed in as C{True}, we assume that the entire disc 874 will be overwritten, and the media will be blanked before writing it if 875 possible (i.e. if the media is rewritable). 876 877 If C{writeMulti} is passed in as C{True}, then a multisession disc will 878 be written if possible (i.e. if the drive supports writing multisession 879 discs). 880 881 if C{imagePath} is passed in as C{None}, then the existing image 882 configured with C{initializeImage} will be used. Under these 883 circumstances, the passed-in C{newDisc} flag will be ignored. 884 885 By default, we assume that the disc can be written multisession and that 886 we should append to the current contents of the disc. In any case, the 887 ISO image must be generated appropriately (i.e. must take into account 888 any existing session boundaries, etc.) 889 890 @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image 891 @type imagePath: String representing a path on disk 892 893 @param newDisc: Indicates whether the entire disc will overwritten. 894 @type newDisc: Boolean true/false. 895 896 @param writeMulti: Indicates whether a multisession disc should be written, if possible. 897 @type writeMulti: Boolean true/false 898 899 @raise ValueError: If the image path is not absolute. 900 @raise ValueError: If some path cannot be encoded properly. 901 @raise IOError: If the media could not be written to for some reason. 902 @raise ValueError: If no image is passed in and initializeImage() was not previously called 903 """ 904 if imagePath is None: 905 if self._image is None: 906 raise ValueError("Must call initializeImage() before using this method with no image path.") 907 try: 908 imagePath = self._createImage() 909 self._writeImage(imagePath, writeMulti, self._image.newDisc) 910 finally: 911 if imagePath is not None and os.path.exists(imagePath): 912 try: os.unlink(imagePath) 913 except: pass 914 else: 915 imagePath = encodePath(imagePath) 916 if not os.path.isabs(imagePath): 917 raise ValueError("Image path must be absolute.") 918 self._writeImage(imagePath, writeMulti, newDisc)
919
920 - def _createImage(self):
921 """ 922 Creates an ISO image based on configuration in self._image. 923 @return: Path to the newly-created ISO image on disk. 924 @raise IOError: If there is an error writing the image to disk. 925 @raise ValueError: If there are no filesystem entries in the image 926 @raise ValueError: If a path cannot be encoded properly. 927 """ 928 path = None 929 capacity = self.retrieveCapacity(entireDisc=self._image.newDisc) 930 image = IsoImage(self.device, capacity.boundaries) 931 image.volumeId = self._image.mediaLabel # may be None, which is also valid 932 for key in self._image.entries.keys(): 933 image.addEntry(key, self._image.entries[key], override=False, contentsOnly=True) 934 size = image.getEstimatedSize() 935 logger.info("Image size will be %s." % displayBytes(size)) 936 available = capacity.bytesAvailable 937 logger.debug("Media capacity: %s" % displayBytes(available)) 938 if size > available: 939 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available))) 940 raise IOError("Media does not contain enough capacity to store image.") 941 try: 942 (handle, path) = tempfile.mkstemp(dir=self._image.tmpdir) 943 try: os.close(handle) 944 except: pass 945 image.writeImage(path) 946 logger.debug("Completed creating image [%s]." % path) 947 return path 948 except Exception, e: 949 if path is not None and os.path.exists(path): 950 try: os.unlink(path) 951 except: pass 952 raise e
953
954 - def _writeImage(self, imagePath, writeMulti, newDisc):
955 """ 956 Write an ISO image to disc using cdrecord. 957 The disc is blanked first if C{newDisc} is C{True}. 958 @param imagePath: Path to an ISO image on disk 959 @param writeMulti: Indicates whether a multisession disc should be written, if possible. 960 @param newDisc: Indicates whether the entire disc will overwritten. 961 """ 962 if newDisc: 963 self._blankMedia() 964 args = CdWriter._buildWriteArgs(self.hardwareId, imagePath, self._driveSpeed, writeMulti and self._deviceSupportsMulti) 965 command = resolveCommand(CDRECORD_COMMAND) 966 result = executeCommand(command, args)[0] 967 if result != 0: 968 raise IOError("Error (%d) executing command to write disc." % result) 969 self.refreshMedia()
970
971 - def _blankMedia(self):
972 """ 973 Blanks the media in the device, if the media is rewritable. 974 @raise IOError: If the media could not be written to for some reason. 975 """ 976 if self.isRewritable(): 977 args = CdWriter._buildBlankArgs(self.hardwareId) 978 command = resolveCommand(CDRECORD_COMMAND) 979 result = executeCommand(command, args)[0] 980 if result != 0: 981 raise IOError("Error (%d) executing command to blank disc." % result) 982 self.refreshMedia()
983 984 985 ####################################### 986 # Methods used to parse command output 987 ####################################### 988
989 - def _parsePropertiesOutput(output):
990 """ 991 Parses the output from a C{cdrecord} properties command. 992 993 The C{output} parameter should be a list of strings as returned from 994 C{executeCommand} for a C{cdrecord} command with arguments as from 995 C{_buildPropertiesArgs}. The list of strings will be parsed to yield 996 information about the properties of the device. 997 998 The output is expected to be a huge long list of strings. Unfortunately, 999 the strings aren't in a completely regular format. However, the format 1000 of individual lines seems to be regular enough that we can look for 1001 specific values. Two kinds of parsing take place: one kind of parsing 1002 picks out out specific values like the device id, device vendor, etc. 1003 The other kind of parsing just sets a boolean flag C{True} if a matching 1004 line is found. All of the parsing is done with regular expressions. 1005 1006 Right now, pretty much nothing in the output is required and we should 1007 parse an empty document successfully (albeit resulting in a device that 1008 can't eject, doesn't have a tray and doesnt't support multisession 1009 discs). I had briefly considered erroring out if certain lines weren't 1010 found or couldn't be parsed, but that seems like a bad idea given that 1011 most of the information is just for reference. 1012 1013 The results are returned as a tuple of the object device attributes: 1014 C{(deviceType, deviceVendor, deviceId, deviceBufferSize, 1015 deviceSupportsMulti, deviceHasTray, deviceCanEject)}. 1016 1017 @param output: Output from a C{cdrecord -prcap} command. 1018 1019 @return: Results tuple as described above. 1020 @raise IOError: If there is problem parsing the output. 1021 """ 1022 deviceType = None 1023 deviceVendor = None 1024 deviceId = None 1025 deviceBufferSize = None 1026 deviceSupportsMulti = False 1027 deviceHasTray = False 1028 deviceCanEject = False 1029 typePattern = re.compile(r"(^Device type\s*:\s*)(.*)(\s*)(.*$)") 1030 vendorPattern = re.compile(r"(^Vendor_info\s*:\s*'\s*)(.*?)(\s*')(.*$)") 1031 idPattern = re.compile(r"(^Identifikation\s*:\s*'\s*)(.*?)(\s*')(.*$)") 1032 bufferPattern = re.compile(r"(^\s*Buffer size in KB:\s*)(.*?)(\s*$)") 1033 multiPattern = re.compile(r"^\s*Does read multi-session.*$") 1034 trayPattern = re.compile(r"^\s*Loading mechanism type: tray.*$") 1035 ejectPattern = re.compile(r"^\s*Does support ejection.*$") 1036 for line in output: 1037 if typePattern.search(line): 1038 deviceType = typePattern.search(line).group(2) 1039 logger.info("Device type is [%s]." % deviceType) 1040 elif vendorPattern.search(line): 1041 deviceVendor = vendorPattern.search(line).group(2) 1042 logger.info("Device vendor is [%s]." % deviceVendor) 1043 elif idPattern.search(line): 1044 deviceId = idPattern.search(line).group(2) 1045 logger.info("Device id is [%s]." % deviceId) 1046 elif bufferPattern.search(line): 1047 try: 1048 sectors = int(bufferPattern.search(line).group(2)) 1049 deviceBufferSize = convertSize(sectors, UNIT_KBYTES, UNIT_BYTES) 1050 logger.info("Device buffer size is [%d] bytes." % deviceBufferSize) 1051 except TypeError: pass 1052 elif multiPattern.search(line): 1053 deviceSupportsMulti = True 1054 logger.info("Device does support multisession discs.") 1055 elif trayPattern.search(line): 1056 deviceHasTray = True 1057 logger.info("Device has a tray.") 1058 elif ejectPattern.search(line): 1059 deviceCanEject = True 1060 logger.info("Device can eject its media.") 1061 return (deviceType, deviceVendor, deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, deviceCanEject)
1062 _parsePropertiesOutput = staticmethod(_parsePropertiesOutput) 1063
1064 - def _parseBoundariesOutput(output):
1065 """ 1066 Parses the output from a C{cdrecord} capacity command. 1067 1068 The C{output} parameter should be a list of strings as returned from 1069 C{executeCommand} for a C{cdrecord} command with arguments as from 1070 C{_buildBoundaryArgs}. The list of strings will be parsed to yield 1071 information about the capacity of the media in the device. 1072 1073 Basically, we expect the list of strings to include just one line, a pair 1074 of values. There isn't supposed to be whitespace, but we allow it anyway 1075 in the regular expression. Any lines below the one line we parse are 1076 completely ignored. It would be a good idea to ignore C{stderr} when 1077 executing the C{cdrecord} command that generates output for this method, 1078 because sometimes C{cdrecord} spits out kernel warnings about the actual 1079 output. 1080 1081 The results are returned as a tuple of (lower, upper) as needed by the 1082 C{IsoImage} class. Note that these values are in terms of ISO sectors, 1083 not bytes. Clients should generally consider the boundaries value 1084 opaque, however. 1085 1086 @note: If the boundaries output can't be parsed, we return C{None}. 1087 1088 @param output: Output from a C{cdrecord -msinfo} command. 1089 1090 @return: Boundaries tuple as described above. 1091 @raise IOError: If there is problem parsing the output. 1092 """ 1093 if len(output) < 1: 1094 logger.warn("Unable to read disc (might not be initialized); returning full capacity.") 1095 return None 1096 boundaryPattern = re.compile(r"(^\s*)([0-9]*)(\s*,\s*)([0-9]*)(\s*$)") 1097 parsed = boundaryPattern.search(output[0]) 1098 if not parsed: 1099 raise IOError("Unable to parse output of boundaries command.") 1100 try: 1101 boundaries = ( int(parsed.group(2)), int(parsed.group(4)) ) 1102 except TypeError: 1103 raise IOError("Unable to parse output of boundaries command.") 1104 return boundaries
1105 _parseBoundariesOutput = staticmethod(_parseBoundariesOutput) 1106 1107 1108 ################################# 1109 # Methods used to build commands 1110 ################################# 1111
1112 - def _buildOpenTrayArgs(device):
1113 """ 1114 Builds a list of arguments to be passed to a C{eject} command. 1115 1116 The arguments will cause the C{eject} command to open the tray and 1117 eject the media. No validation is done by this method as to whether 1118 this action actually makes sense. 1119 1120 @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}. 1121 1122 @return: List suitable for passing to L{util.executeCommand} as C{args}. 1123 """ 1124 args = [] 1125 args.append(device) 1126 return args
1127 _buildOpenTrayArgs = staticmethod(_buildOpenTrayArgs) 1128
1129 - def _buildCloseTrayArgs(device):
1130 """ 1131 Builds a list of arguments to be passed to a C{eject} command. 1132 1133 The arguments will cause the C{eject} command to close the tray and reload 1134 the media. No validation is done by this method as to whether this 1135 action actually makes sense. 1136 1137 @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}. 1138 1139 @return: List suitable for passing to L{util.executeCommand} as C{args}. 1140 """ 1141 args = [] 1142 args.append("-t") 1143 args.append(device) 1144 return args
1145 _buildCloseTrayArgs = staticmethod(_buildCloseTrayArgs) 1146
1147 - def _buildPropertiesArgs(hardwareId):
1148 """ 1149 Builds a list of arguments to be passed to a C{cdrecord} command. 1150 1151 The arguments will cause the C{cdrecord} command to ask the device 1152 for a list of its capacities via the C{-prcap} switch. 1153 1154 @param hardwareId: Hardware id for the device (either SCSI id or device path) 1155 1156 @return: List suitable for passing to L{util.executeCommand} as C{args}. 1157 """ 1158 args = [] 1159 args.append("-prcap") 1160 args.append("dev=%s" % hardwareId) 1161 return args
1162 _buildPropertiesArgs = staticmethod(_buildPropertiesArgs) 1163
1164 - def _buildBoundariesArgs(hardwareId):
1165 """ 1166 Builds a list of arguments to be passed to a C{cdrecord} command. 1167 1168 The arguments will cause the C{cdrecord} command to ask the device for 1169 the current multisession boundaries of the media using the C{-msinfo} 1170 switch. 1171 1172 @param hardwareId: Hardware id for the device (either SCSI id or device path) 1173 1174 @return: List suitable for passing to L{util.executeCommand} as C{args}. 1175 """ 1176 args = [] 1177 args.append("-msinfo") 1178 args.append("dev=%s" % hardwareId) 1179 return args
1180 _buildBoundariesArgs = staticmethod(_buildBoundariesArgs) 1181
1182 - def _buildBlankArgs(hardwareId, driveSpeed=None):
1183 """ 1184 Builds a list of arguments to be passed to a C{cdrecord} command. 1185 1186 The arguments will cause the C{cdrecord} command to blank the media in 1187 the device identified by C{hardwareId}. No validation is done by this method 1188 as to whether the action makes sense (i.e. to whether the media even can 1189 be blanked). 1190 1191 @param hardwareId: Hardware id for the device (either SCSI id or device path) 1192 @param driveSpeed: Speed at which the drive writes. 1193 1194 @return: List suitable for passing to L{util.executeCommand} as C{args}. 1195 """ 1196 args = [] 1197 args.append("-v") 1198 args.append("blank=fast") 1199 if driveSpeed is not None: 1200 args.append("speed=%d" % driveSpeed) 1201 args.append("dev=%s" % hardwareId) 1202 return args
1203 _buildBlankArgs = staticmethod(_buildBlankArgs) 1204
1205 - def _buildWriteArgs(hardwareId, imagePath, driveSpeed=None, writeMulti=True):
1206 """ 1207 Builds a list of arguments to be passed to a C{cdrecord} command. 1208 1209 The arguments will cause the C{cdrecord} command to write the indicated 1210 ISO image (C{imagePath}) to the media in the device identified by 1211 C{hardwareId}. The C{writeMulti} argument controls whether to write a 1212 multisession disc. No validation is done by this method as to whether 1213 the action makes sense (i.e. to whether the device even can write 1214 multisession discs, for instance). 1215 1216 @param hardwareId: Hardware id for the device (either SCSI id or device path) 1217 @param imagePath: Path to an ISO image on disk. 1218 @param driveSpeed: Speed at which the drive writes. 1219 @param writeMulti: Indicates whether to write a multisession disc. 1220 1221 @return: List suitable for passing to L{util.executeCommand} as C{args}. 1222 """ 1223 args = [] 1224 args.append("-v") 1225 if driveSpeed is not None: 1226 args.append("speed=%d" % driveSpeed) 1227 args.append("dev=%s" % hardwareId) 1228 if writeMulti: 1229 args.append("-multi") 1230 args.append("-data") 1231 args.append(imagePath) 1232 return args
1233 _buildWriteArgs = staticmethod(_buildWriteArgs)
1234