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 utilities related to image writers.
41 @author: Kenneth J. Pronovici <pronovic@ieee.org>
42 """
43
44
45
46
47
48
49
50 import os
51 import re
52 import logging
53
54
55 from CedarBackup2.util import resolveCommand, executeCommand
56 from CedarBackup2.util import convertSize, UNIT_BYTES, UNIT_SECTORS, encodePath
57
58
59
60
61
62
63 logger = logging.getLogger("CedarBackup2.log.writers.util")
64
65 MKISOFS_COMMAND = [ "mkisofs", ]
66 VOLNAME_COMMAND = [ "volname", ]
67
68
69
70
71
72
73
74
75
76
77 -def validateDevice(device, unittest=False):
78 """
79 Validates a configured device.
80 The device must be an absolute path, must exist, and must be writable.
81 The unittest flag turns off validation of the device on disk.
82 @param device: Filesystem device path.
83 @param unittest: Indicates whether we're unit testing.
84 @return: Device as a string, for instance C{"/dev/cdrw"}
85 @raise ValueError: If the device value is invalid.
86 @raise ValueError: If some path cannot be encoded properly.
87 """
88 if device is None:
89 raise ValueError("Device must be filled in.")
90 device = encodePath(device)
91 if not os.path.isabs(device):
92 raise ValueError("Backup device must be an absolute path.")
93 if not unittest and not os.path.exists(device):
94 raise ValueError("Backup device must exist on disk.")
95 if not unittest and not os.access(device, os.W_OK):
96 raise ValueError("Backup device is not writable by the current user.")
97 return device
98
105 """
106 Validates a SCSI id string.
107 SCSI id must be a string in the form C{[<method>:]scsibus,target,lun}.
108 For Mac OS X (Darwin), we also accept the form C{IO.*Services[/N]}.
109 @note: For consistency, if C{None} is passed in, C{None} will be returned.
110 @param scsiId: SCSI id for the device.
111 @return: SCSI id as a string, for instance C{"ATA:1,0,0"}
112 @raise ValueError: If the SCSI id string is invalid.
113 """
114 if scsiId is not None:
115 pattern = re.compile(r"^\s*(.*:)?\s*[0-9][0-9]*\s*,\s*[0-9][0-9]*\s*,\s*[0-9][0-9]*\s*$")
116 if not pattern.search(scsiId):
117 pattern = re.compile(r"^\s*IO.*Services(\/[0-9][0-9]*)?\s*$")
118 if not pattern.search(scsiId):
119 raise ValueError("SCSI id is not in a valid form.")
120 return scsiId
121
128 """
129 Validates a drive speed value.
130 Drive speed must be an integer which is >= 1.
131 @note: For consistency, if C{None} is passed in, C{None} will be returned.
132 @param driveSpeed: Speed at which the drive writes.
133 @return: Drive speed as an integer
134 @raise ValueError: If the drive speed value is invalid.
135 """
136 if driveSpeed is None:
137 return None
138 try:
139 intSpeed = int(driveSpeed)
140 except TypeError:
141 raise ValueError("Drive speed must be an integer >= 1.")
142 if intSpeed < 1:
143 raise ValueError("Drive speed must an integer >= 1.")
144 return intSpeed
145
170
171
172
173
174
175
176 -class IsoImage(object):
177
178
179
180
181
182 """
183 Represents an ISO filesystem image.
184
185 Summary
186 =======
187
188 This object represents an ISO 9660 filesystem image. It is implemented
189 in terms of the C{mkisofs} program, which has been ported to many
190 operating systems and platforms. A "sensible subset" of the C{mkisofs}
191 functionality is made available through the public interface, allowing
192 callers to set a variety of basic options such as publisher id,
193 application id, etc. as well as specify exactly which files and
194 directories they want included in their image.
195
196 By default, the image is created using the Rock Ridge protocol (using the
197 C{-r} option to C{mkisofs}) because Rock Ridge discs are generally more
198 useful on UN*X filesystems than standard ISO 9660 images. However,
199 callers can fall back to the default C{mkisofs} functionality by setting
200 the C{useRockRidge} instance variable to C{False}. Note, however, that
201 this option is not well-tested.
202
203 Where Files and Directories are Placed in the Image
204 ===================================================
205
206 Although this class is implemented in terms of the C{mkisofs} program,
207 its standard "image contents" semantics are slightly different than the original
208 C{mkisofs} semantics. The difference is that files and directories are
209 added to the image with some additional information about their source
210 directory kept intact.
211
212 As an example, suppose you add the file C{/etc/profile} to your image and
213 you do not configure a graft point. The file C{/profile} will be created
214 in the image. The behavior for directories is similar. For instance,
215 suppose that you add C{/etc/X11} to the image and do not configure a
216 graft point. In this case, the directory C{/X11} will be created in the
217 image, even if the original C{/etc/X11} directory is empty. I{This
218 behavior differs from the standard C{mkisofs} behavior!}
219
220 If a graft point is configured, it will be used to modify the point at
221 which a file or directory is added into an image. Using the examples
222 from above, let's assume you set a graft point of C{base} when adding
223 C{/etc/profile} and C{/etc/X11} to your image. In this case, the file
224 C{/base/profile} and the directory C{/base/X11} would be added to the
225 image.
226
227 I feel that this behavior is more consistent than the original C{mkisofs}
228 behavior. However, to be fair, it is not quite as flexible, and some
229 users might not like it. For this reason, the C{contentsOnly} parameter
230 to the L{addEntry} method can be used to revert to the original behavior
231 if desired.
232
233 @sort: __init__, addEntry, getEstimatedSize, _getEstimatedSize, writeImage,
234 _buildDirEntries _buildGeneralArgs, _buildSizeArgs, _buildWriteArgs,
235 device, boundaries, graftPoint, useRockRidge, applicationId,
236 biblioFile, publisherId, preparerId, volumeId
237 """
238
239
240
241
242
243 - def __init__(self, device=None, boundaries=None, graftPoint=None):
244 """
245 Initializes an empty ISO image object.
246
247 Only the most commonly-used configuration items can be set using this
248 constructor. If you have a need to change the others, do so immediately
249 after creating your object.
250
251 The device and boundaries values are both required in order to write
252 multisession discs. If either is missing or C{None}, a multisession disc
253 will not be written. The boundaries tuple is in terms of ISO sectors, as
254 built by an image writer class and returned in a L{writer.MediaCapacity}
255 object.
256
257 @param device: Name of the device that the image will be written to
258 @type device: Either be a filesystem path or a SCSI address
259
260 @param boundaries: Session boundaries as required by C{mkisofs}
261 @type boundaries: Tuple C{(last_sess_start,next_sess_start)} as returned from C{cdrecord -msinfo}, or C{None}
262
263 @param graftPoint: Default graft point for this page.
264 @type graftPoint: String representing a graft point path (see L{addEntry}).
265 """
266 self._device = None
267 self._boundaries = None
268 self._graftPoint = None
269 self._useRockRidge = True
270 self._applicationId = None
271 self._biblioFile = None
272 self._publisherId = None
273 self._preparerId = None
274 self._volumeId = None
275 self.entries = { }
276 self.device = device
277 self.boundaries = boundaries
278 self.graftPoint = graftPoint
279 self.useRockRidge = True
280 self.applicationId = None
281 self.biblioFile = None
282 self.publisherId = None
283 self.preparerId = None
284 self.volumeId = None
285 logger.debug("Created new ISO image object.")
286
287
288
289
290
291
293 """
294 Property target used to set the device value.
295 If not C{None}, the value can be either an absolute path or a SCSI id.
296 @raise ValueError: If the value is not valid
297 """
298 try:
299 if value is None:
300 self._device = None
301 else:
302 if os.path.isabs(value):
303 self._device = value
304 else:
305 self._device = validateScsiId(value)
306 except ValueError:
307 raise ValueError("Device must either be an absolute path or a valid SCSI id.")
308
310 """
311 Property target used to get the device value.
312 """
313 return self._device
314
316 """
317 Property target used to set the boundaries tuple.
318 If not C{None}, the value must be a tuple of two integers.
319 @raise ValueError: If the tuple values are not integers.
320 @raise IndexError: If the tuple does not contain enough elements.
321 """
322 if value is None:
323 self._boundaries = None
324 else:
325 self._boundaries = (int(value[0]), int(value[1]))
326
328 """
329 Property target used to get the boundaries value.
330 """
331 return self._boundaries
332
334 """
335 Property target used to set the graft point.
336 The value must be a non-empty string if it is not C{None}.
337 @raise ValueError: If the value is an empty string.
338 """
339 if value is not None:
340 if len(value) < 1:
341 raise ValueError("The graft point must be a non-empty string.")
342 self._graftPoint = value
343
345 """
346 Property target used to get the graft point.
347 """
348 return self._graftPoint
349
351 """
352 Property target used to set the use RockRidge flag.
353 No validations, but we normalize the value to C{True} or C{False}.
354 """
355 if value:
356 self._useRockRidge = True
357 else:
358 self._useRockRidge = False
359
361 """
362 Property target used to get the use RockRidge flag.
363 """
364 return self._useRockRidge
365
367 """
368 Property target used to set the application id.
369 The value must be a non-empty string if it is not C{None}.
370 @raise ValueError: If the value is an empty string.
371 """
372 if value is not None:
373 if len(value) < 1:
374 raise ValueError("The application id must be a non-empty string.")
375 self._applicationId = value
376
378 """
379 Property target used to get the application id.
380 """
381 return self._applicationId
382
384 """
385 Property target used to set the biblio file.
386 The value must be a non-empty string if it is not C{None}.
387 @raise ValueError: If the value is an empty string.
388 """
389 if value is not None:
390 if len(value) < 1:
391 raise ValueError("The biblio file must be a non-empty string.")
392 self._biblioFile = value
393
395 """
396 Property target used to get the biblio file.
397 """
398 return self._biblioFile
399
401 """
402 Property target used to set the publisher id.
403 The value must be a non-empty string if it is not C{None}.
404 @raise ValueError: If the value is an empty string.
405 """
406 if value is not None:
407 if len(value) < 1:
408 raise ValueError("The publisher id must be a non-empty string.")
409 self._publisherId = value
410
412 """
413 Property target used to get the publisher id.
414 """
415 return self._publisherId
416
418 """
419 Property target used to set the preparer id.
420 The value must be a non-empty string if it is not C{None}.
421 @raise ValueError: If the value is an empty string.
422 """
423 if value is not None:
424 if len(value) < 1:
425 raise ValueError("The preparer id must be a non-empty string.")
426 self._preparerId = value
427
429 """
430 Property target used to get the preparer id.
431 """
432 return self._preparerId
433
435 """
436 Property target used to set the volume id.
437 The value must be a non-empty string if it is not C{None}.
438 @raise ValueError: If the value is an empty string.
439 """
440 if value is not None:
441 if len(value) < 1:
442 raise ValueError("The volume id must be a non-empty string.")
443 self._volumeId = value
444
446 """
447 Property target used to get the volume id.
448 """
449 return self._volumeId
450
451 device = property(_getDevice, _setDevice, None, "Device that image will be written to (device path or SCSI id).")
452 boundaries = property(_getBoundaries, _setBoundaries, None, "Session boundaries as required by C{mkisofs}.")
453 graftPoint = property(_getGraftPoint, _setGraftPoint, None, "Default image-wide graft point (see L{addEntry} for details).")
454 useRockRidge = property(_getUseRockRidge, _setUseRockRidge, None, "Indicates whether to use RockRidge (default is C{True}).")
455 applicationId = property(_getApplicationId, _setApplicationId, None, "Optionally specifies the ISO header application id value.")
456 biblioFile = property(_getBiblioFile, _setBiblioFile, None, "Optionally specifies the ISO bibliographic file name.")
457 publisherId = property(_getPublisherId, _setPublisherId, None, "Optionally specifies the ISO header publisher id value.")
458 preparerId = property(_getPreparerId, _setPreparerId, None, "Optionally specifies the ISO header preparer id value.")
459 volumeId = property(_getVolumeId, _setVolumeId, None, "Optionally specifies the ISO header volume id value.")
460
461
462
463
464
465
466 - def addEntry(self, path, graftPoint=None, override=False, contentsOnly=False):
467 """
468 Adds an individual file or directory into the ISO image.
469
470 The path must exist and must be a file or a directory. By default, the
471 entry will be placed into the image at the root directory, but this
472 behavior can be overridden using the C{graftPoint} parameter or instance
473 variable.
474
475 You can use the C{contentsOnly} behavior to revert to the "original"
476 C{mkisofs} behavior for adding directories, which is to add only the
477 items within the directory, and not the directory itself.
478
479 @note: Things get I{odd} if you try to add a directory to an image that
480 will be written to a multisession disc, and the same directory already
481 exists in an earlier session on that disc. Not all of the data gets
482 written. You really wouldn't want to do this anyway, I guess.
483
484 @note: An exception will be thrown if the path has already been added to
485 the image, unless the C{override} parameter is set to C{True}.
486
487 @note: The method C{graftPoints} parameter overrides the object-wide
488 instance variable. If neither the method parameter or object-wide value
489 is set, the path will be written at the image root. The graft point
490 behavior is determined by the value which is in effect I{at the time this
491 method is called}, so you I{must} set the object-wide value before
492 calling this method for the first time, or your image may not be
493 consistent.
494
495 @note: You I{cannot} use the local C{graftPoint} parameter to "turn off"
496 an object-wide instance variable by setting it to C{None}. Python's
497 default argument functionality buys us a lot, but it can't make this
498 method psychic. :)
499
500 @param path: File or directory to be added to the image
501 @type path: String representing a path on disk
502
503 @param graftPoint: Graft point to be used when adding this entry
504 @type graftPoint: String representing a graft point path, as described above
505
506 @param override: Override an existing entry with the same path.
507 @type override: Boolean true/false
508
509 @param contentsOnly: Add directory contents only (standard C{mkisofs} behavior).
510 @type contentsOnly: Boolean true/false
511
512 @raise ValueError: If path is not a file or directory, or does not exist.
513 @raise ValueError: If the path has already been added, and override is not set.
514 @raise ValueError: If a path cannot be encoded properly.
515 """
516 path = encodePath(path)
517 if not override:
518 if path in self.entries.keys():
519 raise ValueError("Path has already been added to the image.")
520 if os.path.islink(path):
521 raise ValueError("Path must not be a link.")
522 if os.path.isdir(path):
523 if graftPoint is not None:
524 if contentsOnly:
525 self.entries[path] = graftPoint
526 else:
527 self.entries[path] = os.path.join(graftPoint, os.path.basename(path))
528 elif self.graftPoint is not None:
529 if contentsOnly:
530 self.entries[path] = self.graftPoint
531 else:
532 self.entries[path] = os.path.join(self.graftPoint, os.path.basename(path))
533 else:
534 if contentsOnly:
535 self.entries[path] = None
536 else:
537 self.entries[path] = os.path.basename(path)
538 elif os.path.isfile(path):
539 if graftPoint is not None:
540 self.entries[path] = graftPoint
541 elif self.graftPoint is not None:
542 self.entries[path] = self.graftPoint
543 else:
544 self.entries[path] = None
545 else:
546 raise ValueError("Path must be a file or a directory.")
547
549 """
550 Returns the estimated size (in bytes) of the ISO image.
551
552 This is implemented via the C{-print-size} option to C{mkisofs}, so it
553 might take a bit of time to execute. However, the result is as accurate
554 as we can get, since it takes into account all of the ISO overhead, the
555 true cost of directories in the structure, etc, etc.
556
557 @return: Estimated size of the image, in bytes.
558
559 @raise IOError: If there is a problem calling C{mkisofs}.
560 @raise ValueError: If there are no filesystem entries in the image
561 """
562 if len(self.entries.keys()) == 0:
563 raise ValueError("Image does not contain any entries.")
564 return self._getEstimatedSize(self.entries)
565
567 """
568 Returns the estimated size (in bytes) for the passed-in entries dictionary.
569 @return: Estimated size of the image, in bytes.
570 @raise IOError: If there is a problem calling C{mkisofs}.
571 """
572 args = self._buildSizeArgs(entries)
573 command = resolveCommand(MKISOFS_COMMAND)
574 (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True)
575 if result != 0:
576 raise IOError("Error (%d) executing mkisofs command to estimate size." % result)
577 if len(output) != 1:
578 raise IOError("Unable to parse mkisofs output.")
579 try:
580 sectors = float(output[0])
581 size = convertSize(sectors, UNIT_SECTORS, UNIT_BYTES)
582 return size
583 except:
584 raise IOError("Unable to parse mkisofs output.")
585
587 """
588 Writes this image to disk using the image path.
589
590 @param imagePath: Path to write image out as
591 @type imagePath: String representing a path on disk
592
593 @raise IOError: If there is an error writing the image to disk.
594 @raise ValueError: If there are no filesystem entries in the image
595 @raise ValueError: If a path cannot be encoded properly.
596 """
597 imagePath = encodePath(imagePath)
598 if len(self.entries.keys()) == 0:
599 raise ValueError("Image does not contain any entries.")
600 args = self._buildWriteArgs(self.entries, imagePath)
601 command = resolveCommand(MKISOFS_COMMAND)
602 (result, output) = executeCommand(command, args, returnOutput=False)
603 if result != 0:
604 raise IOError("Error (%d) executing mkisofs command to build image." % result)
605
606
607
608
609
610
611 @staticmethod
613 """
614 Uses an entries dictionary to build a list of directory locations for use
615 by C{mkisofs}.
616
617 We build a list of entries that can be passed to C{mkisofs}. Each entry is
618 either raw (if no graft point was configured) or in graft-point form as
619 described above (if a graft point was configured). The dictionary keys
620 are the path names, and the values are the graft points, if any.
621
622 @param entries: Dictionary of image entries (i.e. self.entries)
623
624 @return: List of directory locations for use by C{mkisofs}
625 """
626 dirEntries = []
627 for key in entries.keys():
628 if entries[key] is None:
629 dirEntries.append(key)
630 else:
631 dirEntries.append("%s/=%s" % (entries[key].strip("/"), key))
632 return dirEntries
633
635 """
636 Builds a list of general arguments to be passed to a C{mkisofs} command.
637
638 The various instance variables (C{applicationId}, etc.) are filled into
639 the list of arguments if they are set.
640 By default, we will build a RockRidge disc. If you decide to change
641 this, think hard about whether you know what you're doing. This option
642 is not well-tested.
643
644 @return: List suitable for passing to L{util.executeCommand} as C{args}.
645 """
646 args = []
647 if self.applicationId is not None:
648 args.append("-A")
649 args.append(self.applicationId)
650 if self.biblioFile is not None:
651 args.append("-biblio")
652 args.append(self.biblioFile)
653 if self.publisherId is not None:
654 args.append("-publisher")
655 args.append(self.publisherId)
656 if self.preparerId is not None:
657 args.append("-p")
658 args.append(self.preparerId)
659 if self.volumeId is not None:
660 args.append("-V")
661 args.append(self.volumeId)
662 return args
663
665 """
666 Builds a list of arguments to be passed to a C{mkisofs} command.
667
668 The various instance variables (C{applicationId}, etc.) are filled into
669 the list of arguments if they are set. The command will be built to just
670 return size output (a simple count of sectors via the C{-print-size} option),
671 rather than an image file on disk.
672
673 By default, we will build a RockRidge disc. If you decide to change
674 this, think hard about whether you know what you're doing. This option
675 is not well-tested.
676
677 @param entries: Dictionary of image entries (i.e. self.entries)
678
679 @return: List suitable for passing to L{util.executeCommand} as C{args}.
680 """
681 args = self._buildGeneralArgs()
682 args.append("-print-size")
683 args.append("-graft-points")
684 if self.useRockRidge:
685 args.append("-r")
686 if self.device is not None and self.boundaries is not None:
687 args.append("-C")
688 args.append("%d,%d" % (self.boundaries[0], self.boundaries[1]))
689 args.append("-M")
690 args.append(self.device)
691 args.extend(self._buildDirEntries(entries))
692 return args
693
695 """
696 Builds a list of arguments to be passed to a C{mkisofs} command.
697
698 The various instance variables (C{applicationId}, etc.) are filled into
699 the list of arguments if they are set. The command will be built to write
700 an image to disk.
701
702 By default, we will build a RockRidge disc. If you decide to change
703 this, think hard about whether you know what you're doing. This option
704 is not well-tested.
705
706 @param entries: Dictionary of image entries (i.e. self.entries)
707
708 @param imagePath: Path to write image out as
709 @type imagePath: String representing a path on disk
710
711 @return: List suitable for passing to L{util.executeCommand} as C{args}.
712 """
713 args = self._buildGeneralArgs()
714 args.append("-graft-points")
715 if self.useRockRidge:
716 args.append("-r")
717 args.append("-o")
718 args.append(imagePath)
719 if self.device is not None and self.boundaries is not None:
720 args.append("-C")
721 args.append("%d,%d" % (self.boundaries[0], self.boundaries[1]))
722 args.append("-M")
723 args.append(self.device)
724 args.extend(self._buildDirEntries(entries))
725 return args
726