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.
32 from imgcreate
.errors
import *
34 def makedirs(dirname
):
35 """A version of os.makedirs() that doesn't throw an
36 exception if the leaf directory already exists.
41 if e
.errno
!= errno
.EEXIST
:
44 def squashfs_compression_type(sqfs_img
):
45 """Check the compression type of a SquashFS image. If the type cannot be
46 ascertained, return 'undetermined'. The calling code must decide what to
49 env
= os
.environ
.copy()
51 args
= ['/usr/sbin/unsquashfs', '-s', sqfs_img
]
53 p
= subprocess
.Popen(args
, stdout
=subprocess
.PIPE
,
54 stderr
=subprocess
.PIPE
, env
=env
)
55 out
, err
= p
.communicate()
57 raise SquashfsError(u
"Error white stat-ing '%s'\n'%s'" % (args
, e
))
59 raise SquashfsError(u
"Error while stat-ing '%s'" % args
)
63 u
"Error while stat-ing '%s'\n'%s'\nreturncode: '%s'" %
64 (args
, err
, p
.returncode
))
66 compress_type
= 'undetermined'
67 for l
in out
.splitlines():
68 if l
.split(None, 1)[0] == 'Compression':
69 compress_type
= l
.split()[1]
73 def mksquashfs(in_img
, out_img
, compress_type
):
74 # Allow gzip to work for older versions of mksquashfs
75 if compress_type
== "gzip":
76 args
= ["/sbin/mksquashfs", in_img
, out_img
]
78 args
= ["/sbin/mksquashfs", in_img
, out_img
, "-comp", compress_type
]
80 if not sys
.stdout
.isatty():
81 args
.append("-no-progress")
85 raise SquashfsError("'%s' exited with error (%d)" %
86 (string
.join(args
, " "), ret
))
88 def resize2fs(fs
, size
= None, minimal
= False, tmpdir
= "/tmp"):
89 if minimal
and size
is not None:
90 raise ResizeError("Can't specify both minimal and a size for resize!")
91 if not minimal
and size
is None:
92 raise ResizeError("Must specify either a size or minimal for resize!")
96 logging
.info("resizing %s" % (fs
,))
97 args
= ["/sbin/resize2fs", fs
]
101 args
.append("%sK" %(size
/ 1024,))
104 raise ResizeError("resize2fs returned an error (%d)!" % (ret
,))
108 raise ResizeError("fsck after resize returned an error (%d)!" % (ret
,))
113 logging
.info("Checking filesystem %s" % fs
)
114 return call(["/sbin/e2fsck", "-f", "-y", fs
])
116 class BindChrootMount
:
117 """Represents a bind mount of a directory into a chroot."""
118 def __init__(self
, src
, chroot
, dest
= None):
124 self
.dest
= self
.root
+ "/" + dest
133 rc
= call(["/bin/mount", "--bind", self
.src
, self
.dest
])
135 raise MountError("Bind-mounting '%s' to '%s' failed" %
136 (self
.src
, self
.dest
))
143 rc
= call(["/bin/umount", self
.dest
])
145 logging
.info("Unable to unmount %s normally, using lazy unmount" % self
.dest
)
146 rc
= call(["/bin/umount", "-l", self
.dest
])
148 raise MountError("Unable to unmount fs at %s" % self
.dest
)
150 logging
.info("lazy umount succeeded on %s" % self
.dest
)
151 print >> sys
.stdout
, "lazy umount succeeded on %s" % self
.dest
156 """LoopbackMount compatibility layer for old API"""
157 def __init__(self
, lofile
, mountdir
, fstype
= None):
158 self
.diskmount
= DiskMount(LoopbackDisk(lofile
,size
= 0),mountdir
,fstype
,rmmountdir
= True)
162 self
.diskmount
.cleanup()
165 self
.diskmount
.unmount()
169 rc
= call(["/sbin/losetup", "-d", self
.loopdev
])
177 losetupProc
= subprocess
.Popen(["/sbin/losetup", "-f"],
178 stdout
=subprocess
.PIPE
)
179 losetupOutput
= losetupProc
.communicate()[0]
181 if losetupProc
.returncode
:
182 raise MountError("Failed to allocate loop device for '%s'" %
185 self
.loopdev
= losetupOutput
.split()[0]
187 rc
= call(["/sbin/losetup", self
.loopdev
, self
.lofile
])
189 raise MountError("Failed to allocate loop device for '%s'" %
195 self
.diskmount
.mount()
197 class SparseLoopbackMount(LoopbackMount
):
198 """SparseLoopbackMount compatibility layer for old API"""
199 def __init__(self
, lofile
, mountdir
, size
, fstype
= None):
200 self
.diskmount
= DiskMount(SparseLoopbackDisk(lofile
,size
),mountdir
,fstype
,rmmountdir
= True)
202 def expand(self
, create
= False, size
= None):
203 self
.diskmount
.disk
.expand(create
, size
)
205 def truncate(self
, size
= None):
206 self
.diskmount
.disk
.truncate(size
)
209 self
.diskmount
.disk
.create()
211 class SparseExtLoopbackMount(SparseLoopbackMount
):
212 """SparseExtLoopbackMount compatibility layer for old API"""
213 def __init__(self
, lofile
, mountdir
, size
, fstype
, blocksize
, fslabel
):
214 self
.diskmount
= ExtDiskMount(SparseLoopbackDisk(lofile
,size
),
215 mountdir
, fstype
, blocksize
, fslabel
,
216 rmmountdir
= True, tmpdir
= "/tmp")
219 def __format_filesystem(self
):
220 self
.diskmount
.__format
_filesystem
()
223 self
.diskmount
.disk
.create()
225 def resize(self
, size
= None):
226 return self
.diskmount
.__resize
_filesystem
(size
)
229 self
.diskmount
.mount()
232 self
.extdiskmount
.__fsck
()
234 def __get_size_from_filesystem(self
):
235 return self
.diskmount
.__get
_size
_from
_filesystem
()
237 def __resize_to_minimal(self
):
238 return self
.diskmount
.__resize
_to
_minimal
()
240 def resparse(self
, size
= None):
241 return self
.diskmount
.resparse(size
)
244 """Generic base object for a disk
246 The 'create' method must make the disk visible as a block device - eg
247 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
248 method must undo the 'create' operation.
250 def __init__(self
, size
, device
= None):
251 self
._device
= device
260 def get_device(self
):
262 def set_device(self
, path
):
264 device
= property(get_device
, set_device
)
268 size
= property(get_size
)
272 """A Disk backed by a block device.
273 Note that create() is a no-op.
275 def __init__(self
, size
, device
):
276 Disk
.__init
__(self
, size
, device
)
284 class LoopbackDisk(Disk
):
285 """A Disk backed by a file via the loop module."""
286 def __init__(self
, lofile
, size
):
287 Disk
.__init
__(self
, size
)
294 return os
.path
.exists(self
.lofile
)
297 if self
.device
is not None:
300 losetupProc
= subprocess
.Popen(["/sbin/losetup", "-f"],
301 stdout
=subprocess
.PIPE
)
302 losetupOutput
= losetupProc
.communicate()[0]
304 if losetupProc
.returncode
:
305 raise MountError("Failed to allocate loop device for '%s'" %
308 device
= losetupOutput
.split()[0]
310 logging
.info("Losetup add %s mapping to %s" % (device
, self
.lofile
))
311 rc
= call(["/sbin/losetup", device
, self
.lofile
])
313 raise MountError("Failed to allocate loop device for '%s'" %
318 if self
.device
is None:
320 logging
.info("Losetup remove %s" % self
.device
)
321 rc
= call(["/sbin/losetup", "-d", self
.device
])
326 class SparseLoopbackDisk(LoopbackDisk
):
327 """A Disk backed by a sparse file via the loop module."""
328 def __init__(self
, lofile
, size
):
329 LoopbackDisk
.__init
__(self
, lofile
, size
)
331 def expand(self
, create
= False, size
= None):
335 makedirs(os
.path
.dirname(self
.lofile
))
340 logging
.info("Extending sparse file %s to %d" % (self
.lofile
, size
))
341 fd
= os
.open(self
.lofile
, flags
)
345 os
.lseek(fd
, size
-1, 0)
349 def truncate(self
, size
= None):
353 logging
.info("Truncating sparse file %s to %d" % (self
.lofile
, size
))
354 fd
= os
.open(self
.lofile
, os
.O_WRONLY
)
355 os
.ftruncate(fd
, size
)
359 self
.expand(create
= True)
360 LoopbackDisk
.create(self
)
363 """A generic base class to deal with mounting things."""
364 def __init__(self
, mountdir
):
365 self
.mountdir
= mountdir
376 class DiskMount(Mount
):
377 """A Mount object that handles mounting of a Disk."""
378 def __init__(self
, disk
, mountdir
, fstype
= None, rmmountdir
= True):
379 Mount
.__init
__(self
, mountdir
)
383 self
.rmmountdir
= rmmountdir
394 logging
.info("Unmounting directory %s" % self
.mountdir
)
395 rc
= call(["/bin/umount", self
.mountdir
])
399 logging
.warn("Unmounting directory %s failed, using lazy umount" % self
.mountdir
)
400 print >> sys
.stdout
, "Unmounting directory %s failed, using lazy umount" %self
.mountdir
401 rc
= call(["/bin/umount", "-l", self
.mountdir
])
403 raise MountError("Unable to unmount filesystem at %s" % self
.mountdir
)
405 logging
.info("lazy umount succeeded on %s" % self
.mountdir
)
406 print >> sys
.stdout
, "lazy umount succeeded on %s" % self
.mountdir
409 if self
.rmdir
and not self
.mounted
:
411 os
.rmdir(self
.mountdir
)
425 if not os
.path
.isdir(self
.mountdir
):
426 logging
.info("Creating mount point %s" % self
.mountdir
)
427 os
.makedirs(self
.mountdir
)
428 self
.rmdir
= self
.rmmountdir
432 logging
.info("Mounting %s at %s" % (self
.disk
.device
, self
.mountdir
))
433 args
= [ "/bin/mount", self
.disk
.device
, self
.mountdir
]
435 args
.extend(["-t", self
.fstype
])
439 raise MountError("Failed to mount '%s' to '%s'" %
440 (self
.disk
.device
, self
.mountdir
))
444 class ExtDiskMount(DiskMount
):
445 """A DiskMount object that is able to format/resize ext[23] filesystems."""
446 def __init__(self
, disk
, mountdir
, fstype
, blocksize
, fslabel
,
447 rmmountdir
=True, tmpdir
="/tmp"):
448 DiskMount
.__init
__(self
, disk
, mountdir
, fstype
, rmmountdir
)
449 self
.blocksize
= blocksize
450 self
.fslabel
= "_" + fslabel
453 def __format_filesystem(self
):
454 logging
.info("Formating %s filesystem on %s" % (self
.fstype
, self
.disk
.device
))
455 rc
= call(["/sbin/mkfs." + self
.fstype
,
456 "-F", "-L", self
.fslabel
,
457 "-m", "1", "-b", str(self
.blocksize
),
459 # str(self.disk.size / self.blocksize)])
462 raise MountError("Error creating %s filesystem" % (self
.fstype
,))
463 logging
.info("Tuning filesystem on %s" % self
.disk
.device
)
464 call(["/sbin/tune2fs", "-c0", "-i0", "-Odir_index",
465 "-ouser_xattr,acl", self
.disk
.device
])
467 def __resize_filesystem(self
, size
= None):
468 current_size
= os
.stat(self
.disk
.lofile
)[stat
.ST_SIZE
]
471 size
= self
.disk
.size
473 if size
== current_size
:
476 if size
> current_size
:
477 self
.disk
.expand(size
)
479 resize2fs(self
.disk
.lofile
, size
, tmpdir
= self
.tmpdir
)
484 if not self
.disk
.fixed() and self
.disk
.exists():
490 self
.__resize
_filesystem
()
492 self
.__format
_filesystem
()
496 DiskMount
.mount(self
)
499 return e2fsck(self
.disk
.lofile
)
502 def __get_size_from_filesystem(self
):
503 def parse_field(output
, field
):
504 for line
in output
.split("\n"):
505 if line
.startswith(field
+ ":"):
506 return line
[len(field
) + 1:].strip()
508 raise KeyError("Failed to find field '%s' in output" % field
)
510 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
512 out
= subprocess
.Popen(['/sbin/dumpe2fs', '-h', self
.disk
.lofile
],
513 stdout
= subprocess
.PIPE
,
514 stderr
= dev_null
).communicate()[0]
518 return int(parse_field(out
, "Block count")) * self
.blocksize
520 def __resize_to_minimal(self
):
521 resize2fs(self
.disk
.lofile
, minimal
= True, tmpdir
= self
.tmpdir
)
522 return self
.__get
_size
_from
_filesystem
()
524 def resparse(self
, size
= None):
526 minsize
= self
.__resize
_to
_minimal
()
527 self
.disk
.truncate(minsize
)
528 self
.__resize
_filesystem
(size
)
531 class DeviceMapperSnapshot(object):
532 def __init__(self
, imgloop
, cowloop
):
533 self
.imgloop
= imgloop
534 self
.cowloop
= cowloop
536 self
.__created
= False
540 if self
.__name
is None:
542 return os
.path
.join("/dev/mapper", self
.__name
)
543 path
= property(get_path
)
549 self
.imgloop
.create()
550 self
.cowloop
.create()
552 self
.__name
= "imgcreate-%d-%d" % (os
.getpid(),
553 random
.randint(0, 2**16))
555 size
= os
.stat(self
.imgloop
.lofile
)[stat
.ST_SIZE
]
557 table
= "0 %d snapshot %s %s p 8" % (size
/ 512,
561 args
= ["/sbin/dmsetup", "create", self
.__name
,
562 "--uuid", "LIVECD-%s" % self
.__name
, "--table", table
]
564 self
.cowloop
.cleanup()
565 self
.imgloop
.cleanup()
566 raise SnapshotError("Could not create snapshot device using: " +
567 string
.join(args
, " "))
569 self
.__created
= True
571 def remove(self
, ignore_errors
= False):
572 if not self
.__created
:
575 # sleep to try to avoid any dm shenanigans
577 rc
= call(["/sbin/dmsetup", "remove", self
.__name
])
578 if not ignore_errors
and rc
!= 0:
579 raise SnapshotError("Could not remove snapshot device")
582 self
.__created
= False
584 self
.cowloop
.cleanup()
585 self
.imgloop
.cleanup()
587 def get_cow_used(self
):
588 if not self
.__created
:
591 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
593 out
= subprocess
.Popen(["/sbin/dmsetup", "status", self
.__name
],
594 stdout
= subprocess
.PIPE
,
595 stderr
= dev_null
).communicate()[0]
600 # dmsetup status on a snapshot returns e.g.
601 # "0 8388608 snapshot 416/1048576"
602 # or, more generally:
604 # where C is the number of 512 byte sectors in use
607 return int((out
.split()[3]).split('/')[0]) * 512
609 raise SnapshotError("Failed to parse dmsetup status: " + out
)
611 def create_image_minimizer(path
, image
, compress_type
, target_size
= None,
614 Builds a copy-on-write image which can be used to
615 create a device-mapper snapshot of an image where
616 the image's filesystem is as small as possible
619 1) Create a sparse COW
620 2) Loopback mount the image and the COW
621 3) Create a device-mapper snapshot of the image
623 4) Resize the filesystem to the minimal size
624 5) Determine the amount of space used in the COW
625 6) Restroy the device-mapper snapshot
626 7) Truncate the COW, removing unused space
627 8) Create a squashfs of the COW
629 imgloop
= LoopbackDisk(image
, None) # Passing bogus size - doesn't matter
631 cowloop
= SparseLoopbackDisk(os
.path
.join(os
.path
.dirname(path
), "osmin"),
634 snapshot
= DeviceMapperSnapshot(imgloop
, cowloop
)
639 if target_size
is not None:
640 resize2fs(snapshot
.path
, target_size
, tmpdir
= tmpdir
)
642 resize2fs(snapshot
.path
, minimal
= True, tmpdir
= tmpdir
)
644 cow_used
= snapshot
.get_cow_used()
646 snapshot
.remove(ignore_errors
= (not sys
.exc_info()[0] is None))
648 cowloop
.truncate(cow_used
)
650 mksquashfs(cowloop
.lofile
, path
, compress_type
)
652 os
.unlink(cowloop
.lofile
)