1   
   2   
   3   
   4   
   5   
   6   
   7   
   8   
   9   
  10   
  11   
  12   
  13   
  14   
  15   
  16   
  17   
  18   
  19   
  20   
  21   
  22   
  23   
  24   
  25   
  26   
  27   
  28   
  29   
  30   
  31   
  32   
  33   
  34   
  35   
  36   
  37   
  38  """ 
  39  Provides functionality related to CD writer devices. 
  40   
  41  @sort: MediaDefinition, MediaCapacity, CdWriter, 
  42         MEDIA_CDRW_74, MEDIA_CDR_74, MEDIA_CDRW_80, MEDIA_CDR_80 
  43   
  44  @var MEDIA_CDRW_74: Constant representing 74-minute CD-RW media. 
  45  @var MEDIA_CDR_74: Constant representing 74-minute CD-R media. 
  46  @var MEDIA_CDRW_80: Constant representing 80-minute CD-RW media. 
  47  @var MEDIA_CDR_80: Constant representing 80-minute CD-R media. 
  48   
  49  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
  50  """ 
  51   
  52   
  53   
  54   
  55   
  56   
  57  import os 
  58  import re 
  59  import logging 
  60  import tempfile 
  61  import time 
  62   
  63   
  64  from CedarBackup2.util import resolveCommand, executeCommand 
  65  from CedarBackup2.util import convertSize, displayBytes, encodePath 
  66  from CedarBackup2.util import UNIT_SECTORS, UNIT_BYTES, UNIT_KBYTES, UNIT_MBYTES 
  67  from CedarBackup2.writers.util import validateDevice, validateScsiId, validateDriveSpeed 
  68  from CedarBackup2.writers.util import IsoImage 
  69   
  70   
  71   
  72   
  73   
  74   
  75  logger = logging.getLogger("CedarBackup2.log.writers.cdwriter") 
  76   
  77  MEDIA_CDRW_74  = 1 
  78  MEDIA_CDR_74   = 2 
  79  MEDIA_CDRW_80  = 3 
  80  MEDIA_CDR_80   = 4 
  81   
  82  CDRECORD_COMMAND = [ "cdrecord", ] 
  83  EJECT_COMMAND    = [ "eject", ] 
  84  MKISOFS_COMMAND  = [ "mkisofs", ] 
 181   
 262   
 269     """ 
 270     Simple value object to hold image properties for C{DvdWriter}. 
 271     """ 
 273        self.newDisc = False 
 274        self.tmpdir = None 
 275        self.mediaLabel = None 
 276        self.entries = None      
   277   
 278   
 279   
 280   
 281   
 282   
 283 -class CdWriter(object): 
  284   
 285      
 286      
 287      
 288   
 289     """ 
 290     Class representing a device that knows how to write CD media. 
 291   
 292     Summary 
 293     ======= 
 294   
 295        This is a class representing a device that knows how to write CD media.  It 
 296        provides common operations for the device, such as ejecting the media, 
 297        writing an ISO image to the media, or checking for the current media 
 298        capacity.  It also provides a place to store device attributes, such as 
 299        whether the device supports writing multisession discs, etc. 
 300   
 301        This class is implemented in terms of the C{eject} and C{cdrecord} 
 302        programs, both of which should be available on most UN*X platforms. 
 303   
 304     Image Writer Interface 
 305     ====================== 
 306   
 307        The following methods make up the "image writer" interface shared 
 308        with other kinds of writers (such as DVD writers):: 
 309   
 310           __init__ 
 311           initializeImage() 
 312           addImageEntry() 
 313           writeImage() 
 314           setImageNewDisc() 
 315           retrieveCapacity() 
 316           getEstimatedImageSize() 
 317   
 318        Only these methods will be used by other Cedar Backup functionality 
 319        that expects a compatible image writer. 
 320   
 321        The media attribute is also assumed to be available. 
 322   
 323     Media Types 
 324     =========== 
 325   
 326        This class knows how to write to two different kinds of media, represented 
 327        by the following constants: 
 328   
 329           - C{MEDIA_CDR_74}: 74-minute CD-R media (650 MB capacity) 
 330           - C{MEDIA_CDRW_74}: 74-minute CD-RW media (650 MB capacity) 
 331           - C{MEDIA_CDR_80}: 80-minute CD-R media (700 MB capacity) 
 332           - C{MEDIA_CDRW_80}: 80-minute CD-RW media (700 MB capacity) 
 333   
 334        Most hardware can read and write both 74-minute and 80-minute CD-R and 
 335        CD-RW media.  Some older drives may only be able to write CD-R media. 
 336        The difference between the two is that CD-RW media can be rewritten 
 337        (erased), while CD-R media cannot be. 
 338   
 339        I do not support any other configurations for a couple of reasons.  The 
 340        first is that I've never tested any other kind of media.  The second is 
 341        that anything other than 74 or 80 minute is apparently non-standard. 
 342   
 343     Device Attributes vs. Media Attributes 
 344     ====================================== 
 345   
 346        A given writer instance has two different kinds of attributes associated 
 347        with it, which I call device attributes and media attributes.  Device 
 348        attributes are things which can be determined without looking at the 
 349        media, such as whether the drive supports writing multisession disks or 
 350        has a tray.  Media attributes are attributes which vary depending on the 
 351        state of the media, such as the remaining capacity on a disc.  In 
 352        general, device attributes are available via instance variables and are 
 353        constant over the life of an object, while media attributes can be 
 354        retrieved through method calls. 
 355   
 356     Talking to Hardware 
 357     =================== 
 358   
 359        This class needs to talk to CD writer hardware in two different ways: 
 360        through cdrecord to actually write to the media, and through the 
 361        filesystem to do things like open and close the tray. 
 362   
 363        Historically, CdWriter has interacted with cdrecord using the scsiId 
 364        attribute, and with most other utilities using the device attribute. 
 365        This changed somewhat in Cedar Backup 2.9.0. 
 366   
 367        When Cedar Backup was first written, the only way to interact with 
 368        cdrecord was by using a SCSI device id.  IDE devices were mapped to 
 369        pseudo-SCSI devices through the kernel.  Later, extended SCSI "methods" 
 370        arrived, and it became common to see C{ATA:1,0,0} or C{ATAPI:0,0,0} as a 
 371        way to address IDE hardware.  By late 2006, C{ATA} and C{ATAPI} had 
 372        apparently been deprecated in favor of just addressing the IDE device 
 373        directly by name, i.e. C{/dev/cdrw}. 
 374   
 375        Because of this latest development, it no longer makes sense to require a 
 376        CdWriter to be created with a SCSI id -- there might not be one.  So, the 
 377        passed-in SCSI id is now optional.  Also, there is now a hardwareId 
 378        attribute.  This attribute is filled in with either the SCSI id (if 
 379        provided) or the device (otherwise).  The hardware id is the value that 
 380        will be passed to cdrecord in the C{dev=} argument. 
 381   
 382     Testing 
 383     ======= 
 384   
 385        It's rather difficult to test this code in an automated fashion, even if 
 386        you have access to a physical CD writer drive.  It's even more difficult 
 387        to test it if you are running on some build daemon (think of a Debian 
 388        autobuilder) which can't be expected to have any hardware or any media 
 389        that you could write to. 
 390   
 391        Because of this, much of the implementation below is in terms of static 
 392        methods that are supposed to take defined actions based on their 
 393        arguments.  Public methods are then implemented in terms of a series of 
 394        calls to simplistic static methods.  This way, we can test as much as 
 395        possible of the functionality via testing the static methods, while 
 396        hoping that if the static methods are called appropriately, things will 
 397        work properly.  It's not perfect, but it's much better than no testing at 
 398        all. 
 399   
 400     @sort: __init__, isRewritable, _retrieveProperties, retrieveCapacity, _getBoundaries, 
 401            _calculateCapacity, openTray, closeTray, refreshMedia, writeImage, 
 402            _blankMedia, _parsePropertiesOutput, _parseBoundariesOutput, 
 403            _buildOpenTrayArgs, _buildCloseTrayArgs, _buildPropertiesArgs, 
 404            _buildBoundariesArgs, _buildBlankArgs, _buildWriteArgs, 
 405            device, scsiId, hardwareId, driveSpeed, media, deviceType, deviceVendor, 
 406            deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, deviceCanEject, 
 407            initializeImage, addImageEntry, writeImage, setImageNewDisc, getEstimatedImageSize 
 408     """ 
 409   
 410      
 411      
 412      
 413   
 414 -   def __init__(self, device, scsiId=None, driveSpeed=None, 
 415                  mediaType=MEDIA_CDRW_74, noEject=False, 
 416                  refreshMediaDelay=0, ejectDelay=0, unittest=False): 
  417        """ 
 418        Initializes a CD writer object. 
 419   
 420        The current user must have write access to the device at the time the 
 421        object is instantiated, or an exception will be thrown.  However, no 
 422        media-related validation is done, and in fact there is no need for any 
 423        media to be in the drive until one of the other media attribute-related 
 424        methods is called. 
 425   
 426        The various instance variables such as C{deviceType}, C{deviceVendor}, 
 427        etc. might be C{None}, if we're unable to parse this specific information 
 428        from the C{cdrecord} output.  This information is just for reference. 
 429   
 430        The SCSI id is optional, but the device path is required.  If the SCSI id 
 431        is passed in, then the hardware id attribute will be taken from the SCSI 
 432        id.  Otherwise, the hardware id will be taken from the device. 
 433   
 434        If cdrecord improperly detects whether your writer device has a tray and 
 435        can be safely opened and closed, then pass in C{noEject=False}.  This 
 436        will override the properties and the device will never be ejected. 
 437   
 438        @note: The C{unittest} parameter should never be set to C{True} 
 439        outside of Cedar Backup code.  It is intended for use in unit testing 
 440        Cedar Backup internals and has no other sensible purpose. 
 441   
 442        @param device: Filesystem device associated with this writer. 
 443        @type device: Absolute path to a filesystem device, i.e. C{/dev/cdrw} 
 444   
 445        @param scsiId: SCSI id for the device (optional). 
 446        @type scsiId: If provided, SCSI id in the form C{[<method>:]scsibus,target,lun} 
 447   
 448        @param driveSpeed: Speed at which the drive writes. 
 449        @type driveSpeed: Use C{2} for 2x device, etc. or C{None} to use device default. 
 450   
 451        @param mediaType: Type of the media that is assumed to be in the drive. 
 452        @type mediaType: One of the valid media type as discussed above. 
 453   
 454        @param noEject: Overrides properties to indicate that the device does not support eject. 
 455        @type noEject: Boolean true/false 
 456   
 457        @param refreshMediaDelay: Refresh media delay to use, if any 
 458        @type refreshMediaDelay: Number of seconds, an integer >= 0 
 459   
 460        @param ejectDelay: Eject delay to use, if any 
 461        @type ejectDelay: Number of seconds, an integer >= 0 
 462   
 463        @param unittest: Turns off certain validations, for use in unit testing. 
 464        @type unittest: Boolean true/false 
 465   
 466        @raise ValueError: If the device is not valid for some reason. 
 467        @raise ValueError: If the SCSI id is not in a valid form. 
 468        @raise ValueError: If the drive speed is not an integer >= 1. 
 469        @raise IOError: If device properties could not be read for some reason. 
 470        """ 
 471        self._image = None   
 472        self._device = validateDevice(device, unittest) 
 473        self._scsiId = validateScsiId(scsiId) 
 474        self._driveSpeed = validateDriveSpeed(driveSpeed) 
 475        self._media = MediaDefinition(mediaType) 
 476        self._noEject = noEject 
 477        self._refreshMediaDelay = refreshMediaDelay 
 478        self._ejectDelay = ejectDelay 
 479        if not unittest: 
 480           (self._deviceType, 
 481            self._deviceVendor, 
 482            self._deviceId, 
 483            self._deviceBufferSize, 
 484            self._deviceSupportsMulti, 
 485            self._deviceHasTray, 
 486            self._deviceCanEject) = self._retrieveProperties() 
  487   
 488   
 489      
 490      
 491      
 492   
 494        """ 
 495        Property target used to get the device value. 
 496        """ 
 497        return self._device 
  498   
 500        """ 
 501        Property target used to get the SCSI id value. 
 502        """ 
 503        return self._scsiId 
  504   
 506        """ 
 507        Property target used to get the hardware id value. 
 508        """ 
 509        if self._scsiId is None: 
 510           return self._device 
 511        return self._scsiId 
  512   
 514        """ 
 515        Property target used to get the drive speed. 
 516        """ 
 517        return self._driveSpeed 
  518   
 524   
 526        """ 
 527        Property target used to get the device type. 
 528        """ 
 529        return self._deviceType 
  530   
 532        """ 
 533        Property target used to get the device vendor. 
 534        """ 
 535        return self._deviceVendor 
  536   
 538        """ 
 539        Property target used to get the device id. 
 540        """ 
 541        return self._deviceId 
  542   
 544        """ 
 545        Property target used to get the device buffer size. 
 546        """ 
 547        return self._deviceBufferSize 
  548   
 550        """ 
 551        Property target used to get the device-support-multi flag. 
 552        """ 
 553        return self._deviceSupportsMulti 
  554   
 556        """ 
 557        Property target used to get the device-has-tray flag. 
 558        """ 
 559        return self._deviceHasTray 
  560   
 562        """ 
 563        Property target used to get the device-can-eject flag. 
 564        """ 
 565        return self._deviceCanEject 
  566   
 572   
 574        """ 
 575        Property target used to get the configured eject delay, in seconds. 
 576        """ 
 577        return self._ejectDelay 
  578   
 579     device = property(_getDevice, None, None, doc="Filesystem device name for this writer.") 
 580     scsiId = property(_getScsiId, None, None, doc="SCSI id for the device, in the form C{[<method>:]scsibus,target,lun}.") 
 581     hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer, either SCSI id or device path.") 
 582     driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.") 
 583     media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.") 
 584     deviceType = property(_getDeviceType, None, None, doc="Type of the device, as returned from C{cdrecord -prcap}.") 
 585     deviceVendor = property(_getDeviceVendor, None, None, doc="Vendor of the device, as returned from C{cdrecord -prcap}.") 
 586     deviceId = property(_getDeviceId, None, None, doc="Device identification, as returned from C{cdrecord -prcap}.") 
 587     deviceBufferSize = property(_getDeviceBufferSize, None, None, doc="Size of the device's write buffer, in bytes.") 
 588     deviceSupportsMulti = property(_getDeviceSupportsMulti, None, None, doc="Indicates whether device supports multisession discs.") 
 589     deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.") 
 590     deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.") 
 591     refreshMediaDelay = property(_getRefreshMediaDelay, None, None, doc="Refresh media delay, in seconds.") 
 592     ejectDelay = property(_getEjectDelay, None, None, doc="Eject delay, in seconds.") 
 593   
 594   
 595      
 596      
 597      
 598   
 600        """Indicates whether the media is rewritable per configuration.""" 
 601        return self._media.rewritable 
  602   
 604        """ 
 605        Retrieves properties for a device from C{cdrecord}. 
 606   
 607        The results are returned as a tuple of the object device attributes as 
 608        returned from L{_parsePropertiesOutput}: C{(deviceType, deviceVendor, 
 609        deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, 
 610        deviceCanEject)}. 
 611   
 612        @return: Results tuple as described above. 
 613        @raise IOError: If there is a problem talking to the device. 
 614        """ 
 615        args = CdWriter._buildPropertiesArgs(self.hardwareId) 
 616        command = resolveCommand(CDRECORD_COMMAND) 
 617        (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True) 
 618        if result != 0: 
 619           raise IOError("Error (%d) executing cdrecord command to get properties." % result) 
 620        return CdWriter._parsePropertiesOutput(output) 
  621   
 623        """ 
 624        Retrieves capacity for the current media in terms of a C{MediaCapacity} 
 625        object. 
 626   
 627        If C{entireDisc} is passed in as C{True} the capacity will be for the 
 628        entire disc, as if it were to be rewritten from scratch.  If the drive 
 629        does not support writing multisession discs or if C{useMulti} is passed 
 630        in as C{False}, the capacity will also be as if the disc were to be 
 631        rewritten from scratch, but the indicated boundaries value will be 
 632        C{None}.  The same will happen if the disc cannot be read for some 
 633        reason.  Otherwise, the capacity (including the boundaries) will 
 634        represent whatever space remains on the disc to be filled by future 
 635        sessions. 
 636   
 637        @param entireDisc: Indicates whether to return capacity for entire disc. 
 638        @type entireDisc: Boolean true/false 
 639   
 640        @param useMulti: Indicates whether a multisession disc should be assumed, if possible. 
 641        @type useMulti: Boolean true/false 
 642   
 643        @return: C{MediaCapacity} object describing the capacity of the media. 
 644        @raise IOError: If the media could not be read for some reason. 
 645        """ 
 646        boundaries = self._getBoundaries(entireDisc, useMulti) 
 647        return CdWriter._calculateCapacity(self._media, boundaries) 
  648   
 650        """ 
 651        Gets the ISO boundaries for the media. 
 652   
 653        If C{entireDisc} is passed in as C{True} the boundaries will be C{None}, 
 654        as if the disc were to be rewritten from scratch.  If the drive does not 
 655        support writing multisession discs, the returned value will be C{None}. 
 656        The same will happen if the disc can't be read for some reason. 
 657        Otherwise, the returned value will be represent the boundaries of the 
 658        disc's current contents. 
 659   
 660        The results are returned as a tuple of (lower, upper) as needed by the 
 661        C{IsoImage} class.  Note that these values are in terms of ISO sectors, 
 662        not bytes.  Clients should generally consider the boundaries value 
 663        opaque, however. 
 664   
 665        @param entireDisc: Indicates whether to return capacity for entire disc. 
 666        @type entireDisc: Boolean true/false 
 667   
 668        @param useMulti: Indicates whether a multisession disc should be assumed, if possible. 
 669        @type useMulti: Boolean true/false 
 670   
 671        @return: Boundaries tuple or C{None}, as described above. 
 672        @raise IOError: If the media could not be read for some reason. 
 673        """ 
 674        if not self._deviceSupportsMulti: 
 675           logger.debug("Device does not support multisession discs; returning boundaries None.") 
 676           return None 
 677        elif not useMulti: 
 678           logger.debug("Use multisession flag is False; returning boundaries None.") 
 679           return None 
 680        elif entireDisc: 
 681           logger.debug("Entire disc flag is True; returning boundaries None.") 
 682           return None 
 683        else: 
 684           args = CdWriter._buildBoundariesArgs(self.hardwareId) 
 685           command = resolveCommand(CDRECORD_COMMAND) 
 686           (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True) 
 687           if result != 0: 
 688              logger.debug("Error (%d) executing cdrecord command to get capacity.", result) 
 689              logger.warn("Unable to read disc (might not be initialized); returning boundaries of None.") 
 690              return None 
 691           boundaries = CdWriter._parseBoundariesOutput(output) 
 692           if boundaries is None: 
 693              logger.debug("Returning disc boundaries: None") 
 694           else: 
 695              logger.debug("Returning disc boundaries: (%d, %d)", boundaries[0], boundaries[1]) 
 696           return boundaries 
  697   
 698     @staticmethod 
 700        """ 
 701        Calculates capacity for the media in terms of boundaries. 
 702   
 703        If C{boundaries} is C{None} or the lower bound is 0 (zero), then the 
 704        capacity will be for the entire disc minus the initial lead in. 
 705        Otherwise, capacity will be as if the caller wanted to add an additional 
 706        session to the end of the existing data on the disc. 
 707   
 708        @param media: MediaDescription object describing the media capacity. 
 709        @param boundaries: Session boundaries as returned from L{_getBoundaries}. 
 710   
 711        @return: C{MediaCapacity} object describing the capacity of the media. 
 712        """ 
 713        if boundaries is None or boundaries[1] == 0: 
 714           logger.debug("Capacity calculations are based on a complete disc rewrite.") 
 715           sectorsAvailable = media.capacity - media.initialLeadIn 
 716           if sectorsAvailable < 0: sectorsAvailable = 0.0 
 717           bytesUsed = 0.0 
 718           bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 
 719        else: 
 720           logger.debug("Capacity calculations are based on a new ISO session.") 
 721           sectorsAvailable = media.capacity - boundaries[1] - media.leadIn 
 722           if sectorsAvailable < 0: sectorsAvailable = 0.0 
 723           bytesUsed = convertSize(boundaries[1], UNIT_SECTORS, UNIT_BYTES) 
 724           bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES) 
 725        logger.debug("Used [%s], available [%s].", displayBytes(bytesUsed), displayBytes(bytesAvailable)) 
 726        return MediaCapacity(bytesUsed, bytesAvailable, boundaries) 
  727   
 728   
 729      
 730      
 731      
 732   
 734        """ 
 735        Initializes the writer's associated ISO image. 
 736   
 737        This method initializes the C{image} instance variable so that the caller 
 738        can use the C{addImageEntry} method.  Once entries have been added, the 
 739        C{writeImage} method can be called with no arguments. 
 740   
 741        @param newDisc: Indicates whether the disc should be re-initialized 
 742        @type newDisc: Boolean true/false. 
 743   
 744        @param tmpdir: Temporary directory to use if needed 
 745        @type tmpdir: String representing a directory path on disk 
 746   
 747        @param mediaLabel: Media label to be applied to the image, if any 
 748        @type mediaLabel: String, no more than 25 characters long 
 749        """ 
 750        self._image = _ImageProperties() 
 751        self._image.newDisc = newDisc 
 752        self._image.tmpdir = encodePath(tmpdir) 
 753        self._image.mediaLabel = mediaLabel 
 754        self._image.entries = {}  
  755   
 756 -   def addImageEntry(self, path, graftPoint): 
  757        """ 
 758        Adds a filepath entry to the writer's associated ISO image. 
 759   
 760        The contents of the filepath -- but not the path itself -- will be added 
 761        to the image at the indicated graft point.  If you don't want to use a 
 762        graft point, just pass C{None}. 
 763   
 764        @note: Before calling this method, you must call L{initializeImage}. 
 765   
 766        @param path: File or directory to be added to the image 
 767        @type path: String representing a path on disk 
 768   
 769        @param graftPoint: Graft point to be used when adding this entry 
 770        @type graftPoint: String representing a graft point path, as described above 
 771   
 772        @raise ValueError: If initializeImage() was not previously called 
 773        """ 
 774        if self._image is None: 
 775           raise ValueError("Must call initializeImage() before using this method.") 
 776        if not os.path.exists(path): 
 777           raise ValueError("Path [%s] does not exist." % path) 
 778        self._image.entries[path] = graftPoint 
  779   
 781        """ 
 782        Resets (overrides) the newDisc flag on the internal image. 
 783        @param newDisc: New disc flag to set 
 784        @raise ValueError: If initializeImage() was not previously called 
 785        """ 
 786        if self._image is None: 
 787           raise ValueError("Must call initializeImage() before using this method.") 
 788        self._image.newDisc = newDisc 
  789   
 790      
 792        """ 
 793        Gets the estimated size of the image associated with the writer. 
 794        @return: Estimated size of the image, in bytes. 
 795        @raise IOError: If there is a problem calling C{mkisofs}. 
 796        @raise ValueError: If initializeImage() was not previously called 
 797        """ 
 798        if self._image is None: 
 799           raise ValueError("Must call initializeImage() before using this method.") 
 800        image = IsoImage() 
 801        for path in self._image.entries.keys(): 
 802           image.addEntry(path, self._image.entries[path], override=False, contentsOnly=True) 
 803        return image.getEstimatedSize() 
  804   
 805   
 806      
 807      
 808      
 809   
 811        """ 
 812        Opens the device's tray and leaves it open. 
 813   
 814        This only works if the device has a tray and supports ejecting its media. 
 815        We have no way to know if the tray is currently open or closed, so we 
 816        just send the appropriate command and hope for the best.  If the device 
 817        does not have a tray or does not support ejecting its media, then we do 
 818        nothing. 
 819   
 820        If the writer was constructed with C{noEject=True}, then this is a no-op. 
 821   
 822        Starting with Debian wheezy on my backup hardware, I started seeing 
 823        consistent problems with the eject command.  I couldn't tell whether 
 824        these problems were due to the device management system or to the new 
 825        kernel (3.2.0).  Initially, I saw simple eject failures, possibly because 
 826        I was opening and closing the tray too quickly.  I worked around that 
 827        behavior with the new ejectDelay flag. 
 828   
 829        Later, I sometimes ran into issues after writing an image to a disc: 
 830        eject would give errors like "unable to eject, last error: Inappropriate 
 831        ioctl for device".  Various sources online (like Ubuntu bug #875543) 
 832        suggested that the drive was being locked somehow, and that the 
 833        workaround was to run 'eject -i off' to unlock it.  Sure enough, that 
 834        fixed the problem for me, so now it's a normal error-handling strategy. 
 835   
 836        @raise IOError: If there is an error talking to the device. 
 837        """ 
 838        if not self._noEject: 
 839           if self._deviceHasTray and self._deviceCanEject: 
 840              args = CdWriter._buildOpenTrayArgs(self._device) 
 841              result = executeCommand(EJECT_COMMAND, args)[0] 
 842              if result != 0: 
 843                 logger.debug("Eject failed; attempting kludge of unlocking the tray before retrying.") 
 844                 self.unlockTray() 
 845                 result = executeCommand(EJECT_COMMAND, args)[0] 
 846                 if result != 0: 
 847                    raise IOError("Error (%d) executing eject command to open tray (failed even after unlocking tray)." % result) 
 848                 logger.debug("Kludge was apparently successful.") 
 849              if self.ejectDelay is not None: 
 850                 logger.debug("Per configuration, sleeping %d seconds after opening tray.", self.ejectDelay) 
 851                 time.sleep(self.ejectDelay) 
  852   
 863   
 865        """ 
 866        Closes the device's tray. 
 867   
 868        This only works if the device has a tray and supports ejecting its media. 
 869        We have no way to know if the tray is currently open or closed, so we 
 870        just send the appropriate command and hope for the best.  If the device 
 871        does not have a tray or does not support ejecting its media, then we do 
 872        nothing. 
 873   
 874        If the writer was constructed with C{noEject=True}, then this is a no-op. 
 875   
 876        @raise IOError: If there is an error talking to the device. 
 877        """ 
 878        if not self._noEject: 
 879           if self._deviceHasTray and self._deviceCanEject: 
 880              args = CdWriter._buildCloseTrayArgs(self._device) 
 881              command = resolveCommand(EJECT_COMMAND) 
 882              result = executeCommand(command, args)[0] 
 883              if result != 0: 
 884                 raise IOError("Error (%d) executing eject command to close tray." % result) 
  885   
 912   
 913 -   def writeImage(self, imagePath=None, newDisc=False, writeMulti=True): 
  914        """ 
 915        Writes an ISO image to the media in the device. 
 916   
 917        If C{newDisc} is passed in as C{True}, we assume that the entire disc 
 918        will be overwritten, and the media will be blanked before writing it if 
 919        possible (i.e. if the media is rewritable). 
 920   
 921        If C{writeMulti} is passed in as C{True}, then a multisession disc will 
 922        be written if possible (i.e. if the drive supports writing multisession 
 923        discs). 
 924   
 925        if C{imagePath} is passed in as C{None}, then the existing image 
 926        configured with C{initializeImage} will be used.  Under these 
 927        circumstances, the passed-in C{newDisc} flag will be ignored. 
 928   
 929        By default, we assume that the disc can be written multisession and that 
 930        we should append to the current contents of the disc.  In any case, the 
 931        ISO image must be generated appropriately (i.e. must take into account 
 932        any existing session boundaries, etc.) 
 933   
 934        @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image 
 935        @type imagePath: String representing a path on disk 
 936   
 937        @param newDisc: Indicates whether the entire disc will overwritten. 
 938        @type newDisc: Boolean true/false. 
 939   
 940        @param writeMulti: Indicates whether a multisession disc should be written, if possible. 
 941        @type writeMulti: Boolean true/false 
 942   
 943        @raise ValueError: If the image path is not absolute. 
 944        @raise ValueError: If some path cannot be encoded properly. 
 945        @raise IOError: If the media could not be written to for some reason. 
 946        @raise ValueError: If no image is passed in and initializeImage() was not previously called 
 947        """ 
 948        if imagePath is None: 
 949           if self._image is None: 
 950              raise ValueError("Must call initializeImage() before using this method with no image path.") 
 951           try: 
 952              imagePath = self._createImage() 
 953              self._writeImage(imagePath, writeMulti, self._image.newDisc) 
 954           finally: 
 955              if imagePath is not None and os.path.exists(imagePath): 
 956                 try: os.unlink(imagePath) 
 957                 except: pass 
 958        else: 
 959           imagePath = encodePath(imagePath) 
 960           if not os.path.isabs(imagePath): 
 961              raise ValueError("Image path must be absolute.") 
 962           self._writeImage(imagePath, writeMulti, newDisc) 
  963   
 964      
 966        """ 
 967        Creates an ISO image based on configuration in self._image. 
 968        @return: Path to the newly-created ISO image on disk. 
 969        @raise IOError: If there is an error writing the image to disk. 
 970        @raise ValueError: If there are no filesystem entries in the image 
 971        @raise ValueError: If a path cannot be encoded properly. 
 972        """ 
 973        path = None 
 974        capacity = self.retrieveCapacity(entireDisc=self._image.newDisc) 
 975        image = IsoImage(self.device, capacity.boundaries) 
 976        image.volumeId = self._image.mediaLabel   
 977        for key in self._image.entries.keys(): 
 978           image.addEntry(key, self._image.entries[key], override=False, contentsOnly=True) 
 979        size = image.getEstimatedSize() 
 980        logger.info("Image size will be %s.", displayBytes(size)) 
 981        available = capacity.bytesAvailable 
 982        logger.debug("Media capacity: %s", displayBytes(available)) 
 983        if size > available: 
 984           logger.error("Image [%s] does not fit in available capacity [%s].", displayBytes(size), displayBytes(available)) 
 985           raise IOError("Media does not contain enough capacity to store image.") 
 986        try: 
 987           (handle, path) = tempfile.mkstemp(dir=self._image.tmpdir) 
 988           try: os.close(handle) 
 989           except: pass 
 990           image.writeImage(path) 
 991           logger.debug("Completed creating image [%s].", path) 
 992           return path 
 993        except Exception, e: 
 994           if path is not None and os.path.exists(path): 
 995              try: os.unlink(path) 
 996              except: pass 
 997           raise e 
  998   
 999 -   def _writeImage(self, imagePath, writeMulti, newDisc): 
 1000        """ 
1001        Write an ISO image to disc using cdrecord. 
1002        The disc is blanked first if C{newDisc} is C{True}. 
1003        @param imagePath: Path to an ISO image on disk 
1004        @param writeMulti: Indicates whether a multisession disc should be written, if possible. 
1005        @param newDisc: Indicates whether the entire disc will overwritten. 
1006        """ 
1007        if newDisc: 
1008           self._blankMedia() 
1009        args = CdWriter._buildWriteArgs(self.hardwareId, imagePath, self._driveSpeed, writeMulti and self._deviceSupportsMulti) 
1010        command = resolveCommand(CDRECORD_COMMAND) 
1011        result = executeCommand(command, args)[0] 
1012        if result != 0: 
1013           raise IOError("Error (%d) executing command to write disc." % result) 
1014        self.refreshMedia() 
 1015   
