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