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