1028   
1029   
1030      
1031      
1032      
1033   
1034     @staticmethod 
1036        """ 
1037        Parses the output from a C{cdrecord} properties command. 
1038   
1039        The C{output} parameter should be a list of strings as returned from 
1040        C{executeCommand} for a C{cdrecord} command with arguments as from 
1041        C{_buildPropertiesArgs}.  The list of strings will be parsed to yield 
1042        information about the properties of the device. 
1043   
1044        The output is expected to be a huge long list of strings.  Unfortunately, 
1045        the strings aren't in a completely regular format.  However, the format 
1046        of individual lines seems to be regular enough that we can look for 
1047        specific values.  Two kinds of parsing take place: one kind of parsing 
1048        picks out out specific values like the device id, device vendor, etc. 
1049        The other kind of parsing just sets a boolean flag C{True} if a matching 
1050        line is found.  All of the parsing is done with regular expressions. 
1051   
1052        Right now, pretty much nothing in the output is required and we should 
1053        parse an empty document successfully (albeit resulting in a device that 
1054        can't eject, doesn't have a tray and doesnt't support multisession 
1055        discs).   I had briefly considered erroring out if certain lines weren't 
1056        found or couldn't be parsed, but that seems like a bad idea given that 
1057        most of the information is just for reference. 
1058   
1059        The results are returned as a tuple of the object device attributes: 
1060        C{(deviceType, deviceVendor, deviceId, deviceBufferSize, 
1061        deviceSupportsMulti, deviceHasTray, deviceCanEject)}. 
1062   
1063        @param output: Output from a C{cdrecord -prcap} command. 
1064   
1065        @return: Results tuple as described above. 
1066        @raise IOError: If there is problem parsing the output. 
1067        """ 
1068        deviceType = None 
1069        deviceVendor = None 
1070        deviceId = None 
1071        deviceBufferSize = None 
1072        deviceSupportsMulti = False 
1073        deviceHasTray = False 
1074        deviceCanEject = False 
1075        typePattern   = re.compile(r"(^Device type\s*:\s*)(.*)(\s*)(.*$)") 
1076        vendorPattern = re.compile(r"(^Vendor_info\s*:\s*'\s*)(.*?)(\s*')(.*$)") 
1077        idPattern     = re.compile(r"(^Identifikation\s*:\s*'\s*)(.*?)(\s*')(.*$)") 
1078        bufferPattern = re.compile(r"(^\s*Buffer size in KB:\s*)(.*?)(\s*$)") 
1079        multiPattern  = re.compile(r"^\s*Does read multi-session.*$") 
1080        trayPattern   = re.compile(r"^\s*Loading mechanism type: tray.*$") 
1081        ejectPattern  = re.compile(r"^\s*Does support ejection.*$") 
1082        for line in output: 
1083           if typePattern.search(line): 
1084              deviceType =  typePattern.search(line).group(2) 
1085              logger.info("Device type is [%s].", deviceType) 
1086           elif vendorPattern.search(line): 
1087              deviceVendor = vendorPattern.search(line).group(2) 
1088              logger.info("Device vendor is [%s].", deviceVendor) 
1089           elif idPattern.search(line): 
1090              deviceId = idPattern.search(line).group(2) 
1091              logger.info("Device id is [%s].", deviceId) 
1092           elif bufferPattern.search(line): 
1093              try: 
1094                 sectors = int(bufferPattern.search(line).group(2)) 
1095                 deviceBufferSize = convertSize(sectors, UNIT_KBYTES, UNIT_BYTES) 
1096                 logger.info("Device buffer size is [%d] bytes.", deviceBufferSize) 
1097              except TypeError: pass 
1098           elif multiPattern.search(line): 
1099              deviceSupportsMulti = True 
1100              logger.info("Device does support multisession discs.") 
1101           elif trayPattern.search(line): 
1102              deviceHasTray = True 
1103              logger.info("Device has a tray.") 
1104           elif ejectPattern.search(line): 
1105              deviceCanEject = True 
1106              logger.info("Device can eject its media.") 
1107        return (deviceType, deviceVendor, deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, deviceCanEject) 
 1108   
