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 """
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
53
54
55
56 import os
57 import re
58 import logging
59 import tempfile
60
61
62 from CedarBackup2.filesystem import BackupFileList
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
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
85
86
155
156
157
158
159
160
218
219
220
221
222
223
225 """
226 Simple value object to hold image properties for C{DvdWriter}.
227 """
229 self.newDisc = False
230 self.tmpdir = None
231 self.mediaLabel = None
232 self.entries = None
233
234
235
236
237
238
240
241
242
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
361
362
363 - def __init__(self, device, scsiId=None, driveSpeed=None,
364 mediaType=MEDIA_DVDPLUSRW, noEject=False, 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 unittest: Turns off certain validations, for use in unit testing.
398 @type unittest: Boolean true/false
399
400 @raise ValueError: If the device is not valid for some reason.
401 @raise ValueError: If the SCSI id is not in a valid form.
402 @raise ValueError: If the drive speed is not an integer >= 1.
403 """
404 if scsiId is not None:
405 logger.warn("SCSI id [%s] will be ignored by DvdWriter." % scsiId)
406 self._image = None
407 self._device = validateDevice(device, unittest)
408 self._scsiId = scsiId
409 self._driveSpeed = validateDriveSpeed(driveSpeed)
410 self._media = MediaDefinition(mediaType)
411 if noEject:
412 self._deviceHasTray = False
413 self._deviceCanEject = False
414 else:
415 self._deviceHasTray = True
416 self._deviceCanEject = True
417
418
419
420
421
422
424 """
425 Property target used to get the device value.
426 """
427 return self._device
428
430 """
431 Property target used to get the SCSI id value.
432 """
433 return self._scsiId
434
436 """
437 Property target used to get the hardware id value.
438 """
439 return self._device
440
442 """
443 Property target used to get the drive speed.
444 """
445 return self._driveSpeed
446
452
454 """
455 Property target used to get the device-has-tray flag.
456 """
457 return self._deviceHasTray
458
460 """
461 Property target used to get the device-can-eject flag.
462 """
463 return self._deviceCanEject
464
465 device = property(_getDevice, None, None, doc="Filesystem device name for this writer.")
466 scsiId = property(_getScsiId, None, None, doc="SCSI id for the device (saved for reference only).")
467 hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer (always the device path).");
468 driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.")
469 media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.")
470 deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.")
471 deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.")
472
473
474
475
476
477
479 """Indicates whether the media is rewritable per configuration."""
480 return self._media.rewritable
481
483 """
484 Retrieves capacity for the current media in terms of a C{MediaCapacity}
485 object.
486
487 If C{entireDisc} is passed in as C{True}, the capacity will be for the
488 entire disc, as if it were to be rewritten from scratch. The same will
489 happen if the disc can't be read for some reason. Otherwise, the capacity
490 will be calculated by subtracting the sectors currently used on the disc,
491 as reported by C{growisofs} itself.
492
493 @param entireDisc: Indicates whether to return capacity for entire disc.
494 @type entireDisc: Boolean true/false
495
496 @return: C{MediaCapacity} object describing the capacity of the media.
497
498 @raise ValueError: If there is a problem parsing the C{growisofs} output
499 @raise IOError: If the media could not be read for some reason.
500 """
501 sectorsUsed = 0
502 if not entireDisc:
503 sectorsUsed = self._retrieveSectorsUsed()
504 sectorsAvailable = self._media.capacity - sectorsUsed
505 bytesUsed = convertSize(sectorsUsed, UNIT_SECTORS, UNIT_BYTES)
506 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES)
507 return MediaCapacity(bytesUsed, bytesAvailable)
508
509
510
511
512
513
515 """
516 Initializes the writer's associated ISO image.
517
518 This method initializes the C{image} instance variable so that the caller
519 can use the C{addImageEntry} method. Once entries have been added, the
520 C{writeImage} method can be called with no arguments.
521
522 @param newDisc: Indicates whether the disc should be re-initialized
523 @type newDisc: Boolean true/false
524
525 @param tmpdir: Temporary directory to use if needed
526 @type tmpdir: String representing a directory path on disk
527
528 @param mediaLabel: Media label to be applied to the image, if any
529 @type mediaLabel: String, no more than 25 characters long
530 """
531 self._image = _ImageProperties()
532 self._image.newDisc = newDisc
533 self._image.tmpdir = encodePath(tmpdir)
534 self._image.mediaLabel = mediaLabel
535 self._image.entries = {}
536
537 - def addImageEntry(self, path, graftPoint):
538 """
539 Adds a filepath entry to the writer's associated ISO image.
540
541 The contents of the filepath -- but not the path itself -- will be added
542 to the image at the indicated graft point. If you don't want to use a
543 graft point, just pass C{None}.
544
545 @note: Before calling this method, you must call L{initializeImage}.
546
547 @param path: File or directory to be added to the image
548 @type path: String representing a path on disk
549
550 @param graftPoint: Graft point to be used when adding this entry
551 @type graftPoint: String representing a graft point path, as described above
552
553 @raise ValueError: If initializeImage() was not previously called
554 @raise ValueError: If the path is not a valid file or directory
555 """
556 if self._image is None:
557 raise ValueError("Must call initializeImage() before using this method.")
558 if not os.path.exists(path):
559 raise ValueError("Path [%s] does not exist." % path)
560 self._image.entries[path] = graftPoint
561
563 """
564 Resets (overrides) the newDisc flag on the internal image.
565 @param newDisc: New disc flag to set
566 @raise ValueError: If initializeImage() was not previously called
567 """
568 if self._image is None:
569 raise ValueError("Must call initializeImage() before using this method.")
570 self._image.newDisc = newDisc
571
573 """
574 Gets the estimated size of the image associated with the writer.
575
576 This is an estimate and is conservative. The actual image could be as
577 much as 450 blocks (sectors) smaller under some circmstances.
578
579 @return: Estimated size of the image, in bytes.
580
581 @raise IOError: If there is a problem calling C{mkisofs}.
582 @raise ValueError: If initializeImage() was not previously called
583 """
584 if self._image is None:
585 raise ValueError("Must call initializeImage() before using this method.")
586 return DvdWriter._getEstimatedImageSize(self._image.entries)
587
588
589
590
591
592
594 """
595 Opens the device's tray and leaves it open.
596
597 This only works if the device has a tray and supports ejecting its media.
598 We have no way to know if the tray is currently open or closed, so we
599 just send the appropriate command and hope for the best. If the device
600 does not have a tray or does not support ejecting its media, then we do
601 nothing.
602
603 @raise IOError: If there is an error talking to the device.
604 """
605 if self._deviceHasTray and self._deviceCanEject:
606 command = resolveCommand(EJECT_COMMAND)
607 args = [ self.device, ]
608 result = executeCommand(command, args)[0]
609 if result != 0:
610 raise IOError("Error (%d) executing eject command to open tray." % result)
611
613 """
614 Closes the device's tray.
615
616 This only works if the device has a tray and supports ejecting its media.
617 We have no way to know if the tray is currently open or closed, so we
618 just send the appropriate command and hope for the best. If the device
619 does not have a tray or does not support ejecting its media, then we do
620 nothing.
621
622 @raise IOError: If there is an error talking to the device.
623 """
624 if self._deviceHasTray and self._deviceCanEject:
625 command = resolveCommand(EJECT_COMMAND)
626 args = [ "-t", self.device, ]
627 result = executeCommand(command, args)[0]
628 if result != 0:
629 raise IOError("Error (%d) executing eject command to close tray." % result)
630
650
651 - def writeImage(self, imagePath=None, newDisc=False, writeMulti=True):
652 """
653 Writes an ISO image to the media in the device.
654
655 If C{newDisc} is passed in as C{True}, we assume that the entire disc
656 will be re-created from scratch. Note that unlike C{CdWriter},
657 C{DvdWriter} does not blank rewritable media before reusing it; however,
658 C{growisofs} is called such that the media will be re-initialized as
659 needed.
660
661 If C{imagePath} is passed in as C{None}, then the existing image
662 configured with C{initializeImage()} will be used. Under these
663 circumstances, the passed-in C{newDisc} flag will be ignored and the
664 value passed in to C{initializeImage()} will apply instead.
665
666 The C{writeMulti} argument is ignored. It exists for compatibility with
667 the Cedar Backup image writer interface.
668
669 @note: The image size indicated in the log ("Image size will be...") is
670 an estimate. The estimate is conservative and is probably larger than
671 the actual space that C{dvdwriter} will use.
672
673 @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image
674 @type imagePath: String representing a path on disk
675
676 @param newDisc: Indicates whether the disc should be re-initialized
677 @type newDisc: Boolean true/false.
678
679 @param writeMulti: Unused
680 @type writeMulti: Boolean true/false
681
682 @raise ValueError: If the image path is not absolute.
683 @raise ValueError: If some path cannot be encoded properly.
684 @raise IOError: If the media could not be written to for some reason.
685 @raise ValueError: If no image is passed in and initializeImage() was not previously called
686 """
687 if not writeMulti:
688 logger.warn("writeMulti value of [%s] ignored." % writeMulti)
689 if imagePath is None:
690 if self._image is None:
691 raise ValueError("Must call initializeImage() before using this method with no image path.")
692 size = self.getEstimatedImageSize()
693 logger.info("Image size will be %s (estimated)." % displayBytes(size))
694 available = self.retrieveCapacity(entireDisc=self._image.newDisc).bytesAvailable
695 if size > available:
696 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available)))
697 raise IOError("Media does not contain enough capacity to store image.")
698 self._writeImage(self._image.newDisc, None, self._image.entries, self._image.mediaLabel)
699 else:
700 if not os.path.isabs(imagePath):
701 raise ValueError("Image path must be absolute.")
702 imagePath = encodePath(imagePath)
703 self._writeImage(newDisc, imagePath, None)
704
705
706
707
708
709
710 - def _writeImage(self, newDisc, imagePath, entries, mediaLabel=None):
711 """
712 Writes an image to disc using either an entries list or an ISO image on
713 disk.
714
715 Callers are assumed to have done validation on paths, etc. before calling
716 this method.
717
718 @param newDisc: Indicates whether the disc should be re-initialized
719 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries}
720 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath}
721
722 @raise IOError: If the media could not be written to for some reason.
723 """
724 command = resolveCommand(GROWISOFS_COMMAND)
725 args = DvdWriter._buildWriteArgs(newDisc, self.hardwareId, self._driveSpeed, imagePath, entries, mediaLabel, dryRun=False)
726 (result, output) = executeCommand(command, args, returnOutput=True)
727 if result != 0:
728 DvdWriter._searchForOverburn(output)
729 raise IOError("Error (%d) executing command to write disc." % result)
730 self.refreshMedia()
731
733 """
734 Gets the estimated size of a set of image entries.
735
736 This is implemented in terms of the C{IsoImage} class. The returned
737 value is calculated by adding a "fudge factor" to the value from
738 C{IsoImage}. This fudge factor was determined by experimentation and is
739 conservative -- the actual image could be as much as 450 blocks smaller
740 under some circumstances.
741
742 @param entries: Dictionary mapping path to graft point.
743
744 @return: Total estimated size of image, in bytes.
745
746 @raise ValueError: If there are no entries in the dictionary
747 @raise ValueError: If any path in the dictionary does not exist
748 @raise IOError: If there is a problem calling C{mkisofs}.
749 """
750 fudgeFactor = convertSize(2500.0, UNIT_SECTORS, UNIT_BYTES)
751 if len(entries.keys()) == 0:
752 raise ValueError("Must add at least one entry with addImageEntry().")
753 image = IsoImage()
754 for path in entries.keys():
755 image.addEntry(path, entries[path], override=False, contentsOnly=True)
756 estimatedSize = image.getEstimatedSize() + fudgeFactor
757 return estimatedSize
758 _getEstimatedImageSize = staticmethod(_getEstimatedImageSize)
759
761 """
762 Retrieves the number of sectors used on the current media.
763
764 This is a little ugly. We need to call growisofs in "dry-run" mode and
765 parse some information from its output. However, to do that, we need to
766 create a dummy file that we can pass to the command -- and we have to
767 make sure to remove it later.
768
769 Once growisofs has been run, then we call C{_parseSectorsUsed} to parse
770 the output and calculate the number of sectors used on the media.
771
772 @return: Number of sectors used on the media
773 """
774 tempdir = tempfile.mkdtemp()
775 try:
776 entries = { tempdir: None }
777 args = DvdWriter._buildWriteArgs(False, self.hardwareId, self.driveSpeed, None, entries, None, dryRun=True)
778 command = resolveCommand(GROWISOFS_COMMAND)
779 (result, output) = executeCommand(command, args, returnOutput=True)
780 if result != 0:
781 logger.debug("Error (%d) calling growisofs to read sectors used." % result)
782 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.")
783 return 0.0
784 sectorsUsed = DvdWriter._parseSectorsUsed(output)
785 logger.debug("Determined sectors used as %s" % sectorsUsed)
786 return sectorsUsed
787 finally:
788 if os.path.exists(tempdir):
789 try:
790 os.rmdir(tempdir)
791 except: pass
792
794 """
795 Parse sectors used information out of C{growisofs} output.
796
797 The first line of a growisofs run looks something like this::
798
799 Executing 'mkisofs -C 973744,1401056 -M /dev/fd/3 -r -graft-points music4/=music | builtin_dd of=/dev/cdrom obs=32k seek=87566'
800
801 Dmitry has determined that the seek value in this line gives us
802 information about how much data has previously been written to the media.
803 That value multiplied by 16 yields the number of sectors used.
804
805 If the seek line cannot be found in the output, then sectors used of zero
806 is assumed.
807
808 @return: Sectors used on the media, as a floating point number.
809
810 @raise ValueError: If the output cannot be parsed properly.
811 """
812 if output is not None:
813 pattern = re.compile(r"(^)(.*)(seek=)(.*)('$)")
814 for line in output:
815 match = pattern.search(line)
816 if match is not None:
817 try:
818 return float(match.group(4).strip()) * 16.0
819 except ValueError:
820 raise ValueError("Unable to parse sectors used out of growisofs output.")
821 logger.warn("Unable to read disc (might not be initialized); returning zero sectors used.")
822 return 0.0
823 _parseSectorsUsed = staticmethod(_parseSectorsUsed)
824
826 """
827 Search for an "overburn" error message in C{growisofs} output.
828
829 The C{growisofs} command returns a non-zero exit code and puts a message
830 into the output -- even on a dry run -- if there is not enough space on
831 the media. This is called an "overburn" condition.
832
833 The error message looks like this::
834
835 :-( /dev/cdrom: 894048 blocks are free, 2033746 to be written!
836
837 This method looks for the overburn error message anywhere in the output.
838 If a matching error message is found, an C{IOError} exception is raised
839 containing relevant information about the problem. Otherwise, the method
840 call returns normally.
841
842 @param output: List of output lines to search, as from C{executeCommand}
843
844 @raise IOError: If an overburn condition is found.
845 """
846 if output is None:
847 return
848 pattern = re.compile(r"(^)(:-[(])(\s*.*:\s*)(.* )(blocks are free, )(.* )(to be written!)")
849 for line in output:
850 match = pattern.search(line)
851 if match is not None:
852 try:
853 available = convertSize(float(match.group(4).strip()), UNIT_SECTORS, UNIT_BYTES)
854 size = convertSize(float(match.group(6).strip()), UNIT_SECTORS, UNIT_BYTES)
855 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available)))
856 except ValueError:
857 logger.error("Image does not fit in available capacity (no useful capacity info available).")
858 raise IOError("Media does not contain enough capacity to store image.")
859 _searchForOverburn = staticmethod(_searchForOverburn)
860
861 - def _buildWriteArgs(newDisc, hardwareId, driveSpeed, imagePath, entries, mediaLabel=None, dryRun=False):
862 """
863 Builds a list of arguments to be passed to a C{growisofs} command.
864
865 The arguments will either cause C{growisofs} to write the indicated image
866 file to disc, or will pass C{growisofs} a list of directories or files
867 that should be written to disc.
868
869 If a new image is created, it will always be created with Rock Ridge
870 extensions (-r). A volume name will be applied (-V) if C{mediaLabel} is
871 not C{None}.
872
873 @param newDisc: Indicates whether the disc should be re-initialized
874 @param hardwareId: Hardware id for the device
875 @param driveSpeed: Speed at which the drive writes.
876 @param imagePath: Path to an ISO image on disk, or c{None} to use C{entries}
877 @param entries: Mapping from path to graft point, or C{None} to use C{imagePath}
878 @param mediaLabel: Media label to set on the image, if any
879 @param dryRun: Says whether to make this a dry run (for checking capacity)
880
881 @note: If we write an existing image to disc, then the mediaLabel is
882 ignored. The media label is an attribute of the image, and should be set
883 on the image when it is created.
884
885 @note: We always pass the undocumented option C{-use-the-force-like=tty}
886 to growisofs. Without this option, growisofs will refuse to execute
887 certain actions when running from cron. A good example is -Z, which
888 happily overwrites an existing DVD from the command-line, but fails when
889 run from cron. It took a while to figure that out, since it worked every
890 time I tested it by hand. :(
891
892 @return: List suitable for passing to L{util.executeCommand} as C{args}.
893
894 @raise ValueError: If caller does not pass one or the other of imagePath or entries.
895 """
896 args = []
897 if (imagePath is None and entries is None) or (imagePath is not None and entries is not None):
898 raise ValueError("Must use either imagePath or entries.")
899 args.append("-use-the-force-luke=tty")
900 if dryRun:
901 args.append("-dry-run")
902 if driveSpeed is not None:
903 args.append("-speed=%d" % driveSpeed)
904 if newDisc:
905 args.append("-Z")
906 else:
907 args.append("-M")
908 if imagePath is not None:
909 args.append("%s=%s" % (hardwareId, imagePath))
910 else:
911 args.append(hardwareId)
912 if mediaLabel is not None:
913 args.append("-V")
914 args.append(mediaLabel)
915 args.append("-r")
916 args.append("-graft-points")
917 keys = entries.keys()
918 keys.sort()
919 for key in keys:
920
921 if entries[key] is None:
922 args.append(key)
923 else:
924 args.append("%s/=%s" % (entries[key].strip("/"), key))
925 return args
926 _buildWriteArgs = staticmethod(_buildWriteArgs)
927