1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17   
 18   
 19   
 20   
 21   
 22   
 23   
 24   
 25   
 26   
 27   
 28   
 29   
 30   
 31   
 32   
 33   
 34   
 35   
 36   
 37   
 38  """ 
 39  Provides functionality related to 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   
 52   
 53   
 54   
 55  import os 
 56  import re 
 57  import logging 
 58  import tempfile 
 59  import time 
 60   
 61   
 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   
 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", ] 
154   
217   
224     """ 
225     Simple value object to hold image properties for C{DvdWriter}. 
226     """ 
228        self.newDisc = False 
229        self.tmpdir = None 
230        self.mediaLabel = None 
231        self.entries = None      
  232   
239   
240      
241      
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      
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   
413        self._device = validateDevice(device, unittest) 
414        self._scsiId = scsiId   
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    
424           self._deviceCanEject = True   
 425   
426   
427      
428      
429      
430   
432        """ 
433        Property target used to get the device value. 
434        """ 
435        return self._device 
 436   
438        """ 
439        Property target used to get the SCSI id value. 
440        """ 
441        return self._scsiId 
 442   
444        """ 
445        Property target used to get the hardware id value. 
446        """ 
447        return self._device 
 448   
450        """ 
451        Property target used to get the drive speed. 
452        """ 
453        return self._driveSpeed 
 454   
460   
462        """ 
463        Property target used to get the device-has-tray flag. 
464        """ 
465        return self._deviceHasTray 
 466   
468        """ 
469        Property target used to get the device-can-eject flag. 
470        """ 
471        return self._deviceCanEject 
 472   
478   
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      
498      
499   
501        """Indicates whether the media is rewritable per configuration.""" 
502        return self._media.rewritable 
 503   
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   
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      
534      
535   
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 = {}  
 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   
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   
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      
613      
614   
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   
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   
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   
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      
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)  
791           raise IOError("Error (%d) executing command to write disc." % result) 
792        self.refreshMedia() 
 793   
794     @staticmethod 
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)   
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   
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 
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 
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")  
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")     
979           args.append("-graft-points") 
980           keys = entries.keys() 
981           keys.sort()  
982           for key in keys: 
983               
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