1109     @staticmethod 
1111        """ 
1112        Parses the output from a C{cdrecord} capacity command. 
1113   
1114        The C{output} parameter should be a list of strings as returned from 
1115        C{executeCommand} for a C{cdrecord} command with arguments as from 
1116        C{_buildBoundaryArgs}.  The list of strings will be parsed to yield 
1117        information about the capacity of the media in the device. 
1118   
1119        Basically, we expect the list of strings to include just one line, a pair 
1120        of values.  There isn't supposed to be whitespace, but we allow it anyway 
1121        in the regular expression.  Any lines below the one line we parse are 
1122        completely ignored.  It would be a good idea to ignore C{stderr} when 
1123        executing the C{cdrecord} command that generates output for this method, 
1124        because sometimes C{cdrecord} spits out kernel warnings about the actual 
1125        output. 
1126   
1127        The results are returned as a tuple of (lower, upper) as needed by the 
1128        C{IsoImage} class.  Note that these values are in terms of ISO sectors, 
1129        not bytes.  Clients should generally consider the boundaries value 
1130        opaque, however. 
1131   
1132        @note: If the boundaries output can't be parsed, we return C{None}. 
1133   
1134        @param output: Output from a C{cdrecord -msinfo} command. 
1135   
1136        @return: Boundaries tuple as described above. 
1137        @raise IOError: If there is problem parsing the output. 
1138        """ 
1139        if len(output) < 1: 
1140           logger.warn("Unable to read disc (might not be initialized); returning full capacity.") 
1141           return None 
1142        boundaryPattern = re.compile(r"(^\s*)([0-9]*)(\s*,\s*)([0-9]*)(\s*$)") 
1143        parsed = boundaryPattern.search(output[0]) 
1144        if not parsed: 
1145           raise IOError("Unable to parse output of boundaries command.") 
1146        try: 
1147           boundaries = ( int(parsed.group(2)), int(parsed.group(4)) ) 
1148        except TypeError: 
1149           raise IOError("Unable to parse output of boundaries command.") 
1150        return boundaries 
 1151   
