2 # fs.py : Filesystem related utilities and classes
4 # Copyright 2007, Red Hat Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Library General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
31 from imgcreate
.errors
import *
33 def makedirs(dirname
):
34 """A version of os.makedirs() that doesn't throw an
35 exception if the leaf directory already exists.
40 if e
.errno
!= errno
.EEXIST
:
43 def mksquashfs(in_img
, out_img
, compress_type
):
44 # Allow gzip to work for older versions of mksquashfs
45 if compress_type
== "gzip":
46 args
= ["/sbin/mksquashfs", in_img
, out_img
]
48 args
= ["/sbin/mksquashfs", in_img
, out_img
, "-comp", compress_type
]
50 if not sys
.stdout
.isatty():
51 args
.append("-no-progress")
53 ret
= subprocess
.call(args
)
55 raise SquashfsError("'%s' exited with error (%d)" %
56 (string
.join(args
, " "), ret
))
58 def resize2fs(fs
, size
= None, minimal
= False):
59 if minimal
and size
is not None:
60 raise ResizeError("Can't specify both minimal and a size for resize!")
61 if not minimal
and size
is None:
62 raise ResizeError("Must specify either a size or minimal for resize!")
65 (fd
, saved_image
) = tempfile
.mkstemp("", "resize-image-", "/tmp")
67 subprocess
.call(["/sbin/e2image", "-r", fs
, saved_image
])
69 args
= ["/sbin/resize2fs", fs
]
73 args
.append("%sK" %(size
/ 1024,))
74 ret
= subprocess
.call(args
)
76 raise ResizeError("resize2fs returned an error (%d)! image to debug at %s" %(ret
, saved_image
))
79 raise ResizeError("fsck after resize returned an error! image to debug at %s" %(saved_image
,))
80 os
.unlink(saved_image
)
84 logging
.debug("Checking filesystem %s" % fs
)
85 rc
= subprocess
.call(["/sbin/e2fsck", "-f", "-y", fs
])
88 class BindChrootMount
:
89 """Represents a bind mount of a directory into a chroot."""
90 def __init__(self
, src
, chroot
, dest
= None):
96 self
.dest
= self
.root
+ "/" + dest
105 rc
= subprocess
.call(["/bin/mount", "--bind", self
.src
, self
.dest
])
107 raise MountError("Bind-mounting '%s' to '%s' failed" %
108 (self
.src
, self
.dest
))
115 rc
= subprocess
.call(["/bin/umount", self
.dest
])
117 logging
.debug("Unable to unmount %s normally, using lazy unmount" % self
.dest
)
118 rc
= subprocess
.call(["/bin/umount", "-l", self
.dest
])
120 raise MountError("Unable to unmount fs at %s" % self
.dest
)
122 logging
.debug("lazy umount succeeded on %s" % self
.dest
)
123 print >> sys
.stdout
, "lazy umount succeeded on %s" % self
.dest
128 """LoopbackMount compatibility layer for old API"""
129 def __init__(self
, lofile
, mountdir
, fstype
= None):
130 self
.diskmount
= DiskMount(LoopbackDisk(lofile
,size
= 0),mountdir
,fstype
,rmmountdir
= True)
134 self
.diskmount
.cleanup()
137 self
.diskmount
.unmount()
141 rc
= subprocess
.call(["/sbin/losetup", "-d", self
.loopdev
])
149 losetupProc
= subprocess
.Popen(["/sbin/losetup", "-f"],
150 stdout
=subprocess
.PIPE
)
151 losetupOutput
= losetupProc
.communicate()[0]
153 if losetupProc
.returncode
:
154 raise MountError("Failed to allocate loop device for '%s'" %
157 self
.loopdev
= losetupOutput
.split()[0]
159 rc
= subprocess
.call(["/sbin/losetup", self
.loopdev
, self
.lofile
])
161 raise MountError("Failed to allocate loop device for '%s'" %
167 self
.diskmount
.mount()
169 class SparseLoopbackMount(LoopbackMount
):
170 """SparseLoopbackMount compatibility layer for old API"""
171 def __init__(self
, lofile
, mountdir
, size
, fstype
= None):
172 self
.diskmount
= DiskMount(SparseLoopbackDisk(lofile
,size
),mountdir
,fstype
,rmmountdir
= True)
174 def expand(self
, create
= False, size
= None):
175 self
.diskmount
.disk
.expand(create
, size
)
177 def truncate(self
, size
= None):
178 self
.diskmount
.disk
.truncate(size
)
181 self
.diskmount
.disk
.create()
183 class SparseExtLoopbackMount(SparseLoopbackMount
):
184 """SparseExtLoopbackMount compatibility layer for old API"""
185 def __init__(self
, lofile
, mountdir
, size
, fstype
, blocksize
, fslabel
):
186 self
.diskmount
= ExtDiskMount(SparseLoopbackDisk(lofile
,size
), mountdir
, fstype
, blocksize
, fslabel
, rmmountdir
= True)
189 def __format_filesystem(self
):
190 self
.diskmount
.__format
_filesystem
()
193 self
.diskmount
.disk
.create()
195 def resize(self
, size
= None):
196 return self
.diskmount
.__resize
_filesystem
(size
)
199 self
.diskmount
.mount()
202 self
.extdiskmount
.__fsck
()
204 def __get_size_from_filesystem(self
):
205 return self
.diskmount
.__get
_size
_from
_filesystem
()
207 def __resize_to_minimal(self
):
208 return self
.diskmount
.__resize
_to
_minimal
()
210 def resparse(self
, size
= None):
211 return self
.diskmount
.resparse(size
)
214 """Generic base object for a disk
216 The 'create' method must make the disk visible as a block device - eg
217 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
218 method must undo the 'create' operation.
220 def __init__(self
, size
, device
= None):
221 self
._device
= device
230 def get_device(self
):
232 def set_device(self
, path
):
234 device
= property(get_device
, set_device
)
238 size
= property(get_size
)
242 """A Disk backed by a block device.
243 Note that create() is a no-op.
245 def __init__(self
, size
, device
):
246 Disk
.__init
__(self
, size
, device
)
254 class LoopbackDisk(Disk
):
255 """A Disk backed by a file via the loop module."""
256 def __init__(self
, lofile
, size
):
257 Disk
.__init
__(self
, size
)
264 return os
.path
.exists(self
.lofile
)
267 if self
.device
is not None:
270 losetupProc
= subprocess
.Popen(["/sbin/losetup", "-f"],
271 stdout
=subprocess
.PIPE
)
272 losetupOutput
= losetupProc
.communicate()[0]
274 if losetupProc
.returncode
:
275 raise MountError("Failed to allocate loop device for '%s'" %
278 device
= losetupOutput
.split()[0]
280 logging
.debug("Losetup add %s mapping to %s" % (device
, self
.lofile
))
281 rc
= subprocess
.call(["/sbin/losetup", device
, self
.lofile
])
283 raise MountError("Failed to allocate loop device for '%s'" %
288 if self
.device
is None:
290 logging
.debug("Losetup remove %s" % self
.device
)
291 rc
= subprocess
.call(["/sbin/losetup", "-d", self
.device
])
296 class SparseLoopbackDisk(LoopbackDisk
):
297 """A Disk backed by a sparse file via the loop module."""
298 def __init__(self
, lofile
, size
):
299 LoopbackDisk
.__init
__(self
, lofile
, size
)
301 def expand(self
, create
= False, size
= None):
305 makedirs(os
.path
.dirname(self
.lofile
))
310 logging
.debug("Extending sparse file %s to %d" % (self
.lofile
, size
))
311 fd
= os
.open(self
.lofile
, flags
)
315 os
.lseek(fd
, size
-1, 0)
319 def truncate(self
, size
= None):
323 logging
.debug("Truncating sparse file %s to %d" % (self
.lofile
, size
))
324 fd
= os
.open(self
.lofile
, os
.O_WRONLY
)
325 os
.ftruncate(fd
, size
)
329 self
.expand(create
= True)
330 LoopbackDisk
.create(self
)
333 """A generic base class to deal with mounting things."""
334 def __init__(self
, mountdir
):
335 self
.mountdir
= mountdir
346 class DiskMount(Mount
):
347 """A Mount object that handles mounting of a Disk."""
348 def __init__(self
, disk
, mountdir
, fstype
= None, rmmountdir
= True):
349 Mount
.__init
__(self
, mountdir
)
353 self
.rmmountdir
= rmmountdir
364 logging
.debug("Unmounting directory %s" % self
.mountdir
)
365 rc
= subprocess
.call(["/bin/umount", self
.mountdir
])
369 logging
.debug("Unmounting directory %s failed, using lazy umount" % self
.mountdir
)
370 print >> sys
.stdout
, "Unmounting directory %s failed, using lazy umount" %self
.mountdir
371 rc
= subprocess
.call(["/bin/umount", "-l", self
.mountdir
])
373 raise MountError("Unable to unmount filesystem at %s" % self
.mountdir
)
375 logging
.debug("lazy umount succeeded on %s" % self
.mountdir
)
376 print >> sys
.stdout
, "lazy umount succeeded on %s" % self
.mountdir
379 if self
.rmdir
and not self
.mounted
:
381 os
.rmdir(self
.mountdir
)
395 if not os
.path
.isdir(self
.mountdir
):
396 logging
.debug("Creating mount point %s" % self
.mountdir
)
397 os
.makedirs(self
.mountdir
)
398 self
.rmdir
= self
.rmmountdir
402 logging
.debug("Mounting %s at %s" % (self
.disk
.device
, self
.mountdir
))
403 args
= [ "/bin/mount", self
.disk
.device
, self
.mountdir
]
405 args
.extend(["-t", self
.fstype
])
407 rc
= subprocess
.call(args
)
409 raise MountError("Failed to mount '%s' to '%s'" %
410 (self
.disk
.device
, self
.mountdir
))
414 class ExtDiskMount(DiskMount
):
415 """A DiskMount object that is able to format/resize ext[23] filesystems."""
416 def __init__(self
, disk
, mountdir
, fstype
, blocksize
, fslabel
, rmmountdir
=True):
417 DiskMount
.__init
__(self
, disk
, mountdir
, fstype
, rmmountdir
)
418 self
.blocksize
= blocksize
419 self
.fslabel
= "_" + fslabel
421 def __format_filesystem(self
):
422 logging
.debug("Formating %s filesystem on %s" % (self
.fstype
, self
.disk
.device
))
423 rc
= subprocess
.call(["/sbin/mkfs." + self
.fstype
,
424 "-F", "-L", self
.fslabel
,
425 "-m", "1", "-b", str(self
.blocksize
),
427 # str(self.disk.size / self.blocksize)])
429 raise MountError("Error creating %s filesystem" % (self
.fstype
,))
430 logging
.debug("Tuning filesystem on %s" % self
.disk
.device
)
431 subprocess
.call(["/sbin/tune2fs", "-c0", "-i0", "-Odir_index",
432 "-ouser_xattr,acl", self
.disk
.device
])
434 def __resize_filesystem(self
, size
= None):
435 current_size
= os
.stat(self
.disk
.lofile
)[stat
.ST_SIZE
]
438 size
= self
.disk
.size
440 if size
== current_size
:
443 if size
> current_size
:
444 self
.disk
.expand(size
)
446 resize2fs(self
.disk
.lofile
, size
)
451 if not self
.disk
.fixed() and self
.disk
.exists():
457 self
.__resize
_filesystem
()
459 self
.__format
_filesystem
()
463 DiskMount
.mount(self
)
466 return e2fsck(self
.disk
.lofile
)
469 def __get_size_from_filesystem(self
):
470 def parse_field(output
, field
):
471 for line
in output
.split("\n"):
472 if line
.startswith(field
+ ":"):
473 return line
[len(field
) + 1:].strip()
475 raise KeyError("Failed to find field '%s' in output" % field
)
477 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
479 out
= subprocess
.Popen(['/sbin/dumpe2fs', '-h', self
.disk
.lofile
],
480 stdout
= subprocess
.PIPE
,
481 stderr
= dev_null
).communicate()[0]
485 return int(parse_field(out
, "Block count")) * self
.blocksize
487 def __resize_to_minimal(self
):
488 resize2fs(self
.disk
.lofile
, minimal
= True)
489 return self
.__get
_size
_from
_filesystem
()
491 def resparse(self
, size
= None):
493 minsize
= self
.__resize
_to
_minimal
()
494 self
.disk
.truncate(minsize
)
495 self
.__resize
_filesystem
(size
)
498 class DeviceMapperSnapshot(object):
499 def __init__(self
, imgloop
, cowloop
):
500 self
.imgloop
= imgloop
501 self
.cowloop
= cowloop
503 self
.__created
= False
507 if self
.__name
is None:
509 return os
.path
.join("/dev/mapper", self
.__name
)
510 path
= property(get_path
)
516 self
.imgloop
.create()
517 self
.cowloop
.create()
519 self
.__name
= "imgcreate-%d-%d" % (os
.getpid(),
520 random
.randint(0, 2**16))
522 size
= os
.stat(self
.imgloop
.lofile
)[stat
.ST_SIZE
]
524 table
= "0 %d snapshot %s %s p 8" % (size
/ 512,
528 args
= ["/sbin/dmsetup", "create", self
.__name
, "--table", table
]
529 if subprocess
.call(args
) != 0:
530 self
.cowloop
.cleanup()
531 self
.imgloop
.cleanup()
532 raise SnapshotError("Could not create snapshot device using: " +
533 string
.join(args
, " "))
535 self
.__created
= True
537 def remove(self
, ignore_errors
= False):
538 if not self
.__created
:
541 # sleep to try to avoid any dm shenanigans
543 rc
= subprocess
.call(["/sbin/dmsetup", "remove", self
.__name
])
544 if not ignore_errors
and rc
!= 0:
545 raise SnapshotError("Could not remove snapshot device")
548 self
.__created
= False
550 self
.cowloop
.cleanup()
551 self
.imgloop
.cleanup()
553 def get_cow_used(self
):
554 if not self
.__created
:
557 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
559 out
= subprocess
.Popen(["/sbin/dmsetup", "status", self
.__name
],
560 stdout
= subprocess
.PIPE
,
561 stderr
= dev_null
).communicate()[0]
566 # dmsetup status on a snapshot returns e.g.
567 # "0 8388608 snapshot 416/1048576"
568 # or, more generally:
570 # where C is the number of 512 byte sectors in use
573 return int((out
.split()[3]).split('/')[0]) * 512
575 raise SnapshotError("Failed to parse dmsetup status: " + out
)
577 def create_image_minimizer(path
, image
, compress_type
, target_size
= None):
579 Builds a copy-on-write image which can be used to
580 create a device-mapper snapshot of an image where
581 the image's filesystem is as small as possible
584 1) Create a sparse COW
585 2) Loopback mount the image and the COW
586 3) Create a device-mapper snapshot of the image
588 4) Resize the filesystem to the minimal size
589 5) Determine the amount of space used in the COW
590 6) Restroy the device-mapper snapshot
591 7) Truncate the COW, removing unused space
592 8) Create a squashfs of the COW
594 imgloop
= LoopbackDisk(image
, None) # Passing bogus size - doesn't matter
596 cowloop
= SparseLoopbackDisk(os
.path
.join(os
.path
.dirname(path
), "osmin"),
599 snapshot
= DeviceMapperSnapshot(imgloop
, cowloop
)
604 if target_size
is not None:
605 resize2fs(snapshot
.path
, target_size
)
607 resize2fs(snapshot
.path
, minimal
= True)
609 cow_used
= snapshot
.get_cow_used()
611 snapshot
.remove(ignore_errors
= (not sys
.exc_info()[0] is None))
613 cowloop
.truncate(cow_used
)
615 mksquashfs(cowloop
.lofile
, path
, compress_type
)
617 os
.unlink(cowloop
.lofile
)