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

Source Code for Module CedarBackup2.writers.dvdwriter

  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) 2007-2008,2010 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.5) 
 29  # Project  : Cedar Backup, release 2 
 30  # Purpose  : Provides functionality related to DVD writer devices. 
 31  # 
 32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
 33   
 34  ######################################################################## 
 35  # Module documentation 
 36  ######################################################################## 
 37   
 38  """ 
 39  Provides functionality related to DVD writer devices. 
 40   
 41  @sort: MediaDefinition, DvdWriter, MEDIA_DVDPLUSR, MEDIA_DVDPLUSRW 
 42   
 43  @var MEDIA_DVDPLUSR: Constant representing DVD+R media. 
 44  @var MEDIA_DVDPLUSRW: Constant representing DVD+RW media. 
 45   
 46  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
 47  @author: Dmitry Rutsky <rutsky@inbox.ru> 
 48  """ 
 49   
 50  ######################################################################## 
 51  # Imported modules 
 52  ######################################################################## 
 53   
 54  # System modules 
 55  import os 
 56  import re 
 57  import logging 
 58  import tempfile 
 59  import time 
 60   
 61  # Cedar Backup modules 
 62  from CedarBackup2.writers.util import IsoImage 
 63  from CedarBackup2.util import resolveCommand, executeCommand 
 64  from CedarBackup2.util import convertSize, displayBytes, encodePath 
 65  from CedarBackup2.util import UNIT_SECTORS, UNIT_BYTES, UNIT_GBYTES 
 66  from CedarBackup2.writers.util import validateDevice, validateDriveSpeed 
 67   
 68   
 69  ######################################################################## 
 70  # Module-wide constants and variables 
 71  ######################################################################## 
 72   
 73  logger = logging.getLogger("CedarBackup2.log.writers.dvdwriter") 
 74   
 75  MEDIA_DVDPLUSR  = 1 
 76  MEDIA_DVDPLUSRW = 2 
 77   
 78  GROWISOFS_COMMAND = [ "growisofs", ] 
 79  EJECT_COMMAND     = [ "eject", ] 