1152   
1153      
1154      
1155      
1156   
1157     @staticmethod 
1159        """ 
1160        Builds a list of arguments to be passed to a C{eject} command. 
1161   
1162        The arguments will cause the C{eject} command to open the tray and 
1163        eject the media.  No validation is done by this method as to whether 
1164        this action actually makes sense. 
1165   
1166        @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}. 
1167   
1168        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1169        """ 
1170        args = [] 
1171        args.append(device) 
1172        return args 
 1173   
1174     @staticmethod 
1176        """ 
1177        Builds a list of arguments to be passed to a C{eject} command. 
1178   
1179        The arguments will cause the C{eject} command to unlock the tray. 
1180   
1181        @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}. 
1182   
1183        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1184        """ 
1185        args = [] 
1186        args.append("-i") 
1187        args.append("off") 
1188        args.append(device) 
1189        return args 
 1190   
1191     @staticmethod 
1193        """ 
1194        Builds a list of arguments to be passed to a C{eject} command. 
1195   
1196        The arguments will cause the C{eject} command to close the tray and reload 
1197        the media.  No validation is done by this method as to whether this 
1198        action actually makes sense. 
1199   
1200        @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}. 
1201   
1202        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1203        """ 
1204        args = [] 
1205        args.append("-t") 
1206        args.append(device) 
1207        return args 
 1208   
