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