initial import code into git
[platform/upstream/mic.git] / micng / utils / fs_related.py
1 #
2 # fs.py : Filesystem related utilities and classes
3 #
4 # Copyright 2007, Red Hat  Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Library General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
19
20 import os
21 import sys
22 import errno
23 import stat
24 import subprocess
25 import random
26 import string
27 import logging
28 import time
29 import fcntl
30 import struct
31 import termios
32
33 from errors import *
34 from urlgrabber.grabber import URLGrabber
35 from urlgrabber.grabber import URLGrabError
36
37 def terminal_width(fd=1):
38     """ Get the real terminal width """
39     try:
40         buf = 'abcdefgh'
41         buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
42         return struct.unpack('hhhh', buf)[1]
43     except: # IOError
44         return 80
45
46 def truncate_url(url, width):
47     if len(url) > width:
48         return os.path.basename(url)[0:width]
49     return url
50
51 class TextProgress(object):
52     def start(self, filename, url, *args, **kwargs):
53         self.url = url
54         self.termwidth = terminal_width()
55         sys.stdout.write("Retrieving %s " % truncate_url(self.url, self.termwidth - 17))
56         sys.stdout.flush()
57         self.indicators = ["-", "\\", "|", "/"]
58         self.counter = 0
59     def update(self, *args):
60         if sys.stdout.isatty():
61             sys.stdout.write("\rRetrieving %s %s" % (truncate_url(self.url, self.termwidth - 17), self.indicators[self.counter%4]))
62             sys.stdout.flush()
63             self.counter += 1
64         else:
65             pass
66     def end(self, *args):
67         if sys.stdout.isatty():
68             sys.stdout.write("\rRetrieving %s ...OK\n" % (self.url,))
69         else:
70             sys.stdout.write("...OK\n")
71         sys.stdout.flush()
72
73 def find_binary_path(binary):
74     if os.environ.has_key("PATH"):
75         paths = os.environ["PATH"].split(":")
76     else:
77         paths = []
78         if os.environ.has_key("HOME"):
79             paths += [os.environ["HOME"] + "/bin"]
80         paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
81
82     for path in paths:
83         bin_path = "%s/%s" % (path, binary)
84         if os.path.exists(bin_path):
85             return bin_path
86     raise CreatorError("Command '%s' is not available." % binary)
87
88 def makedirs(dirname):
89     """A version of os.makedirs() that doesn't throw an
90     exception if the leaf directory already exists.
91     """
92     try:
93         os.makedirs(dirname)
94     except OSError, (err, msg):
95         if err != errno.EEXIST:
96             raise
97
98 def mksquashfs(in_img, out_img):
99     fullpathmksquashfs = find_binary_path("mksquashfs")
100     args = [fullpathmksquashfs, in_img, out_img]
101
102     if not sys.stdout.isatty():
103         args.append("-no-progress")
104
105     ret = subprocess.call(args, stdout=sys.stdout, stderr=sys.stderr)
106     if ret != 0:
107         raise SquashfsError("'%s' exited with error (%d)" %
108                             (string.join(args, " "), ret))
109
110 def resize2fs(fs, size):
111     dev_null = os.open("/dev/null", os.O_WRONLY)
112     try:
113         resize2fs = find_binary_path("resize2fs")
114         return subprocess.call([resize2fs, fs, "%sK" % (size / 1024,)],
115                                stdout = dev_null, stderr = dev_null)
116     finally:
117         os.close(dev_null)
118
119 def my_fuser(file):
120     ret = False
121     fuser = find_binary_path("fuser")
122     if not os.path.exists(file):
123         return ret
124     dev_null = os.open("/dev/null", os.O_WRONLY)
125     rc = subprocess.call([fuser, "-s", file], stderr=dev_null) 
126     if rc == 0:
127         fuser_proc = subprocess.Popen([fuser, file], stdout=subprocess.PIPE, stderr=dev_null)
128         pids = fuser_proc.communicate()[0].strip().split()
129         for pid in pids:
130             fd = open("/proc/%s/cmdline" % pid, "r")
131             cmdline = fd.read()
132             fd.close()
133             if cmdline[:-1] == "/bin/bash":
134                 ret = True
135                 break
136     os.close(dev_null)
137     return ret
138
139 class BindChrootMount:
140     """Represents a bind mount of a directory into a chroot."""
141     def __init__(self, src, chroot, dest = None, option = None):
142         self.src = src
143         self.root = os.path.abspath(os.path.expanduser(chroot))
144         self.option = option
145
146         if not dest:
147             dest = src
148         self.dest = self.root + "/" + dest
149
150         self.mounted = False
151         self.mountcmd = find_binary_path("mount")
152         self.umountcmd = find_binary_path("umount")
153
154     def ismounted(self):
155         ret = False
156         dev_null = os.open("/dev/null", os.O_WRONLY)
157         catcmd = find_binary_path("cat")
158         args = [ catcmd, "/proc/mounts" ]
159         proc_mounts = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=dev_null)
160         outputs = proc_mounts.communicate()[0].strip().split("\n")
161         for line in outputs:
162             if line.split()[1] == os.path.abspath(self.dest):
163                 ret = True
164                 break
165         os.close(dev_null)
166         return ret
167
168     def has_chroot_instance(self):
169         lock = os.path.join(self.root, ".chroot.lock")
170         return my_fuser(lock)
171
172     def mount(self):
173         if self.mounted or self.ismounted():
174             return
175
176         makedirs(self.dest)
177         rc = subprocess.call([self.mountcmd, "--bind", self.src, self.dest])
178         if rc != 0:
179             raise MountError("Bind-mounting '%s' to '%s' failed" %
180                              (self.src, self.dest))
181         if self.option:
182             rc = subprocess.call([self.mountcmd, "-o", "remount,%s" % self.option, self.dest])
183             if rc != 0:
184                 raise MountError("Bind-remounting '%s' failed" % self.dest)
185         self.mounted = True
186
187     def unmount(self):
188         if self.has_chroot_instance():
189             return
190
191         if self.ismounted():
192             subprocess.call([self.umountcmd, "-l", self.dest])
193         self.mounted = False
194
195 class LoopbackMount:
196     """LoopbackMount  compatibility layer for old API"""
197     def __init__(self, lofile, mountdir, fstype = None):
198         self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
199         self.losetup = False
200         self.losetupcmd = find_binary_path("losetup")
201
202     def cleanup(self):
203         self.diskmount.cleanup()
204
205     def unmount(self):
206         self.diskmount.unmount()
207
208     def lounsetup(self):
209         if self.losetup:
210             rc = subprocess.call([self.losetupcmd, "-d", self.loopdev])
211             self.losetup = False
212             self.loopdev = None
213
214     def loopsetup(self):
215         if self.losetup:
216             return
217
218         losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
219                                        stdout=subprocess.PIPE)
220         losetupOutput = losetupProc.communicate()[0]
221
222         if losetupProc.returncode:
223             raise MountError("Failed to allocate loop device for '%s'" %
224                              self.lofile)
225
226         self.loopdev = losetupOutput.split()[0]
227
228         rc = subprocess.call([self.losetupcmd, self.loopdev, self.lofile])
229         if rc != 0:
230             raise MountError("Failed to allocate loop device for '%s'" %
231                              self.lofile)
232
233         self.losetup = True
234
235     def mount(self):
236         self.diskmount.mount()
237
238 class SparseLoopbackMount(LoopbackMount):
239     """SparseLoopbackMount  compatibility layer for old API"""
240     def __init__(self, lofile, mountdir, size, fstype = None):
241         self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
242
243     def expand(self, create = False, size = None):
244         self.diskmount.disk.expand(create, size)
245
246     def truncate(self, size = None):
247         self.diskmount.disk.truncate(size)
248
249     def create(self):
250         self.diskmount.disk.create()
251
252 class SparseExtLoopbackMount(SparseLoopbackMount):
253     """SparseExtLoopbackMount  compatibility layer for old API"""
254     def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
255         self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
256
257
258     def __format_filesystem(self):
259         self.diskmount.__format_filesystem()
260
261     def create(self):
262         self.diskmount.disk.create()
263
264     def resize(self, size = None):
265         return self.diskmount.__resize_filesystem(size)
266
267     def mount(self):
268         self.diskmount.mount()
269
270     def __fsck(self):
271         self.extdiskmount.__fsck()
272
273     def __get_size_from_filesystem(self):
274         return self.diskmount.__get_size_from_filesystem()
275
276     def __resize_to_minimal(self):
277         return self.diskmount.__resize_to_minimal()
278
279     def resparse(self, size = None):
280         return self.diskmount.resparse(size)
281
282 class Disk:
283     """Generic base object for a disk
284
285     The 'create' method must make the disk visible as a block device - eg
286     by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
287     method must undo the 'create' operation.
288     """
289     def __init__(self, size, device = None):
290         self._device = device
291         self._size = size
292
293     def create(self):
294         pass
295
296     def cleanup(self):
297         pass
298
299     def get_device(self):
300         return self._device
301     def set_device(self, path):
302         self._device = path
303     device = property(get_device, set_device)
304
305     def get_size(self):
306         return self._size
307     size = property(get_size)
308
309
310 class RawDisk(Disk):
311     """A Disk backed by a block device.
312     Note that create() is a no-op.
313     """
314     def __init__(self, size, device):
315         Disk.__init__(self, size, device)
316
317     def fixed(self):
318         return True
319
320     def exists(self):
321         return True
322
323 class LoopbackDisk(Disk):
324     """A Disk backed by a file via the loop module."""
325     def __init__(self, lofile, size):
326         Disk.__init__(self, size)
327         self.lofile = lofile
328         self.losetupcmd = find_binary_path("losetup")
329
330     def fixed(self):
331         return False
332
333     def exists(self):
334         return os.path.exists(self.lofile)
335
336     def create(self):
337         if self.device is not None:
338             return
339
340         losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
341                                        stdout=subprocess.PIPE)
342         losetupOutput = losetupProc.communicate()[0]
343
344         if losetupProc.returncode:
345             raise MountError("Failed to allocate loop device for '%s'" %
346                              self.lofile)
347
348         device = losetupOutput.split()[0]
349
350         logging.debug("Losetup add %s mapping to %s"  % (device, self.lofile))
351         rc = subprocess.call([self.losetupcmd, device, self.lofile])
352         if rc != 0:
353             raise MountError("Failed to allocate loop device for '%s'" %
354                              self.lofile)
355         self.device = device
356
357     def cleanup(self):
358         if self.device is None:
359             return
360         logging.debug("Losetup remove %s" % self.device)
361         rc = subprocess.call([self.losetupcmd, "-d", self.device])
362         self.device = None
363
364
365
366 class SparseLoopbackDisk(LoopbackDisk):
367     """A Disk backed by a sparse file via the loop module."""
368     def __init__(self, lofile, size):
369         LoopbackDisk.__init__(self, lofile, size)
370
371     def expand(self, create = False, size = None):
372         flags = os.O_WRONLY
373         if create:
374             flags |= os.O_CREAT
375             if not os.path.exists(self.lofile):
376                 makedirs(os.path.dirname(self.lofile))
377
378         if size is None:
379             size = self.size
380
381         logging.debug("Extending sparse file %s to %d" % (self.lofile, size))
382         if create:
383             fd = os.open(self.lofile, flags, 0644)
384         else:
385             fd = os.open(self.lofile, flags)
386
387         os.lseek(fd, size, os.SEEK_SET)
388         os.write(fd, '\x00')
389         os.close(fd)
390
391     def truncate(self, size = None):
392         if size is None:
393             size = self.size
394
395         logging.debug("Truncating sparse file %s to %d" % (self.lofile, size))
396         fd = os.open(self.lofile, os.O_WRONLY)
397         os.ftruncate(fd, size)
398         os.close(fd)
399
400     def create(self):
401         self.expand(create = True)
402         LoopbackDisk.create(self)
403
404 class Mount:
405     """A generic base class to deal with mounting things."""
406     def __init__(self, mountdir):
407         self.mountdir = mountdir
408
409     def cleanup(self):
410         self.unmount()
411
412     def mount(self, options = None):
413         pass
414
415     def unmount(self):
416         pass
417
418 class DiskMount(Mount):
419     """A Mount object that handles mounting of a Disk."""
420     def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
421         Mount.__init__(self, mountdir)
422
423         self.disk = disk
424         self.fstype = fstype
425         self.rmmountdir = rmmountdir
426
427         self.mounted = False
428         self.rmdir   = False
429         if fstype:
430             self.mkfscmd = find_binary_path("mkfs." + self.fstype)
431         else:
432             self.mkfscmd = None
433         self.mountcmd = find_binary_path("mount")
434         self.umountcmd = find_binary_path("umount")
435
436     def cleanup(self):
437         Mount.cleanup(self)
438         self.disk.cleanup()
439
440     def unmount(self):
441         if self.mounted:
442             logging.debug("Unmounting directory %s" % self.mountdir)
443             synccmd = find_binary_path("sync")
444             subprocess.call([synccmd]) # sync the data on this mount point
445             rc = subprocess.call([self.umountcmd, "-l", self.mountdir])
446             if rc == 0:
447                 self.mounted = False
448             else:
449                 raise MountError("Failed to umount %s" % self.mountdir)
450         if self.rmdir and not self.mounted:
451             try:
452                 os.rmdir(self.mountdir)
453             except OSError, e:
454                 pass
455             self.rmdir = False
456
457
458     def __create(self):
459         self.disk.create()
460
461
462     def mount(self, options = None):
463         if self.mounted:
464             return
465
466         if not os.path.isdir(self.mountdir):
467             logging.debug("Creating mount point %s" % self.mountdir)
468             os.makedirs(self.mountdir)
469             self.rmdir = self.rmmountdir
470
471         self.__create()
472
473         logging.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
474         if options:
475             args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
476         else:
477             args = [ self.mountcmd, self.disk.device, self.mountdir ]
478         if self.fstype:
479             args.extend(["-t", self.fstype])
480
481         rc = subprocess.call(args)
482         if rc != 0:
483             raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
484                              (self.disk.device, self.mountdir, " ".join(args), rc))
485
486         self.mounted = True
487
488 class ExtDiskMount(DiskMount):
489     """A DiskMount object that is able to format/resize ext[23] filesystems."""
490     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
491         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
492         self.blocksize = blocksize
493         self.fslabel = fslabel.replace("/", "")
494         self.uuid  = None
495         self.skipformat = skipformat
496         self.fsopts = fsopts
497         self.dumpe2fs = find_binary_path("dumpe2fs")
498         self.tune2fs = find_binary_path("tune2fs")
499
500     def __parse_field(self, output, field):
501         for line in output.split("\n"):
502             if line.startswith(field + ":"):
503                 return line[len(field) + 1:].strip()
504
505         raise KeyError("Failed to find field '%s' in output" % field)
506
507     def __format_filesystem(self):
508         if self.skipformat:
509             logging.debug("Skip filesystem format.")
510             return
511         logging.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
512         rc = subprocess.call([self.mkfscmd,
513                               "-F", "-L", self.fslabel,
514                               "-m", "1", "-b", str(self.blocksize),
515                               self.disk.device], stdout=sys.stdout,
516                               stderr=sys.stderr)
517         #                      str(self.disk.size / self.blocksize)])
518         if rc != 0:
519             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
520
521         dev_null = os.open("/dev/null", os.O_WRONLY)
522         try:
523             out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.device],
524                                    stdout = subprocess.PIPE,
525                                    stderr = dev_null).communicate()[0]
526         finally:
527             os.close(dev_null)
528
529         self.uuid = self.__parse_field(out, "Filesystem UUID")
530         logging.debug("Tuning filesystem on %s" % self.disk.device)
531         subprocess.call([self.tune2fs, "-c0", "-i0", "-Odir_index",
532                          "-ouser_xattr,acl", self.disk.device],
533                          stdout=sys.stdout, stderr=sys.stderr)
534
535     def __resize_filesystem(self, size = None):
536         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
537
538         if size is None:
539             size = self.disk.size
540
541         if size == current_size:
542             return
543
544         if size > current_size:
545             self.disk.expand(size)
546
547         self.__fsck()
548
549         resize2fs(self.disk.lofile, size)
550         return size
551
552     def __create(self):
553         resize = False
554         if not self.disk.fixed() and self.disk.exists():
555             resize = True
556
557         self.disk.create()
558
559         if resize:
560             self.__resize_filesystem()
561         else:
562             self.__format_filesystem()
563
564     def mount(self, options = None):
565         self.__create()
566         DiskMount.mount(self, options)
567
568     def __fsck(self):
569         logging.debug("Checking filesystem %s" % self.disk.lofile)
570         subprocess.call(["/sbin/e2fsck", "-f", "-y", self.disk.lofile], stdout=sys.stdout, stderr=sys.stderr)
571
572     def __get_size_from_filesystem(self):
573         dev_null = os.open("/dev/null", os.O_WRONLY)
574         try:
575             out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.lofile],
576                                    stdout = subprocess.PIPE,
577                                    stderr = dev_null).communicate()[0]
578         finally:
579             os.close(dev_null)
580
581         return int(self.__parse_field(out, "Block count")) * self.blocksize
582
583     def __resize_to_minimal(self):
584         self.__fsck()
585
586         #
587         # Use a binary search to find the minimal size
588         # we can resize the image to
589         #
590         bot = 0
591         top = self.__get_size_from_filesystem()
592         while top != (bot + 1):
593             t = bot + ((top - bot) / 2)
594
595             if not resize2fs(self.disk.lofile, t):
596                 top = t
597             else:
598                 bot = t
599         return top
600
601     def resparse(self, size = None):
602         self.cleanup()
603         minsize = self.__resize_to_minimal()
604         self.disk.truncate(minsize)
605         self.__resize_filesystem(size)
606         return minsize
607
608 class VfatDiskMount(DiskMount):
609     """A DiskMount object that is able to format vfat/msdos filesystems."""
610     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
611         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
612         self.blocksize = blocksize
613         self.fslabel = fslabel.replace("/", "")
614         self.uuid = "%08X" % int(time.time())
615         self.skipformat = skipformat
616         self.fsopts = fsopts
617         self.fsckcmd = find_binary_path("fsck." + self.fstype)
618
619     def __format_filesystem(self):
620         if self.skipformat:
621             logging.debug("Skip filesystem format.")
622             return
623         logging.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
624         blah = [self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device]
625         rc = subprocess.call(blah)
626         if rc != 0:
627             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
628         logging.debug("Tuning filesystem on %s" % self.disk.device)
629
630     def __resize_filesystem(self, size = None):
631         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
632
633         if size is None:
634             size = self.disk.size
635
636         if size == current_size:
637             return
638
639         if size > current_size:
640             self.disk.expand(size)
641
642         self.__fsck()
643
644         #resize2fs(self.disk.lofile, size)
645         return size
646
647     def __create(self):
648         resize = False
649         if not self.disk.fixed() and self.disk.exists():
650             resize = True
651
652         self.disk.create()
653
654         if resize:
655             self.__resize_filesystem()
656         else:
657             self.__format_filesystem()
658
659     def mount(self, options = None):
660         self.__create()
661         DiskMount.mount(self, options)
662
663     def __fsck(self):
664         logging.debug("Checking filesystem %s" % self.disk.lofile)
665         subprocess.call([self.fsckcmd, "-y", self.disk.lofile])
666
667     def __get_size_from_filesystem(self):
668         return self.disk.size
669
670     def __resize_to_minimal(self):
671         self.__fsck()
672
673         #
674         # Use a binary search to find the minimal size
675         # we can resize the image to
676         #
677         bot = 0
678         top = self.__get_size_from_filesystem()
679         return top
680
681     def resparse(self, size = None):
682         self.cleanup()
683         minsize = self.__resize_to_minimal()
684         self.disk.truncate(minsize)
685         self.__resize_filesystem(size)
686         return minsize
687
688 class BtrfsDiskMount(DiskMount):
689     """A DiskMount object that is able to format/resize btrfs filesystems."""
690     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
691         self.__check_btrfs()
692         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
693         self.blocksize = blocksize
694         self.fslabel = fslabel.replace("/", "")
695         self.uuid  = None
696         self.skipformat = skipformat
697         self.fsopts = fsopts
698         self.blkidcmd = find_binary_path("blkid")
699         self.btrfsckcmd = find_binary_path("btrfsck")
700
701     def __check_btrfs(self):
702         found = False
703         """ Need to load btrfs module to mount it """
704         load_module("btrfs")
705         for line in open("/proc/filesystems").xreadlines():
706             if line.find("btrfs") > -1:
707                 found = True
708                 break
709         if not found:
710             raise MountError("Your system can't mount btrfs filesystem, please make sure your kernel has btrfs support and the module btrfs.ko has been loaded.")
711
712         # disable selinux, selinux will block write
713         if os.path.exists("/usr/sbin/setenforce"):
714             subprocess.call(["/usr/sbin/setenforce", "0"])
715
716     def __parse_field(self, output, field):
717         for line in output.split(" "):
718             if line.startswith(field + "="):
719                 return line[len(field) + 1:].strip().replace("\"", "")
720
721         raise KeyError("Failed to find field '%s' in output" % field)
722
723     def __format_filesystem(self):
724         if self.skipformat:
725             logging.debug("Skip filesystem format.")
726             return
727         logging.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
728         rc = subprocess.call([self.mkfscmd, "-L", self.fslabel, self.disk.device])
729         if rc != 0:
730             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
731
732         dev_null = os.open("/dev/null", os.O_WRONLY)
733         try:
734             out = subprocess.Popen([self.blkidcmd, self.disk.device],
735                                    stdout = subprocess.PIPE,
736                                    stderr = dev_null).communicate()[0]
737         finally:
738             os.close(dev_null)
739
740         self.uuid = self.__parse_field(out, "UUID")
741
742     def __resize_filesystem(self, size = None):
743         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
744
745         if size is None:
746             size = self.disk.size
747
748         if size == current_size:
749             return
750
751         if size > current_size:
752             self.disk.expand(size)
753
754         self.__fsck()
755         return size
756
757     def __create(self):
758         resize = False
759         if not self.disk.fixed() and self.disk.exists():
760             resize = True
761
762         self.disk.create()
763
764         if resize:
765             self.__resize_filesystem()
766         else:
767             self.__format_filesystem()
768
769     def mount(self, options = None):
770         self.__create()
771         DiskMount.mount(self, options)
772
773     def __fsck(self):
774         logging.debug("Checking filesystem %s" % self.disk.lofile)
775         subprocess.call([self.btrfsckcmd, self.disk.lofile])
776
777     def __get_size_from_filesystem(self):
778         return self.disk.size
779
780     def __resize_to_minimal(self):
781         self.__fsck()
782
783         return self.__get_size_from_filesystem()
784
785     def resparse(self, size = None):
786         self.cleanup()
787         minsize = self.__resize_to_minimal()
788         self.disk.truncate(minsize)
789         self.__resize_filesystem(size)
790         return minsize
791
792 class DeviceMapperSnapshot(object):
793     def __init__(self, imgloop, cowloop):
794         self.imgloop = imgloop
795         self.cowloop = cowloop
796
797         self.__created = False
798         self.__name = None
799         self.dmsetupcmd = find_binary_path("dmsetup")
800
801         """Load dm_snapshot if it isn't loaded"""
802         load_module("dm_snapshot")
803
804     def get_path(self):
805         if self.__name is None:
806             return None
807         return os.path.join("/dev/mapper", self.__name)
808     path = property(get_path)
809
810     def create(self):
811         if self.__created:
812             return
813
814         self.imgloop.create()
815         self.cowloop.create()
816
817         self.__name = "imgcreate-%d-%d" % (os.getpid(),
818                                            random.randint(0, 2**16))
819
820         size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
821
822         table = "0 %d snapshot %s %s p 8" % (size / 512,
823                                              self.imgloop.device,
824                                              self.cowloop.device)
825
826         args = [self.dmsetupcmd, "create", self.__name, "--table", table]
827         if subprocess.call(args) != 0:
828             self.cowloop.cleanup()
829             self.imgloop.cleanup()
830             raise SnapshotError("Could not create snapshot device using: " +
831                                 string.join(args, " "))
832
833         self.__created = True
834
835     def remove(self, ignore_errors = False):
836         if not self.__created:
837             return
838         
839         time.sleep(2)
840         rc = subprocess.call([self.dmsetupcmd, "remove", self.__name])
841         if not ignore_errors and rc != 0:
842             raise SnapshotError("Could not remove snapshot device")
843
844         self.__name = None
845         self.__created = False
846
847         self.cowloop.cleanup()
848         self.imgloop.cleanup()
849
850     def get_cow_used(self):
851         if not self.__created:
852             return 0
853
854         dev_null = os.open("/dev/null", os.O_WRONLY)
855         try:
856             out = subprocess.Popen([self.dmsetupcmd, "status", self.__name],
857                                    stdout = subprocess.PIPE,
858                                    stderr = dev_null).communicate()[0]
859         finally:
860             os.close(dev_null)
861
862         #
863         # dmsetup status on a snapshot returns e.g.
864         #   "0 8388608 snapshot 416/1048576"
865         # or, more generally:
866         #   "A B snapshot C/D"
867         # where C is the number of 512 byte sectors in use
868         #
869         try:
870             return int((out.split()[3]).split('/')[0]) * 512
871         except ValueError:
872             raise SnapshotError("Failed to parse dmsetup status: " + out)
873
874 def create_image_minimizer(path, image, minimal_size):
875     """
876     Builds a copy-on-write image which can be used to
877     create a device-mapper snapshot of an image where
878     the image's filesystem is as small as possible
879
880     The steps taken are:
881       1) Create a sparse COW
882       2) Loopback mount the image and the COW
883       3) Create a device-mapper snapshot of the image
884          using the COW
885       4) Resize the filesystem to the minimal size
886       5) Determine the amount of space used in the COW
887       6) Restroy the device-mapper snapshot
888       7) Truncate the COW, removing unused space
889       8) Create a squashfs of the COW
890     """
891     imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
892
893     cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
894                                  64L * 1024L * 1024L)
895
896     snapshot = DeviceMapperSnapshot(imgloop, cowloop)
897
898     try:
899         snapshot.create()
900
901         resize2fs(snapshot.path, minimal_size)
902
903         cow_used = snapshot.get_cow_used()
904     finally:
905         snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
906
907     cowloop.truncate(cow_used)
908
909     mksquashfs(cowloop.lofile, path)
910
911     os.unlink(cowloop.lofile)
912
913 def load_module(module):
914     found = False
915     for line in open('/proc/modules').xreadlines():
916         if line.startswith("%s " % module):
917             found = True
918             break
919     if not found:
920         print "Loading %s..." % module
921         dev_null = os.open("/dev/null", os.O_WRONLY)
922         modprobecmd = find_binary_path("modprobe")
923         modprobe = subprocess.Popen([modprobecmd, module],
924                      stdout=dev_null, stderr=dev_null)
925         os.waitpid(modprobe.pid, 0)
926         os.close(dev_null)
927
928 def myurlgrab(url, filename, proxies):
929     g = URLGrabber()
930     if url.startswith("file:///"):
931         file = url.replace("file://", "")
932         if not os.path.exists(file):
933             raise CreatorError("URLGrabber error: can't find file %s" % file)
934         copycmd = find_binary_path("cp")
935         subprocess.call([copycmd, "-f", file, filename])
936     else:
937         try:
938             filename = g.urlgrab(url = url, filename = filename,
939                 ssl_verify_host = False, ssl_verify_peer = False,
940                 proxies = proxies, http_headers = (('Pragma', 'no-cache'),))
941         except URLGrabError, e:
942             raise CreatorError("URLGrabber error: %s: %s" % (e, url))
943         except:
944             raise CreatorError("URLGrabber error: %s" % url)
945     return filename