1209     @staticmethod 
1211        """ 
1212        Builds a list of arguments to be passed to a C{cdrecord} command. 
1213   
1214        The arguments will cause the C{cdrecord} command to ask the device 
1215        for a list of its capacities via the C{-prcap} switch. 
1216   
1217        @param hardwareId: Hardware id for the device (either SCSI id or device path) 
1218   
1219        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1220        """ 
1221        args = [] 
1222        args.append("-prcap") 
1223        args.append("dev=%s" % hardwareId) 
1224        return args 
 1225   
1226     @staticmethod 
1228        """ 
1229        Builds a list of arguments to be passed to a C{cdrecord} command. 
1230   
1231        The arguments will cause the C{cdrecord} command to ask the device for 
1232        the current multisession boundaries of the media using the C{-msinfo} 
1233        switch. 
1234   
1235        @param hardwareId: Hardware id for the device (either SCSI id or device path) 
1236   
1237        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1238        """ 
1239        args = [] 
1240        args.append("-msinfo") 
1241        args.append("dev=%s" % hardwareId) 
1242        return args 
 1243   
1244     @staticmethod 
1246        """ 
1247        Builds a list of arguments to be passed to a C{cdrecord} command. 
1248   
1249        The arguments will cause the C{cdrecord} command to blank the media in 
1250        the device identified by C{hardwareId}.  No validation is done by this method 
1251        as to whether the action makes sense (i.e. to whether the media even can 
1252        be blanked). 
1253   
1254        @param hardwareId: Hardware id for the device (either SCSI id or device path) 
1255        @param driveSpeed: Speed at which the drive writes. 
1256   
1257        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1258        """ 
1259        args = [] 
1260        args.append("-v") 
1261        args.append("blank=fast") 
1262        if driveSpeed is not None: 
1263           args.append("speed=%d" % driveSpeed) 
1264        args.append("dev=%s" % hardwareId) 
1265        return args 
 1266   