80 81 82 ######################################################################## 83 # MediaDefinition class definition 84 ######################################################################## 85 86 -class MediaDefinition(object):
87 88 """ 89 Class encapsulating information about DVD media definitions. 90 91 The following media types are accepted: 92 93 - C{MEDIA_DVDPLUSR}: DVD+R media (4.4 GB capacity) 94 - C{MEDIA_DVDPLUSRW}: DVD+RW media (4.4 GB capacity) 95 96 Note that the capacity attribute returns capacity in terms of ISO sectors 97 (C{util.ISO_SECTOR_SIZE)}. This is for compatibility with the CD writer 98 functionality. 99 100 The capacities are 4.4 GB because Cedar Backup deals in "true" gigabytes 101 of 1024*1024*1024 bytes per gigabyte. 102 103 @sort: __init__, mediaType, rewritable, capacity 104 """ 105
106 - def __init__(self, mediaType):
107 """ 108 Creates a media definition for the indicated media type. 109 @param mediaType: Type of the media, as discussed above. 110 @raise ValueError: If the media type is unknown or unsupported. 111 """ 112 self._mediaType = None 113 self._rewritable = False 114 self._capacity = 0.0 115 self._setValues(mediaType)
116
117 - def _setValues(self, mediaType):
118 """ 119 Sets values based on media type. 120 @param mediaType: Type of the media, as discussed above. 121 @raise ValueError: If the media type is unknown or unsupported. 122 """ 123 if mediaType not in [MEDIA_DVDPLUSR, MEDIA_DVDPLUSRW, ]: 124 raise ValueError("Invalid media type %d." % mediaType) 125 self._mediaType = mediaType 126 if self._mediaType == MEDIA_DVDPLUSR: 127 self._rewritable = False 128 self._capacity = convertSize(4.4, UNIT_GBYTES, UNIT_SECTORS) # 4.4 "true" GB = 4.7 "marketing" GB 129 elif self._mediaType == MEDIA_DVDPLUSRW: 130 self._rewritable = True 131 self._capacity = convertSize(4.4, UNIT_GBYTES, UNIT_SECTORS) # 4.4 "true" GB = 4.7 "marketing" GB
132
133 - def _getMediaType(self):
134 """ 135 Property target used to get the media type value. 136 """ 137 return self._mediaType
138
139 - def _getRewritable(self):
140 """ 141 Property target used to get the rewritable flag value. 142 """ 143 return self._rewritable
144
145 - def _getCapacity(self):
146 """ 147 Property target used to get the capacity value. 148 """ 149 return self._capacity
150 151 mediaType = property(_getMediaType, None, None, doc="Configured media type.") 152 rewritable = property(_getRewritable, None, None, doc="Boolean indicating whether the media is rewritable.") 153 capacity = property(_getCapacity, None, None, doc="Total capacity of media in 2048-byte sectors.")
154
155 156 ######################################################################## 157 # MediaCapacity class definition 158 ######################################################################## 159 160 -class MediaCapacity(object):
161 162 """ 163 Class encapsulating information about DVD media capacity. 164 165 Space used and space available do not include any information about media 166 lead-in or other overhead. 167 168 @sort: __init__, bytesUsed, bytesAvailable, totalCapacity, utilized 169 """ 170
171 - def __init__(self, bytesUsed, bytesAvailable):
172 """ 173 Initializes a capacity object. 174 @raise ValueError: If the bytes used and available values are not floats. 175 """ 176 self._bytesUsed = float(bytesUsed) 177 self._bytesAvailable = float(bytesAvailable)
178
179 - def __str__(self):
180 """ 181 Informal string representation for class instance. 182 """ 183 return "utilized %s of %s (%.2f%%)" % (displayBytes(self.bytesUsed), displayBytes(self.totalCapacity), self.utilized)
184
185 - def _getBytesUsed(self):
186 """ 187 Property target used to get the bytes-used value. 188 """ 189 return self._bytesUsed
190
191 - def _getBytesAvailable(self):
192 """ 193 Property target available to get the bytes-available value. 194 """ 195 return self._bytesAvailable
196
197 - def _getTotalCapacity(self):
198 """ 199 Property target to get the total capacity (used + available). 200 """ 201 return self.bytesUsed + self.bytesAvailable
202
203 - def _getUtilized(self):
204 """ 205 Property target to get the percent of capacity which is utilized. 206 """ 207 if self.bytesAvailable <= 0.0: 208 return 100.0 209 elif self.bytesUsed <= 0.0: 210 return 0.0 211 return (self.bytesUsed / self.totalCapacity) * 100.0
212 213 bytesUsed = property(_getBytesUsed, None, None, doc="Space used on disc, in bytes.") 214 bytesAvailable = property(_getBytesAvailable, None, None, doc="Space available on disc, in bytes.") 215 totalCapacity = property(_getTotalCapacity, None, None, doc="Total capacity of the disc, in bytes.") 216 utilized = property(_getUtilized, None, None, "Percentage of the total capacity which is utilized.")
217
218 219 ######################################################################## 220 # _ImageProperties class definition 221 ######################################################################## 222 223 -class _ImageProperties(object):
224 """ 225 Simple value object to hold image properties for C{DvdWriter}. 226 """
227 - def __init__(self):
228 self.newDisc = False 229 self.tmpdir = None 230 self.mediaLabel = None 231 self.entries = None # dict mapping path to graft point
232
233 234 ######################################################################## 235 # DvdWriter class definition 236 ######################################################################## 237 238 -class DvdWriter(object):
239 240 ###################### 241 # Class documentation 242 ###################### 243 244 """ 245 Class representing a device that knows how to write some kinds of DVD media. 246 247 Summary 248 ======= 249 250 This is a class representing a device that knows how to write some kinds 251 of DVD media. It provides common operations for the device, such as 252 ejecting the media and writing data to the media. 253 254 This class is implemented in terms of the C{eject} and C{growisofs} 255 utilities, all of which should be available on most UN*X platforms. 256 257 Image Writer Interface 258 ====================== 259 260 The following methods make up the "image writer" interface shared 261 with other kinds of writers:: 262 263 __init__ 264 initializeImage() 265 addImageEntry() 266 writeImage() 267 setImageNewDisc() 268 retrieveCapacity() 269 getEstimatedImageSize() 270 271 Only these methods will be used by other Cedar Backup functionality 272 that expects a compatible image writer. 273 274 The media attribute is also assumed to be available. 275 276 Unlike the C{CdWriter}, the C{DvdWriter} can only operate in terms of 277 filesystem devices, not SCSI devices. So, although the constructor 278 interface accepts a SCSI device parameter for the sake of compatibility, 279 it's not used. 280 281 Media Types 282 =========== 283 284 This class knows how to write to DVD+R and DVD+RW media, represented 285 by the following constants: 286 287 - C{MEDIA_DVDPLUSR}: DVD+R media (4.4 GB capacity) 288 - C{MEDIA_DVDPLUSRW}: DVD+RW media (4.4 GB capacity) 289 290 The difference is that DVD+RW media can be rewritten, while DVD+R media 291 cannot be (although at present, C{DvdWriter} does not really 292 differentiate between rewritable and non-rewritable media). 293 294 The capacities are 4.4 GB because Cedar Backup deals in "true" gigabytes 295 of 1024*1024*1024 bytes per gigabyte. 296 297 The underlying C{growisofs} utility does support other kinds of media 298 (including DVD-R, DVD-RW and BlueRay) which work somewhat differently 299 than standard DVD+R and DVD+RW media. I don't support these other kinds 300 of media because I haven't had any opportunity to work with them. The 301 same goes for dual-layer media of any type. 302 303 Device Attributes vs. Media Attributes 304 ====================================== 305 306 As with the cdwriter functionality, a given dvdwriter instance has two 307 different kinds of attributes associated with it. I call these device 308 attributes and media attributes. 309 310 Device attributes are things which can be determined without looking at 311 the media. Media attributes are attributes which vary depending on the 312 state of the media. In general, device attributes are available via 313 instance variables and are constant over the life of an object, while 314 media attributes can be retrieved through method calls. 315 316 Compared to cdwriters, dvdwriters have very few attributes. This is due 317 to differences between the way C{growisofs} works relative to 318 C{cdrecord}. 319 320 Media Capacity 321 ============== 322 323 One major difference between the C{cdrecord}/C{mkisofs} utilities used by 324 the cdwriter class and the C{growisofs} utility used here is that the 325 process of estimating remaining capacity and image size is more 326 straightforward with C{cdrecord}/C{mkisofs} than with C{growisofs}. 327 328 In this class, remaining capacity is calculated by asking doing a dry run 329 of C{growisofs} and grabbing some information from the output of that 330 command. Image size is estimated by asking the C{IsoImage} class for an 331 estimate and then adding on a "fudge factor" determined through 332 experimentation. 333 334 Testing 335 ======= 336 337 It's rather difficult to test this code in an automated fashion, even if 338 you have access to a physical DVD writer drive. It's even more difficult 339 to test it if you are running on some build daemon (think of a Debian 340 autobuilder) which can't be expected to have any hardware or any media 341 that you could write to. 342 343 Because of this, some of the implementation below is in terms of static 344 methods that are supposed to take defined actions based on their 345 arguments. Public methods are then implemented in terms of a series of 346 calls to simplistic static methods. This way, we can test as much as 347 possible of the "difficult" functionality via testing the static methods, 348 while hoping that if the static methods are called appropriately, things 349 will work properly. It's not perfect, but it's much better than no 350 testing at all. 351 352 @sort: __init__, isRewritable, retrieveCapacity, openTray, closeTray, refreshMedia, 353 initializeImage, addImageEntry, writeImage, setImageNewDisc, getEstimatedImageSize, 354 _writeImage, _getEstimatedImageSize, _searchForOverburn, _buildWriteArgs, 355 device, scsiId, hardwareId, driveSpeed, media, deviceHasTray, deviceCanEject 356 """ 357 358 ############## 359 # Constructor 360 ############## 361
362 - def __init__(self, device, scsiId=None, driveSpeed=None, 363 mediaType=MEDIA_DVDPLUSRW, noEject=False, 364 refreshMediaDelay=0, ejectDelay=0, unittest=False):
365 """ 366 Initializes a DVD writer object. 367 368 Since C{growisofs} can only address devices using the device path (i.e. 369 C{/dev/dvd}), the hardware id will always be set based on the device. If 370 passed in, it will be saved for reference purposes only. 371 372 We have no way to query the device to ask whether it has a tray or can be 373 safely opened and closed. So, the C{noEject} flag is used to set these 374 values. If C{noEject=False}, then we assume a tray exists and open/close 375 is safe. If C{noEject=True}, then we assume that there is no tray and 376 open/close is not safe. 377 378 @note: The C{unittest} parameter should never be set to C{True} 379 outside of Cedar Backup code. It is intended for use in unit testing 380 Cedar Backup internals and has no other sensible purpose. 381 382 @param device: Filesystem device associated with this writer. 383 @type device: Absolute path to a filesystem device, i.e. C{/dev/dvd} 384 385 @param scsiId: SCSI id for the device (optional, for reference only). 386 @type scsiId: If provided, SCSI id in the form C{[<method>:]scsibus,target,lun} 387 388 @param driveSpeed: Speed at which the drive writes. 389 @type driveSpeed: Use C{2} for 2x device, etc. or C{None} to use device default. 390 391 @param mediaType: Type of the media that is assumed to be in the drive. 392 @type mediaType: One of the valid media type as discussed above. 393 394 @param noEject: Tells Cedar Backup that the device cannot safely be ejected 395 @type noEject: Boolean true/false 396 397 @param refreshMediaDelay: Refresh media delay to use, if any 398 @type refreshMediaDelay: Number of seconds, an integer >= 0 399 400 @param ejectDelay: Eject delay to use, if any 401 @type ejectDelay: Number of seconds, an integer >= 0 402 403 @param unittest: Turns off certain validations, for use in unit testing. 404 @type unittest: Boolean true/false 405 406 @raise ValueError: If the device is not valid for some reason. 407 @raise ValueError: If the SCSI id is not in a valid form. 408 @raise ValueError: If the drive speed is not an integer >= 1. 409 """ 410 if scsiId is not None: 411 logger.warn("SCSI id [%s] will be ignored by DvdWriter." % scsiId) 412 self._image = None # optionally filled in by initializeImage() 413 self._device = validateDevice(device, unittest) 414 self._scsiId = scsiId # not validated, because it's just for reference 415 self._driveSpeed = validateDriveSpeed(driveSpeed) 416 self._media = MediaDefinition(mediaType) 417 self._refreshMediaDelay = refreshMediaDelay 418 self._ejectDelay = ejectDelay 419 if noEject: 420 self._deviceHasTray = False 421 self._deviceCanEject = False 422 else: 423 self._deviceHasTray = True # just assume 424 self._deviceCanEject = True # just assume
425 426 427 ############# 428 # Properties 429 ############# 430
431 - def _getDevice(self):
432 """ 433 Property target used to get the device value. 434 """ 435 return self._device
436
437 - def _getScsiId(self):
438 """ 439 Property target used to get the SCSI id value. 440 """ 441 return self._scsiId
442
443 - def _getHardwareId(self):
444 """ 445 Property target used to get the hardware id value. 446 """ 447 return self._device
448
449 - def _getDriveSpeed(self):
450 """ 451 Property target used to get the drive speed. 452 """ 453 return self._driveSpeed
454
455 - def _getMedia(self):
456 """ 457 Property target used to get the media description. 458 """ 459 return self._media
460
461 - def _getDeviceHasTray(self):
462 """ 463 Property target used to get the device-has-tray flag. 464 """ 465 return self._deviceHasTray
466
467 - def _getDeviceCanEject(self):
468 """ 469 Property target used to get the device-can-eject flag. 470 """ 471 return self._deviceCanEject
472
473 - def _getRefreshMediaDelay(self):
474 """ 475 Property target used to get the configured refresh media delay, in seconds. 476 """ 477 return self._refreshMediaDelay
478
479 - def _getEjectDelay(self):
480 """ 481 Property target used to get the configured eject delay, in seconds. 482 """ 483 return self._ejectDelay
484 485 device = property(_getDevice, None, None, doc="Filesystem device name for this writer.") 486 scsiId = property(_getScsiId, None, None, doc="SCSI id for the device (saved for reference only).") 487 hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer (always the device path).") 488 driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.") 489 media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.") 490 deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.") 491 deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.") 492 refreshMediaDelay = property(_getRefreshMediaDelay, None, None, doc="Refresh media delay, in seconds.") 493 ejectDelay = property(_getEjectDelay, None, None, doc="Eject delay, in seconds.") 494 495 496 ################################################# 497 # Methods related to device and media attributes 498 ################################################# 499
500 - def isRewritable(self):
501 """Indicates whether the media is rewritable per configuration.""" 502 return self._media.rewritable
503
504 - def retrieveCapacity(self, entireDisc=False):
505 """ 506 Retrieves capacity for the current media in terms of a C{MediaCapacity} 507 object. 508 509 If C{entireDisc} is passed in as C{True}, the capacity will be for the 510 entire disc, as if it were to be rewritten from scratch. The same will 511 happen if the disc can't be read for some reason. Otherwise, the capacity 512 will be calculated by subtracting the sectors currently used on the disc, 513 as reported by C{growisofs} itself. 514 515 @param entireDisc: Indicates whether to return capacity for entire disc. 516 @type entireDisc: Boolean true/false 517 518 @return: C{MediaCapacity} object describing the capacity of the media. 519 520 @raise ValueError: If there is a problem parsing the C{growisofs} output 521 @raise IOError: If the media could not be read for some reason. 522 """ 523 sectorsUsed = 0 524 if not entireDisc: 525 sectorsUsed = self._retrieveSectorsUsed() 526 sectorsAvailable = self._media.capacity - sectorsUsed # both are in sectors 527 bytesUsed = convertSize(sectorsUsed, UNIT_SECTORS, UNIT_BYTES) 528 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 529 return MediaCapacity(bytesUsed, bytesAvailable)
530 531 532 ####################################################### 533 # Methods used for working with the internal ISO image 534 ####################################################### 535
536 - def initializeImage(self, newDisc, tmpdir, mediaLabel=None):
537 """ 538 Initializes the writer's associated ISO image. 539 540 This method initializes the C{image} instance variable so that the caller 541 can use the C{addImageEntry} method. Once entries have been added, the 542 C{writeImage} method can be called with no arguments. 543 544 @param newDisc: Indicates whether the disc should be re-initialized 545 @type newDisc: Boolean true/false 546 547 @param tmpdir: Temporary directory to use if needed 548 @type tmpdir: String representing a directory path on disk 549 550 @param mediaLabel: Media label to be applied to the image, if any 551 @type mediaLabel: String, no more than 25 characters long 552 """ 553 self._image = _ImageProperties() 554 self._image.newDisc = newDisc 555 self._image.tmpdir = encodePath(tmpdir) 556 self._image.mediaLabel = mediaLabel 557 self._image.entries = {} # mapping from path to graft point (if any)
558
559 - def addImageEntry(self, path, graftPoint):
560 """ 561 Adds a filepath entry to the writer's associated ISO image. 562 563 The contents of the filepath -- but not the path itself -- will be added 564 to the image at the indicated graft point. If you don't want to use a 565 graft point, just pass C{None}. 566 567 @note: Before calling this method, you must call L{initializeImage}. 568 569 @param path: File or directory to be added to the image 570 @type path: String representing a path on disk 571 572 @param graftPoint: Graft point to be used when adding this entry 573 @type graftPoint: String representing a graft point path, as described above 574 575 @raise ValueError: If initializeImage() was not previously called 576 @raise ValueError: If the path is not a valid file or directory 577 """ 578 if self._image is None: 579 raise ValueError("Must call initializeImage() before using this method.") 580 if not os.path.exists(path): 581 raise ValueError("Path [%s] does not exist." % path) 582 self._image.entries[path] = graftPoint
583
584 - def setImageNewDisc(self, newDisc):
585 """ 586 Resets (overrides) the newDisc flag on the internal image. 587 @param newDisc: New disc flag to set 588 @raise ValueError: If initializeImage() was not previously called 589 """ 590 if self._image is None: 591 raise ValueError("Must call initializeImage() before using this method.") 592 self._image.newDisc = newDisc
593
594 - def getEstimatedImageSize(self):
595 """ 596 Gets the estimated size of the image associated with the writer. 597 598 This is an estimate and is conservative. The actual image could be as 599 much as 450 blocks (sectors) smaller under some circmstances. 600 601 @return: Estimated size of the image, in bytes. 602 603 @raise IOError: If there is a problem calling C{mkisofs}. 604 @raise ValueError: If initializeImage() was not previously called 605 """ 606 if self._image is None: 607 raise ValueError("Must call initializeImage() before using this method.") 608 return DvdWriter._getEstimatedImageSize(self._image.entries)
609 610 611 ###################################### 612 # Methods which expose device actions 613 ###################################### 614
615 - def openTray(self):
616 """ 617 Opens the device's tray and leaves it open. 618 619 This only works if the device has a tray and supports ejecting its media. 620 We have no way to know if the tray is currently open or closed, so we 621 just send the appropriate command and hope for the best. If the device 622 does not have a tray or does not support ejecting its media, then we do 623 nothing. 624 625 Starting with Debian wheezy on my backup hardware, I started seeing 626 consistent problems with the eject command. I couldn't tell whether 627 these problems were due to the device management system or to the new 628 kernel (3.2.0). Initially, I saw simple eject failures, possibly because 629 I was opening and closing the tray too quickly. I worked around that 630 behavior with the new ejectDelay flag. 631 632 Later, I sometimes ran into issues after writing an image to a disc: 633 eject would give errors like "unable to eject, last error: Inappropriate 634 ioctl for device". Various sources online (like Ubuntu bug #875543) 635 suggested that the drive was being locked somehow, and that the 636 workaround was to run 'eject -i off' to unlock it. Sure enough, that 637 fixed the problem for me, so now it's a normal error-handling strategy. 638 639 @raise IOError: If there is an error talking to the device. 640 """ 641 if self._deviceHasTray and self._deviceCanEject: 642 command = resolveCommand(EJECT_COMMAND) 643 args = [ self.device, ] 644 result = executeCommand(command, args)[0] 645 if result != 0: 646 logger.debug("Eject failed; attempting kludge of unlocking the tray before retrying.") 647 self.unlockTray() 648 result = executeCommand(command, args)[0] 649 if result != 0: 650 raise IOError("Error (%d) executing eject command to open tray (failed even after unlocking tray)." % result) 651 logger.debug("Kludge was apparently successful.") 652 if self.ejectDelay is not None: 653 logger.debug("Per configuration, sleeping %d seconds after opening tray." % self.ejectDelay) 654 time.sleep(self.ejectDelay)
655
656 - def unlockTray(self):
657 """ 658 Unlocks the device's tray via 'eject -i off'. 659 @raise IOError: If there is an error talking to the device. 660 """ 661 command = resolveCommand(EJECT_COMMAND) 662 args = [ "-i", "off", self.device, ] 663 result = executeCommand(command, args)[0] 664 if result != 0: 665 raise IOError("Error (%d) executing eject command to unlock tray." % result)
666
667 - def closeTray(self):
668 """ 669 Closes the device's tray. 670 671 This only works if the device has a tray and supports ejecting its media. 672 We have no way to know if the tray is currently open or closed, so we 673 just send the appropriate command and hope for the best. If the device 674 does not have a tray or does not support ejecting its media, then we do 675 nothing. 676 677 @raise IOError: If there is an error talking to the device. 678 """ 679 if self._deviceHasTray and self._deviceCanEject: 680 command = resolveCommand(EJECT_COMMAND) 681 args = [ "-t", self.device, ] 682 result = executeCommand(command, args)[0] 683 if result != 0: 684 raise IOError("Error (%d) executing eject command to close tray." % result)
685
686 - def refreshMedia(self):
687 """ 688 Opens and then immediately closes the device's tray, to refresh the 689 device's idea of the media. 690 691 Sometimes, a device gets confused about the state of its media. Often, 692 all it takes to solve the problem is to eject the media and then 693 immediately reload it. (There are also configurable eject and refresh 694 media delays which can be applied, for situations where this makes a 695 difference.) 696 697 This only works if the device has a tray and supports ejecting its media. 698 We have no way to know if the tray is currently open or closed, so we 699 just send the appropriate command and hope for the best. If the device 700 does not have a tray or does not support ejecting its media, then we do 701 nothing. The configured delays still apply, though. 702 703 @raise IOError: If there is an error talking to the device. 704 """ 705 self.openTray() 706 self.closeTray() 707 self.unlockTray() # on some systems, writing a disc leaves the tray locked, yikes! 708 if self.refreshMediaDelay is not None: 709 logger.debug("Per configuration, sleeping %d seconds to stabilize media state." % self.refreshMediaDelay) 710 time.sleep(self.refreshMediaDelay) 711 logger.debug("Media refresh complete; hopefully media state is stable now.")
712
713 - def writeImage(self, imagePath=None, newDisc=False, writeMulti=True):
714 """ 715 Writes an ISO image to the media in the device. 716 717 If C{newDisc} is passed in as C{True}, we assume that the entire disc 718 will be re-created from scratch. Note that unlike C{CdWriter}, 719 C{DvdWriter} does not blank rewritable media before reusing it; however, 720 C{growisofs} is called such that the media will be re-initialized as 721 needed. 722 723 If C{imagePath} is passed in as C{None}, then the existing image 724 configured with C{initializeImage()} will be used. Under these 725 circumstances, the passed-in C{newDisc} flag will be ignored and the 726 value passed in to C{initializeImage()} will apply instead. 727 728 The C{writeMulti} argument is ignored. It exists for compatibility with 729 the Cedar Backup image writer interface. 730 731 @note: The image size indicated in the log ("Image size will be...") is 732 an estimate. The estimate is conservative and is probably larger than 733 the actual space that C{dvdwriter} will use. 734 735 @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image 736 @type imagePath: String representing a path on disk 737 738 @param newDisc: Indicates whether the disc should be re-initialized 739 @type newDisc: Boolean true/false. 740 741 @param writeMulti: Unused 742 @type writeMulti: Boolean true/false 743 744 @raise ValueError: If the image path is not absolute. 745 @raise ValueError: If some path cannot be encoded properly. 746 @raise IOError: If the media could not be written to for some reason. 747 @raise ValueError: If no image is passed in and initializeImage() was not previously called 748 """ 749 if not writeMulti: 750 logger.warn("writeMulti value of [%s] ignored." % writeMulti) 751 if imagePath is None: 752 if self._image is None: 753 raise ValueError("Must call initializeImage() before using this method with no image path.") 754 size = self.getEstimatedImageSize() 755 logger.info("Image size will be %s (estimated)." % displayBytes(size)) 756 available = self.retrieveCapacity(entireDisc=self._image.newDisc).bytesAvailable 757 if size > available: 758 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available))) 759 raise IOError("Media does not contain enough capacity to store image.") 760 self._writeImage(self._image.newDisc, None, self._image.entries, self._image.mediaLabel) 761 else: 762 if not os.path.isabs(imagePath): 763 raise ValueError("Image path must be absolute.") 764 imagePath = encodePath(imagePath) 765 self._writeImage(newDisc, imagePath, None)
766 767 768 ################################################################## 769 # Utility methods for dealing with growisofs and dvd+rw-mediainfo 770 ################################################################## 771
772 - def _writeImage(self, newDisc, imagePath, entries, mediaLabel=None):
773 """ 774 Writes an image to disc using either an entries list or an ISO image on 775 disk. 776 777 Callers are assumed to have done validation on paths, etc. before calling 778 this method. 779 780 @param newDisc: Indicates whether the disc should be re-initialized 781 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries} 782 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath} 783 784 @raise IOError: If the media could not be written to for some reason. 785 """ 786 command = resolveCommand(GROWISOFS_COMMAND) 787 args = DvdWriter._buildWriteArgs(newDisc, self.hardwareId, self._driveSpeed, imagePath, entries, mediaLabel, dryRun=False) 788 (result, output) = executeCommand(command, args, returnOutput=True) 789 if result != 0: 790 DvdWriter._searchForOverburn(output) # throws own exception if overburn condition is found 791 raise IOError("Error (%d) executing command to write disc." % result) 792 self.refreshMedia()
793 794 @staticmethod
795 - def _getEstimatedImageSize(entries):
796 """ 797 Gets the estimated size of a set of image entries. 798 799 This is implemented in terms of the C{IsoImage} class. The returned 800 value is calculated by adding a "fudge factor" to the value from 801 C{IsoImage}. This fudge factor was determined by experimentation and is 802 conservative -- the actual image could be as much as 450 blocks smaller 803 under some circumstances. 804 805 @param entries: Dictionary mapping path to graft point. 806 807 @return: Total estimated size of image, in bytes. 808 809 @raise ValueError: If there are no entries in the dictionary 810 @raise ValueError: If any path in the dictionary does not exist 811 @raise IOError: If there is a problem calling C{mkisofs}. 812 """ 813 fudgeFactor = convertSize(2500.0, UNIT_SECTORS, UNIT_BYTES) # determined through experimentation 814 if len(entries.keys()) == 0: 815 raise ValueError("Must add at least one entry with addImageEntry().") 816 image = IsoImage() 817 for path in entries.keys(): 818 image.addEntry(path, entries[path], override=False, contentsOnly=True) 819 estimatedSize = image.getEstimatedSize() + fudgeFactor 820 return estimatedSize
821
822 - def _retrieveSectorsUsed(self):
823 """ 824 Retrieves the number of sectors used on the current media. 825 826 This is a little ugly. We need to call growisofs in "dry-run" mode and 827 parse some information from its output. However, to do that, we need to 828 create a dummy file that we can pass to the command -- and we have to 829 make sure to remove it later. 830 831 Once growisofs has been run, then we call C{_parseSectorsUsed} to parse 832 the output and calculate the number of sectors used on the media. 833 834 @return: Number of sectors used on the media 835 """ 836 tempdir = tempfile.mkdtemp() 837 try: 838 entries = { tempdir: None } 839 args = DvdWriter._buildWriteArgs(False, self.hardwareId, self.driveSpeed, None, entries, None, dryRun=True) 840 command = resolveCommand(GROWISOFS_COMMAND) 841 (result, output) = executeCommand(command, args, returnOutput=True) 842 if result != 0: 843 logger.debug("Error (%d) calling growisofs to read sectors used." % result) 844 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.") 845 return 0.0 846 sectorsUsed = DvdWriter._parseSectorsUsed(output) 847 logger.debug("Determined sectors used as %s" % sectorsUsed) 848 return sectorsUsed 849 finally: 850 if os.path.exists(tempdir): 851 try: 852 os.rmdir(tempdir) 853 except: pass
854 855 @staticmethod
856 - def _parseSectorsUsed(output):
857 """ 858 Parse sectors used information out of C{growisofs} output. 859 860 The first line of a growisofs run looks something like this:: 861 862 Executing 'mkisofs -C 973744,1401056 -M /dev/fd/3 -r -graft-points music4/=music | builtin_dd of=/dev/cdrom obs=32k seek=87566' 863 864 Dmitry has determined that the seek value in this line gives us 865 information about how much data has previously been written to the media. 866 That value multiplied by 16 yields the number of sectors used. 867 868 If the seek line cannot be found in the output, then sectors used of zero 869 is assumed. 870 871 @return: Sectors used on the media, as a floating point number. 872 873 @raise ValueError: If the output cannot be parsed properly. 874 """ 875 if output is not None: 876 pattern = re.compile(r"(^)(.*)(seek=)(.*)('$)") 877 for line in output: 878 match = pattern.search(line) 879 if match is not None: 880 try: 881 return float(match.group(4).strip()) * 16.0 882 except ValueError: 883 raise ValueError("Unable to parse sectors used out of growisofs output.") 884 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.") 885 return 0.0
886 887 @staticmethod
888 - def _searchForOverburn(output):
889 """ 890 Search for an "overburn" error message in C{growisofs} output. 891 892 The C{growisofs} command returns a non-zero exit code and puts a message 893 into the output -- even on a dry run -- if there is not enough space on 894 the media. This is called an "overburn" condition. 895 896 The error message looks like this:: 897 898 :-( /dev/cdrom: 894048 blocks are free, 2033746 to be written! 899 900 This method looks for the overburn error message anywhere in the output. 901 If a matching error message is found, an C{IOError} exception is raised 902 containing relevant information about the problem. Otherwise, the method 903 call returns normally. 904 905 @param output: List of output lines to search, as from C{executeCommand} 906 907 @raise IOError: If an overburn condition is found. 908 """ 909 if output is None: 910 return 911 pattern = re.compile(r"(^)(:-[(])(\s*.*:\s*)(.* )(blocks are free, )(.* )(to be written!)") 912 for line in output: 913 match = pattern.search(line) 914 if match is not None: 915 try: 916 available = convertSize(float(match.group(4).strip()), UNIT_SECTORS, UNIT_BYTES) 917 size = convertSize(float(match.group(6).strip()), UNIT_SECTORS, UNIT_BYTES) 918 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available))) 919 except ValueError: 920 logger.error("Image does not fit in available capacity (no useful capacity info available).") 921 raise IOError("Media does not contain enough capacity to store image.")
922 923 @staticmethod
924 - def _buildWriteArgs(newDisc, hardwareId, driveSpeed, imagePath, entries, mediaLabel=None, dryRun=False):
925 """ 926 Builds a list of arguments to be passed to a C{growisofs} command. 927 928 The arguments will either cause C{growisofs} to write the indicated image 929 file to disc, or will pass C{growisofs} a list of directories or files 930 that should be written to disc. 931 932 If a new image is created, it will always be created with Rock Ridge 933 extensions (-r). A volume name will be applied (-V) if C{mediaLabel} is 934 not C{None}. 935 936 @param newDisc: Indicates whether the disc should be re-initialized 937 @param hardwareId: Hardware id for the device 938 @param driveSpeed: Speed at which the drive writes. 939 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries} 940 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath} 941 @param mediaLabel: Media label to set on the image, if any 942 @param dryRun: Says whether to make this a dry run (for checking capacity) 943 944 @note: If we write an existing image to disc, then the mediaLabel is 945 ignored. The media label is an attribute of the image, and should be set 946 on the image when it is created. 947 948 @note: We always pass the undocumented option C{-use-the-force-like=tty} 949 to growisofs. Without this option, growisofs will refuse to execute 950 certain actions when running from cron. A good example is -Z, which 951 happily overwrites an existing DVD from the command-line, but fails when 952 run from cron. It took a while to figure that out, since it worked every 953 time I tested it by hand. :( 954 955 @return: List suitable for passing to L{util.executeCommand} as C{args}. 956 957 @raise ValueError: If caller does not pass one or the other of imagePath or entries. 958 """ 959 args = [] 960 if (imagePath is None and entries is None) or (imagePath is not None and entries is not None): 961 raise ValueError("Must use either imagePath or entries.") 962 args.append("-use-the-force-luke=tty") # tell growisofs to let us run from cron 963 if dryRun: 964 args.append("-dry-run") 965 if driveSpeed is not None: 966 args.append("-speed=%d" % driveSpeed) 967 if newDisc: 968 args.append("-Z") 969 else: 970 args.append("-M") 971 if imagePath is not None: 972 args.append("%s=%s" % (hardwareId, imagePath)) 973 else: 974 args.append(hardwareId) 975 if mediaLabel is not None: 976 args.append("-V") 977 args.append(mediaLabel) 978 args.append("-r") # Rock Ridge extensions with sane ownership and permissions 979 args.append("-graft-points") 980 keys = entries.keys() 981 keys.sort() # just so we get consistent results 982 for key in keys: 983 # Same syntax as when calling mkisofs in IsoImage 984 if entries[key] is None: 985 args.append(key) 986 else: 987 args.append("%s/=%s" % (entries[key].strip("/"), key)) 988 return args
989