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