1267     @staticmethod 
1268 -   def _buildWriteArgs(hardwareId, imagePath, driveSpeed=None, writeMulti=True): 
 1269        """ 
1270        Builds a list of arguments to be passed to a C{cdrecord} command. 
1271   
1272        The arguments will cause the C{cdrecord} command to write the indicated 
1273        ISO image (C{imagePath}) to the media in the device identified by 
1274        C{hardwareId}.  The C{writeMulti} argument controls whether to write a 
1275        multisession disc.  No validation is done by this method as to whether 
1276        the action makes sense (i.e. to whether the device even can write 
1277        multisession discs, for instance). 
1278   
1279        @param hardwareId: Hardware id for the device (either SCSI id or device path) 
1280        @param imagePath: Path to an ISO image on disk. 
1281        @param driveSpeed: Speed at which the drive writes. 
1282        @param writeMulti: Indicates whether to write a multisession disc. 
1283   
1284        @return: List suitable for passing to L{util.executeCommand} as C{args}. 
1285        """ 
1286        args = [] 
1287        args.append("-v") 
1288        if driveSpeed is not None: 
1289           args.append("speed=%d" % driveSpeed) 
1290        args.append("dev=%s" % hardwareId) 
1291        if writeMulti: 
1292           args.append("-multi") 
1293        args.append("-data") 
1294        args.append(imagePath) 
1295        return args 
  1296