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