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 CedarBackup2.util import resolveCommand, executeCommand 
 55  from CedarBackup2.util import convertSize, UNIT_BYTES, UNIT_SECTORS, encodePath 
 56   
 57   
 58   
 59   
 60   
 61   
 62  logger = logging.getLogger("CedarBackup2.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 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(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(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 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