CedarBackup2.writers.dvdwriter

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 (>= 2.7)
     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.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