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