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 CD writer devices.
41
42 @sort: MediaDefinition, MediaCapacity, CdWriter,
43 MEDIA_CDRW_74, MEDIA_CDR_74, MEDIA_CDRW_80, MEDIA_CDR_80
44
45 @var MEDIA_CDRW_74: Constant representing 74-minute CD-RW media.
46 @var MEDIA_CDR_74: Constant representing 74-minute CD-R media.
47 @var MEDIA_CDRW_80: Constant representing 80-minute CD-RW media.
48 @var MEDIA_CDR_80: Constant representing 80-minute CD-R media.
49
50 @author: Kenneth J. Pronovici <pronovic@ieee.org>
51 """
52
53
54
55
56
57
58 import os
59 import re
60 import logging
61 import tempfile
62 import time
63
64
65 from CedarBackup2.util import resolveCommand, executeCommand
66 from CedarBackup2.util import convertSize, displayBytes, encodePath
67 from CedarBackup2.util import UNIT_SECTORS, UNIT_BYTES, UNIT_KBYTES, UNIT_MBYTES
68 from CedarBackup2.writers.util import validateDevice, validateScsiId, validateDriveSpeed
69 from CedarBackup2.writers.util import IsoImage
70
71
72
73
74
75
76 logger = logging.getLogger("CedarBackup2.log.writers.cdwriter")
77
78 MEDIA_CDRW_74 = 1
79 MEDIA_CDR_74 = 2
80 MEDIA_CDRW_80 = 3
81 MEDIA_CDR_80 = 4
82
83 CDRECORD_COMMAND = [ "cdrecord", ]
84 EJECT_COMMAND = [ "eject", ]
85 MKISOFS_COMMAND = [ "mkisofs", ]
182
263
270 """
271 Simple value object to hold image properties for C{DvdWriter}.
272 """
274 self.newDisc = False
275 self.tmpdir = None
276 self.mediaLabel = None
277 self.entries = None
278
279
280
281
282
283
284 -class CdWriter(object):
285
286
287
288
289
290 """
291 Class representing a device that knows how to write CD media.
292
293 Summary
294 =======
295
296 This is a class representing a device that knows how to write CD media. It
297 provides common operations for the device, such as ejecting the media,
298 writing an ISO image to the media, or checking for the current media
299 capacity. It also provides a place to store device attributes, such as
300 whether the device supports writing multisession discs, etc.
301
302 This class is implemented in terms of the C{eject} and C{cdrecord}
303 programs, both of which should be available on most UN*X platforms.
304
305 Image Writer Interface
306 ======================
307
308 The following methods make up the "image writer" interface shared
309 with other kinds of writers (such as DVD writers)::
310
311 __init__
312 initializeImage()
313 addImageEntry()
314 writeImage()
315 setImageNewDisc()
316 retrieveCapacity()
317 getEstimatedImageSize()
318
319 Only these methods will be used by other Cedar Backup functionality
320 that expects a compatible image writer.
321
322 The media attribute is also assumed to be available.
323
324 Media Types
325 ===========
326
327 This class knows how to write to two different kinds of media, represented
328 by the following constants:
329
330 - C{MEDIA_CDR_74}: 74-minute CD-R media (650 MB capacity)
331 - C{MEDIA_CDRW_74}: 74-minute CD-RW media (650 MB capacity)
332 - C{MEDIA_CDR_80}: 80-minute CD-R media (700 MB capacity)
333 - C{MEDIA_CDRW_80}: 80-minute CD-RW media (700 MB capacity)
334
335 Most hardware can read and write both 74-minute and 80-minute CD-R and
336 CD-RW media. Some older drives may only be able to write CD-R media.
337 The difference between the two is that CD-RW media can be rewritten
338 (erased), while CD-R media cannot be.
339
340 I do not support any other configurations for a couple of reasons. The
341 first is that I've never tested any other kind of media. The second is
342 that anything other than 74 or 80 minute is apparently non-standard.
343
344 Device Attributes vs. Media Attributes
345 ======================================
346
347 A given writer instance has two different kinds of attributes associated
348 with it, which I call device attributes and media attributes. Device
349 attributes are things which can be determined without looking at the
350 media, such as whether the drive supports writing multisession disks or
351 has a tray. Media attributes are attributes which vary depending on the
352 state of the media, such as the remaining capacity on a disc. In
353 general, device attributes are available via instance variables and are
354 constant over the life of an object, while media attributes can be
355 retrieved through method calls.
356
357 Talking to Hardware
358 ===================
359
360 This class needs to talk to CD writer hardware in two different ways:
361 through cdrecord to actually write to the media, and through the
362 filesystem to do things like open and close the tray.
363
364 Historically, CdWriter has interacted with cdrecord using the scsiId
365 attribute, and with most other utilities using the device attribute.
366 This changed somewhat in Cedar Backup 2.9.0.
367
368 When Cedar Backup was first written, the only way to interact with
369 cdrecord was by using a SCSI device id. IDE devices were mapped to
370 pseudo-SCSI devices through the kernel. Later, extended SCSI "methods"
371 arrived, and it became common to see C{ATA:1,0,0} or C{ATAPI:0,0,0} as a
372 way to address IDE hardware. By late 2006, C{ATA} and C{ATAPI} had
373 apparently been deprecated in favor of just addressing the IDE device
374 directly by name, i.e. C{/dev/cdrw}.
375
376 Because of this latest development, it no longer makes sense to require a
377 CdWriter to be created with a SCSI id -- there might not be one. So, the
378 passed-in SCSI id is now optional. Also, there is now a hardwareId
379 attribute. This attribute is filled in with either the SCSI id (if
380 provided) or the device (otherwise). The hardware id is the value that
381 will be passed to cdrecord in the C{dev=} argument.
382
383 Testing
384 =======
385
386 It's rather difficult to test this code in an automated fashion, even if
387 you have access to a physical CD writer drive. It's even more difficult
388 to test it if you are running on some build daemon (think of a Debian
389 autobuilder) which can't be expected to have any hardware or any media
390 that you could write to.
391
392 Because of this, much of the implementation below is in terms of static
393 methods that are supposed to take defined actions based on their
394 arguments. Public methods are then implemented in terms of a series of
395 calls to simplistic static methods. This way, we can test as much as
396 possible of the functionality via testing the static methods, while
397 hoping that if the static methods are called appropriately, things will
398 work properly. It's not perfect, but it's much better than no testing at
399 all.
400
401 @sort: __init__, isRewritable, _retrieveProperties, retrieveCapacity, _getBoundaries,
402 _calculateCapacity, openTray, closeTray, refreshMedia, writeImage,
403 _blankMedia, _parsePropertiesOutput, _parseBoundariesOutput,
404 _buildOpenTrayArgs, _buildCloseTrayArgs, _buildPropertiesArgs,
405 _buildBoundariesArgs, _buildBlankArgs, _buildWriteArgs,
406 device, scsiId, hardwareId, driveSpeed, media, deviceType, deviceVendor,
407 deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, deviceCanEject,
408 initializeImage, addImageEntry, writeImage, setImageNewDisc, getEstimatedImageSize
409 """
410
411
412
413
414
415 - def __init__(self, device, scsiId=None, driveSpeed=None,
416 mediaType=MEDIA_CDRW_74, noEject=False,
417 refreshMediaDelay=0, ejectDelay=0, unittest=False):
418 """
419 Initializes a CD writer object.
420
421 The current user must have write access to the device at the time the
422 object is instantiated, or an exception will be thrown. However, no
423 media-related validation is done, and in fact there is no need for any
424 media to be in the drive until one of the other media attribute-related
425 methods is called.
426
427 The various instance variables such as C{deviceType}, C{deviceVendor},
428 etc. might be C{None}, if we're unable to parse this specific information
429 from the C{cdrecord} output. This information is just for reference.
430
431 The SCSI id is optional, but the device path is required. If the SCSI id
432 is passed in, then the hardware id attribute will be taken from the SCSI
433 id. Otherwise, the hardware id will be taken from the device.
434
435 If cdrecord improperly detects whether your writer device has a tray and
436 can be safely opened and closed, then pass in C{noEject=False}. This
437 will override the properties and the device will never be ejected.
438
439 @note: The C{unittest} parameter should never be set to C{True}
440 outside of Cedar Backup code. It is intended for use in unit testing
441 Cedar Backup internals and has no other sensible purpose.
442
443 @param device: Filesystem device associated with this writer.
444 @type device: Absolute path to a filesystem device, i.e. C{/dev/cdrw}
445
446 @param scsiId: SCSI id for the device (optional).
447 @type scsiId: If provided, SCSI id in the form C{[<method>:]scsibus,target,lun}
448
449 @param driveSpeed: Speed at which the drive writes.
450 @type driveSpeed: Use C{2} for 2x device, etc. or C{None} to use device default.
451
452 @param mediaType: Type of the media that is assumed to be in the drive.
453 @type mediaType: One of the valid media type as discussed above.
454
455 @param noEject: Overrides properties to indicate that the device does not support eject.
456 @type noEject: Boolean true/false
457
458 @param refreshMediaDelay: Refresh media delay to use, if any
459 @type refreshMediaDelay: Number of seconds, an integer >= 0
460
461 @param ejectDelay: Eject delay to use, if any
462 @type ejectDelay: Number of seconds, an integer >= 0
463
464 @param unittest: Turns off certain validations, for use in unit testing.
465 @type unittest: Boolean true/false
466
467 @raise ValueError: If the device is not valid for some reason.
468 @raise ValueError: If the SCSI id is not in a valid form.
469 @raise ValueError: If the drive speed is not an integer >= 1.
470 @raise IOError: If device properties could not be read for some reason.
471 """
472 self._image = None
473 self._device = validateDevice(device, unittest)
474 self._scsiId = validateScsiId(scsiId)
475 self._driveSpeed = validateDriveSpeed(driveSpeed)
476 self._media = MediaDefinition(mediaType)
477 self._noEject = noEject
478 self._refreshMediaDelay = refreshMediaDelay
479 self._ejectDelay = ejectDelay
480 if not unittest:
481 (self._deviceType,
482 self._deviceVendor,
483 self._deviceId,
484 self._deviceBufferSize,
485 self._deviceSupportsMulti,
486 self._deviceHasTray,
487 self._deviceCanEject) = self._retrieveProperties()
488
489
490
491
492
493
495 """
496 Property target used to get the device value.
497 """
498 return self._device
499
501 """
502 Property target used to get the SCSI id value.
503 """
504 return self._scsiId
505
507 """
508 Property target used to get the hardware id value.
509 """
510 if self._scsiId is None:
511 return self._device
512 return self._scsiId
513
515 """
516 Property target used to get the drive speed.
517 """
518 return self._driveSpeed
519
525
527 """
528 Property target used to get the device type.
529 """
530 return self._deviceType
531
533 """
534 Property target used to get the device vendor.
535 """
536 return self._deviceVendor
537
539 """
540 Property target used to get the device id.
541 """
542 return self._deviceId
543
545 """
546 Property target used to get the device buffer size.
547 """
548 return self._deviceBufferSize
549
551 """
552 Property target used to get the device-support-multi flag.
553 """
554 return self._deviceSupportsMulti
555
557 """
558 Property target used to get the device-has-tray flag.
559 """
560 return self._deviceHasTray
561
563 """
564 Property target used to get the device-can-eject flag.
565 """
566 return self._deviceCanEject
567
573
575 """
576 Property target used to get the configured eject delay, in seconds.
577 """
578 return self._ejectDelay
579
580 device = property(_getDevice, None, None, doc="Filesystem device name for this writer.")
581 scsiId = property(_getScsiId, None, None, doc="SCSI id for the device, in the form C{[<method>:]scsibus,target,lun}.")
582 hardwareId = property(_getHardwareId, None, None, doc="Hardware id for this writer, either SCSI id or device path.")
583 driveSpeed = property(_getDriveSpeed, None, None, doc="Speed at which the drive writes.")
584 media = property(_getMedia, None, None, doc="Definition of media that is expected to be in the device.")
585 deviceType = property(_getDeviceType, None, None, doc="Type of the device, as returned from C{cdrecord -prcap}.")
586 deviceVendor = property(_getDeviceVendor, None, None, doc="Vendor of the device, as returned from C{cdrecord -prcap}.")
587 deviceId = property(_getDeviceId, None, None, doc="Device identification, as returned from C{cdrecord -prcap}.")
588 deviceBufferSize = property(_getDeviceBufferSize, None, None, doc="Size of the device's write buffer, in bytes.")
589 deviceSupportsMulti = property(_getDeviceSupportsMulti, None, None, doc="Indicates whether device supports multisession discs.")
590 deviceHasTray = property(_getDeviceHasTray, None, None, doc="Indicates whether the device has a media tray.")
591 deviceCanEject = property(_getDeviceCanEject, None, None, doc="Indicates whether the device supports ejecting its media.")
592 refreshMediaDelay = property(_getRefreshMediaDelay, None, None, doc="Refresh media delay, in seconds.")
593 ejectDelay = property(_getEjectDelay, None, None, doc="Eject delay, in seconds.")
594
595
596
597
598
599
601 """Indicates whether the media is rewritable per configuration."""
602 return self._media.rewritable
603
605 """
606 Retrieves properties for a device from C{cdrecord}.
607
608 The results are returned as a tuple of the object device attributes as
609 returned from L{_parsePropertiesOutput}: C{(deviceType, deviceVendor,
610 deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray,
611 deviceCanEject)}.
612
613 @return: Results tuple as described above.
614 @raise IOError: If there is a problem talking to the device.
615 """
616 args = CdWriter._buildPropertiesArgs(self.hardwareId)
617 command = resolveCommand(CDRECORD_COMMAND)
618 (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True)
619 if result != 0:
620 raise IOError("Error (%d) executing cdrecord command to get properties." % result)
621 return CdWriter._parsePropertiesOutput(output)
622
624 """
625 Retrieves capacity for the current media in terms of a C{MediaCapacity}
626 object.
627
628 If C{entireDisc} is passed in as C{True} the capacity will be for the
629 entire disc, as if it were to be rewritten from scratch. If the drive
630 does not support writing multisession discs or if C{useMulti} is passed
631 in as C{False}, the capacity will also be as if the disc were to be
632 rewritten from scratch, but the indicated boundaries value will be
633 C{None}. The same will happen if the disc cannot be read for some
634 reason. Otherwise, the capacity (including the boundaries) will
635 represent whatever space remains on the disc to be filled by future
636 sessions.
637
638 @param entireDisc: Indicates whether to return capacity for entire disc.
639 @type entireDisc: Boolean true/false
640
641 @param useMulti: Indicates whether a multisession disc should be assumed, if possible.
642 @type useMulti: Boolean true/false
643
644 @return: C{MediaCapacity} object describing the capacity of the media.
645 @raise IOError: If the media could not be read for some reason.
646 """
647 boundaries = self._getBoundaries(entireDisc, useMulti)
648 return CdWriter._calculateCapacity(self._media, boundaries)
649
651 """
652 Gets the ISO boundaries for the media.
653
654 If C{entireDisc} is passed in as C{True} the boundaries will be C{None},
655 as if the disc were to be rewritten from scratch. If the drive does not
656 support writing multisession discs, the returned value will be C{None}.
657 The same will happen if the disc can't be read for some reason.
658 Otherwise, the returned value will be represent the boundaries of the
659 disc's current contents.
660
661 The results are returned as a tuple of (lower, upper) as needed by the
662 C{IsoImage} class. Note that these values are in terms of ISO sectors,
663 not bytes. Clients should generally consider the boundaries value
664 opaque, however.
665
666 @param entireDisc: Indicates whether to return capacity for entire disc.
667 @type entireDisc: Boolean true/false
668
669 @param useMulti: Indicates whether a multisession disc should be assumed, if possible.
670 @type useMulti: Boolean true/false
671
672 @return: Boundaries tuple or C{None}, as described above.
673 @raise IOError: If the media could not be read for some reason.
674 """
675 if not self._deviceSupportsMulti:
676 logger.debug("Device does not support multisession discs; returning boundaries None.")
677 return None
678 elif not useMulti:
679 logger.debug("Use multisession flag is False; returning boundaries None.")
680 return None
681 elif entireDisc:
682 logger.debug("Entire disc flag is True; returning boundaries None.")
683 return None
684 else:
685 args = CdWriter._buildBoundariesArgs(self.hardwareId)
686 command = resolveCommand(CDRECORD_COMMAND)
687 (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True)
688 if result != 0:
689 logger.debug("Error (%d) executing cdrecord command to get capacity." % result)
690 logger.warn("Unable to read disc (might not be initialized); returning boundaries of None.")
691 return None
692 boundaries = CdWriter._parseBoundariesOutput(output)
693 if boundaries is None:
694 logger.debug("Returning disc boundaries: None")
695 else:
696 logger.debug("Returning disc boundaries: (%d, %d)" % (boundaries[0], boundaries[1]))
697 return boundaries
698
699 @staticmethod
701 """
702 Calculates capacity for the media in terms of boundaries.
703
704 If C{boundaries} is C{None} or the lower bound is 0 (zero), then the
705 capacity will be for the entire disc minus the initial lead in.
706 Otherwise, capacity will be as if the caller wanted to add an additional
707 session to the end of the existing data on the disc.
708
709 @param media: MediaDescription object describing the media capacity.
710 @param boundaries: Session boundaries as returned from L{_getBoundaries}.
711
712 @return: C{MediaCapacity} object describing the capacity of the media.
713 """
714 if boundaries is None or boundaries[1] == 0:
715 logger.debug("Capacity calculations are based on a complete disc rewrite.")
716 sectorsAvailable = media.capacity - media.initialLeadIn
717 if sectorsAvailable < 0: sectorsAvailable = 0
718 bytesUsed = 0
719 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES)
720 else:
721 logger.debug("Capacity calculations are based on a new ISO session.")
722 sectorsAvailable = media.capacity - boundaries[1] - media.leadIn
723 if sectorsAvailable < 0: sectorsAvailable = 0
724 bytesUsed = convertSize(boundaries[1], UNIT_SECTORS, UNIT_BYTES)
725 bytesAvailable = convertSize(sectorsAvailable, UNIT_SECTORS, UNIT_BYTES)
726 logger.debug("Used [%s], available [%s]." % (displayBytes(bytesUsed), displayBytes(bytesAvailable)))
727 return MediaCapacity(bytesUsed, bytesAvailable, boundaries)
728
729
730
731
732
733
735 """
736 Initializes the writer's associated ISO image.
737
738 This method initializes the C{image} instance variable so that the caller
739 can use the C{addImageEntry} method. Once entries have been added, the
740 C{writeImage} method can be called with no arguments.
741
742 @param newDisc: Indicates whether the disc should be re-initialized
743 @type newDisc: Boolean true/false.
744
745 @param tmpdir: Temporary directory to use if needed
746 @type tmpdir: String representing a directory path on disk
747
748 @param mediaLabel: Media label to be applied to the image, if any
749 @type mediaLabel: String, no more than 25 characters long
750 """
751 self._image = _ImageProperties()
752 self._image.newDisc = newDisc
753 self._image.tmpdir = encodePath(tmpdir)
754 self._image.mediaLabel = mediaLabel
755 self._image.entries = {}
756
757 - def addImageEntry(self, path, graftPoint):
758 """
759 Adds a filepath entry to the writer's associated ISO image.
760
761 The contents of the filepath -- but not the path itself -- will be added
762 to the image at the indicated graft point. If you don't want to use a
763 graft point, just pass C{None}.
764
765 @note: Before calling this method, you must call L{initializeImage}.
766
767 @param path: File or directory to be added to the image
768 @type path: String representing a path on disk
769
770 @param graftPoint: Graft point to be used when adding this entry
771 @type graftPoint: String representing a graft point path, as described above
772
773 @raise ValueError: If initializeImage() was not previously called
774 """
775 if self._image is None:
776 raise ValueError("Must call initializeImage() before using this method.")
777 if not os.path.exists(path):
778 raise ValueError("Path [%s] does not exist." % path)
779 self._image.entries[path] = graftPoint
780
782 """
783 Resets (overrides) the newDisc flag on the internal image.
784 @param newDisc: New disc flag to set
785 @raise ValueError: If initializeImage() was not previously called
786 """
787 if self._image is None:
788 raise ValueError("Must call initializeImage() before using this method.")
789 self._image.newDisc = newDisc
790
792 """
793 Gets the estimated size of the image associated with the writer.
794 @return: Estimated size of the image, in bytes.
795 @raise IOError: If there is a problem calling C{mkisofs}.
796 @raise ValueError: If initializeImage() was not previously called
797 """
798 if self._image is None:
799 raise ValueError("Must call initializeImage() before using this method.")
800 image = IsoImage()
801 for path in self._image.entries.keys():
802 image.addEntry(path, self._image.entries[path], override=False, contentsOnly=True)
803 return image.getEstimatedSize()
804
805
806
807
808
809
811 """
812 Opens the device's tray and leaves it open.
813
814 This only works if the device has a tray and supports ejecting its media.
815 We have no way to know if the tray is currently open or closed, so we
816 just send the appropriate command and hope for the best. If the device
817 does not have a tray or does not support ejecting its media, then we do
818 nothing.
819
820 If the writer was constructed with C{noEject=True}, then this is a no-op.
821
822 Starting with Debian wheezy on my backup hardware, I started seeing
823 consistent problems with the eject command. I couldn't tell whether
824 these problems were due to the device management system or to the new
825 kernel (3.2.0). Initially, I saw simple eject failures, possibly because
826 I was opening and closing the tray too quickly. I worked around that
827 behavior with the new ejectDelay flag.
828
829 Later, I sometimes ran into issues after writing an image to a disc:
830 eject would give errors like "unable to eject, last error: Inappropriate
831 ioctl for device". Various sources online (like Ubuntu bug #875543)
832 suggested that the drive was being locked somehow, and that the
833 workaround was to run 'eject -i off' to unlock it. Sure enough, that
834 fixed the problem for me, so now it's a normal error-handling strategy.
835
836 @raise IOError: If there is an error talking to the device.
837 """
838 if not self._noEject:
839 if self._deviceHasTray and self._deviceCanEject:
840 args = CdWriter._buildOpenTrayArgs(self._device)
841 result = executeCommand(EJECT_COMMAND, args)[0]
842 if result != 0:
843 logger.debug("Eject failed; attempting kludge of unlocking the tray before retrying.")
844 self.unlockTray()
845 result = executeCommand(EJECT_COMMAND, args)[0]
846 if result != 0:
847 raise IOError("Error (%d) executing eject command to open tray (failed even after unlocking tray)." % result)
848 logger.debug("Kludge was apparently successful.")
849 if self.ejectDelay is not None:
850 logger.debug("Per configuration, sleeping %d seconds after opening tray." % self.ejectDelay)
851 time.sleep(self.ejectDelay)
852
863
865 """
866 Closes the device's tray.
867
868 This only works if the device has a tray and supports ejecting its media.
869 We have no way to know if the tray is currently open or closed, so we
870 just send the appropriate command and hope for the best. If the device
871 does not have a tray or does not support ejecting its media, then we do
872 nothing.
873
874 If the writer was constructed with C{noEject=True}, then this is a no-op.
875
876 @raise IOError: If there is an error talking to the device.
877 """
878 if not self._noEject:
879 if self._deviceHasTray and self._deviceCanEject:
880 args = CdWriter._buildCloseTrayArgs(self._device)
881 command = resolveCommand(EJECT_COMMAND)
882 result = executeCommand(command, args)[0]
883 if result != 0:
884 raise IOError("Error (%d) executing eject command to close tray." % result)
885
912
913 - def writeImage(self, imagePath=None, newDisc=False, writeMulti=True):
914 """
915 Writes an ISO image to the media in the device.
916
917 If C{newDisc} is passed in as C{True}, we assume that the entire disc
918 will be overwritten, and the media will be blanked before writing it if
919 possible (i.e. if the media is rewritable).
920
921 If C{writeMulti} is passed in as C{True}, then a multisession disc will
922 be written if possible (i.e. if the drive supports writing multisession
923 discs).
924
925 if C{imagePath} is passed in as C{None}, then the existing image
926 configured with C{initializeImage} will be used. Under these
927 circumstances, the passed-in C{newDisc} flag will be ignored.
928
929 By default, we assume that the disc can be written multisession and that
930 we should append to the current contents of the disc. In any case, the
931 ISO image must be generated appropriately (i.e. must take into account
932 any existing session boundaries, etc.)
933
934 @param imagePath: Path to an ISO image on disk, or C{None} to use writer's image
935 @type imagePath: String representing a path on disk
936
937 @param newDisc: Indicates whether the entire disc will overwritten.
938 @type newDisc: Boolean true/false.
939
940 @param writeMulti: Indicates whether a multisession disc should be written, if possible.
941 @type writeMulti: Boolean true/false
942
943 @raise ValueError: If the image path is not absolute.
944 @raise ValueError: If some path cannot be encoded properly.
945 @raise IOError: If the media could not be written to for some reason.
946 @raise ValueError: If no image is passed in and initializeImage() was not previously called
947 """
948 if imagePath is None:
949 if self._image is None:
950 raise ValueError("Must call initializeImage() before using this method with no image path.")
951 try:
952 imagePath = self._createImage()
953 self._writeImage(imagePath, writeMulti, self._image.newDisc)
954 finally:
955 if imagePath is not None and os.path.exists(imagePath):
956 try: os.unlink(imagePath)
957 except: pass
958 else:
959 imagePath = encodePath(imagePath)
960 if not os.path.isabs(imagePath):
961 raise ValueError("Image path must be absolute.")
962 self._writeImage(imagePath, writeMulti, newDisc)
963
965 """
966 Creates an ISO image based on configuration in self._image.
967 @return: Path to the newly-created ISO image on disk.
968 @raise IOError: If there is an error writing the image to disk.
969 @raise ValueError: If there are no filesystem entries in the image
970 @raise ValueError: If a path cannot be encoded properly.
971 """
972 path = None
973 capacity = self.retrieveCapacity(entireDisc=self._image.newDisc)
974 image = IsoImage(self.device, capacity.boundaries)
975 image.volumeId = self._image.mediaLabel
976 for key in self._image.entries.keys():
977 image.addEntry(key, self._image.entries[key], override=False, contentsOnly=True)
978 size = image.getEstimatedSize()
979 logger.info("Image size will be %s." % displayBytes(size))
980 available = capacity.bytesAvailable
981 logger.debug("Media capacity: %s" % displayBytes(available))
982 if size > available:
983 logger.error("Image [%s] does not fit in available capacity [%s]." % (displayBytes(size), displayBytes(available)))
984 raise IOError("Media does not contain enough capacity to store image.")
985 try:
986 (handle, path) = tempfile.mkstemp(dir=self._image.tmpdir)
987 try: os.close(handle)
988 except: pass
989 image.writeImage(path)
990 logger.debug("Completed creating image [%s]." % path)
991 return path
992 except Exception, e:
993 if path is not None and os.path.exists(path):
994 try: os.unlink(path)
995 except: pass
996 raise e
997
998 - def _writeImage(self, imagePath, writeMulti, newDisc):
999 """
1000 Write an ISO image to disc using cdrecord.
1001 The disc is blanked first if C{newDisc} is C{True}.
1002 @param imagePath: Path to an ISO image on disk
1003 @param writeMulti: Indicates whether a multisession disc should be written, if possible.
1004 @param newDisc: Indicates whether the entire disc will overwritten.
1005 """
1006 if newDisc:
1007 self._blankMedia()
1008 args = CdWriter._buildWriteArgs(self.hardwareId, imagePath, self._driveSpeed, writeMulti and self._deviceSupportsMulti)
1009 command = resolveCommand(CDRECORD_COMMAND)
1010 result = executeCommand(command, args)[0]
1011 if result != 0:
1012 raise IOError("Error (%d) executing command to write disc." % result)
1013 self.refreshMedia()
1014
1027
1028
1029
1030
1031
1032
1033 @staticmethod
1035 """
1036 Parses the output from a C{cdrecord} properties command.
1037
1038 The C{output} parameter should be a list of strings as returned from
1039 C{executeCommand} for a C{cdrecord} command with arguments as from
1040 C{_buildPropertiesArgs}. The list of strings will be parsed to yield
1041 information about the properties of the device.
1042
1043 The output is expected to be a huge long list of strings. Unfortunately,
1044 the strings aren't in a completely regular format. However, the format
1045 of individual lines seems to be regular enough that we can look for
1046 specific values. Two kinds of parsing take place: one kind of parsing
1047 picks out out specific values like the device id, device vendor, etc.
1048 The other kind of parsing just sets a boolean flag C{True} if a matching
1049 line is found. All of the parsing is done with regular expressions.
1050
1051 Right now, pretty much nothing in the output is required and we should
1052 parse an empty document successfully (albeit resulting in a device that
1053 can't eject, doesn't have a tray and doesnt't support multisession
1054 discs). I had briefly considered erroring out if certain lines weren't
1055 found or couldn't be parsed, but that seems like a bad idea given that
1056 most of the information is just for reference.
1057
1058 The results are returned as a tuple of the object device attributes:
1059 C{(deviceType, deviceVendor, deviceId, deviceBufferSize,
1060 deviceSupportsMulti, deviceHasTray, deviceCanEject)}.
1061
1062 @param output: Output from a C{cdrecord -prcap} command.
1063
1064 @return: Results tuple as described above.
1065 @raise IOError: If there is problem parsing the output.
1066 """
1067 deviceType = None
1068 deviceVendor = None
1069 deviceId = None
1070 deviceBufferSize = None
1071 deviceSupportsMulti = False
1072 deviceHasTray = False
1073 deviceCanEject = False
1074 typePattern = re.compile(r"(^Device type\s*:\s*)(.*)(\s*)(.*$)")
1075 vendorPattern = re.compile(r"(^Vendor_info\s*:\s*'\s*)(.*?)(\s*')(.*$)")
1076 idPattern = re.compile(r"(^Identifikation\s*:\s*'\s*)(.*?)(\s*')(.*$)")
1077 bufferPattern = re.compile(r"(^\s*Buffer size in KB:\s*)(.*?)(\s*$)")
1078 multiPattern = re.compile(r"^\s*Does read multi-session.*$")
1079 trayPattern = re.compile(r"^\s*Loading mechanism type: tray.*$")
1080 ejectPattern = re.compile(r"^\s*Does support ejection.*$")
1081 for line in output:
1082 if typePattern.search(line):
1083 deviceType = typePattern.search(line).group(2)
1084 logger.info("Device type is [%s]." % deviceType)
1085 elif vendorPattern.search(line):
1086 deviceVendor = vendorPattern.search(line).group(2)
1087 logger.info("Device vendor is [%s]." % deviceVendor)
1088 elif idPattern.search(line):
1089 deviceId = idPattern.search(line).group(2)
1090 logger.info("Device id is [%s]." % deviceId)
1091 elif bufferPattern.search(line):
1092 try:
1093 sectors = int(bufferPattern.search(line).group(2))
1094 deviceBufferSize = convertSize(sectors, UNIT_KBYTES, UNIT_BYTES)
1095 logger.info("Device buffer size is [%d] bytes." % deviceBufferSize)
1096 except TypeError: pass
1097 elif multiPattern.search(line):
1098 deviceSupportsMulti = True
1099 logger.info("Device does support multisession discs.")
1100 elif trayPattern.search(line):
1101 deviceHasTray = True
1102 logger.info("Device has a tray.")
1103 elif ejectPattern.search(line):
1104 deviceCanEject = True
1105 logger.info("Device can eject its media.")
1106 return (deviceType, deviceVendor, deviceId, deviceBufferSize, deviceSupportsMulti, deviceHasTray, deviceCanEject)
1107
1108 @staticmethod
1110 """
1111 Parses the output from a C{cdrecord} capacity command.
1112
1113 The C{output} parameter should be a list of strings as returned from
1114 C{executeCommand} for a C{cdrecord} command with arguments as from
1115 C{_buildBoundaryArgs}. The list of strings will be parsed to yield
1116 information about the capacity of the media in the device.
1117
1118 Basically, we expect the list of strings to include just one line, a pair
1119 of values. There isn't supposed to be whitespace, but we allow it anyway
1120 in the regular expression. Any lines below the one line we parse are
1121 completely ignored. It would be a good idea to ignore C{stderr} when
1122 executing the C{cdrecord} command that generates output for this method,
1123 because sometimes C{cdrecord} spits out kernel warnings about the actual
1124 output.
1125
1126 The results are returned as a tuple of (lower, upper) as needed by the
1127 C{IsoImage} class. Note that these values are in terms of ISO sectors,
1128 not bytes. Clients should generally consider the boundaries value
1129 opaque, however.
1130
1131 @note: If the boundaries output can't be parsed, we return C{None}.
1132
1133 @param output: Output from a C{cdrecord -msinfo} command.
1134
1135 @return: Boundaries tuple as described above.
1136 @raise IOError: If there is problem parsing the output.
1137 """
1138 if len(output) < 1:
1139 logger.warn("Unable to read disc (might not be initialized); returning full capacity.")
1140 return None
1141 boundaryPattern = re.compile(r"(^\s*)([0-9]*)(\s*,\s*)([0-9]*)(\s*$)")
1142 parsed = boundaryPattern.search(output[0])
1143 if not parsed:
1144 raise IOError("Unable to parse output of boundaries command.")
1145 try:
1146 boundaries = ( int(parsed.group(2)), int(parsed.group(4)) )
1147 except TypeError:
1148 raise IOError("Unable to parse output of boundaries command.")
1149 return boundaries
1150
1151
1152
1153
1154
1155
1156 @staticmethod
1158 """
1159 Builds a list of arguments to be passed to a C{eject} command.
1160
1161 The arguments will cause the C{eject} command to open the tray and
1162 eject the media. No validation is done by this method as to whether
1163 this action actually makes sense.
1164
1165 @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}.
1166
1167 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1168 """
1169 args = []
1170 args.append(device)
1171 return args
1172
1173 @staticmethod
1175 """
1176 Builds a list of arguments to be passed to a C{eject} command.
1177
1178 The arguments will cause the C{eject} command to unlock the tray.
1179
1180 @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}.
1181
1182 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1183 """
1184 args = []
1185 args.append("-i")
1186 args.append("off")
1187 args.append(device)
1188 return args
1189
1190 @staticmethod
1192 """
1193 Builds a list of arguments to be passed to a C{eject} command.
1194
1195 The arguments will cause the C{eject} command to close the tray and reload
1196 the media. No validation is done by this method as to whether this
1197 action actually makes sense.
1198
1199 @param device: Filesystem device name for this writer, i.e. C{/dev/cdrw}.
1200
1201 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1202 """
1203 args = []
1204 args.append("-t")
1205 args.append(device)
1206 return args
1207
1208 @staticmethod
1210 """
1211 Builds a list of arguments to be passed to a C{cdrecord} command.
1212
1213 The arguments will cause the C{cdrecord} command to ask the device
1214 for a list of its capacities via the C{-prcap} switch.
1215
1216 @param hardwareId: Hardware id for the device (either SCSI id or device path)
1217
1218 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1219 """
1220 args = []
1221 args.append("-prcap")
1222 args.append("dev=%s" % hardwareId)
1223 return args
1224
1225 @staticmethod
1227 """
1228 Builds a list of arguments to be passed to a C{cdrecord} command.
1229
1230 The arguments will cause the C{cdrecord} command to ask the device for
1231 the current multisession boundaries of the media using the C{-msinfo}
1232 switch.
1233
1234 @param hardwareId: Hardware id for the device (either SCSI id or device path)
1235
1236 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1237 """
1238 args = []
1239 args.append("-msinfo")
1240 args.append("dev=%s" % hardwareId)
1241 return args
1242
1243 @staticmethod
1245 """
1246 Builds a list of arguments to be passed to a C{cdrecord} command.
1247
1248 The arguments will cause the C{cdrecord} command to blank the media in
1249 the device identified by C{hardwareId}. No validation is done by this method
1250 as to whether the action makes sense (i.e. to whether the media even can
1251 be blanked).
1252
1253 @param hardwareId: Hardware id for the device (either SCSI id or device path)
1254 @param driveSpeed: Speed at which the drive writes.
1255
1256 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1257 """
1258 args = []
1259 args.append("-v")
1260 args.append("blank=fast")
1261 if driveSpeed is not None:
1262 args.append("speed=%d" % driveSpeed)
1263 args.append("dev=%s" % hardwareId)
1264 return args
1265
1266 @staticmethod
1267 - def _buildWriteArgs(hardwareId, imagePath, driveSpeed=None, writeMulti=True):
1268 """
1269 Builds a list of arguments to be passed to a C{cdrecord} command.
1270
1271 The arguments will cause the C{cdrecord} command to write the indicated
1272 ISO image (C{imagePath}) to the media in the device identified by
1273 C{hardwareId}. The C{writeMulti} argument controls whether to write a
1274 multisession disc. No validation is done by this method as to whether
1275 the action makes sense (i.e. to whether the device even can write
1276 multisession discs, for instance).
1277
1278 @param hardwareId: Hardware id for the device (either SCSI id or device path)
1279 @param imagePath: Path to an ISO image on disk.
1280 @param driveSpeed: Speed at which the drive writes.
1281 @param writeMulti: Indicates whether to write a multisession disc.
1282
1283 @return: List suitable for passing to L{util.executeCommand} as C{args}.
1284 """
1285 args = []
1286 args.append("-v")
1287 if driveSpeed is not None:
1288 args.append("speed=%d" % driveSpeed)
1289 args.append("dev=%s" % hardwareId)
1290 if writeMulti:
1291 args.append("-multi")
1292 args.append("-data")
1293 args.append(imagePath)
1294 return args
1295