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
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