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