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 mksquashfs(in_img
, out_img
, compress_type
):
45 # Allow gzip to work for older versions of mksquashfs
46 if compress_type
== "gzip":
47 args
= ["/sbin/mksquashfs", in_img
, out_img
]
49 args
= ["/sbin/mksquashfs", in_img
, out_img
, "-comp", compress_type
]
51 if not sys
.stdout
.isatty():
52 args
.append("-no-progress")
56 raise SquashfsError("'%s' exited with error (%d)" %
57 (string
.join(args
, " "), ret
))
59 def resize2fs(fs
, size
= None, minimal
= False):
60 if minimal
and size
is not None:
61 raise ResizeError("Can't specify both minimal and a size for resize!")
62 if not minimal
and size
is None:
63 raise ResizeError("Must specify either a size or minimal for resize!")
66 (fd
, saved_image
) = tempfile
.mkstemp("", "resize-image-", "/tmp")
68 call(["/sbin/e2image", "-r", fs
, saved_image
])
70 args
= ["/sbin/resize2fs", fs
]
74 args
.append("%sK" %(size
/ 1024,))
77 raise ResizeError("resize2fs returned an error (%d)! image to debug at %s" %(ret
, saved_image
))
80 raise ResizeError("fsck after resize returned an error! image to debug at %s" %(saved_image
,))
81 os
.unlink(saved_image
)
85 logging
.info("Checking filesystem %s" % fs
)
86 rc
= call(["/sbin/e2fsck", "-f", "-y", fs
])
89 class BindChrootMount
:
90 """Represents a bind mount of a directory into a chroot."""
91 def __init__(self
, src
, chroot
, dest
= None):
97 self
.dest
= self
.root
+ "/" + dest
106 rc
= call(["/bin/mount", "--bind", self
.src
, self
.dest
])
108 raise MountError("Bind-mounting '%s' to '%s' failed" %
109 (self
.src
, self
.dest
))
116 rc
= call(["/bin/umount", self
.dest
])
118 logging
.info("Unable to unmount %s normally, using lazy unmount" % self
.dest
)
119 rc
= call(["/bin/umount", "-l", self
.dest
])
121 raise MountError("Unable to unmount fs at %s" % self
.dest
)
123 logging
.info("lazy umount succeeded on %s" % self
.dest
)
124 print >> sys
.stdout
, "lazy umount succeeded on %s" % self
.dest
129 """LoopbackMount compatibility layer for old API"""
130 def __init__(self
, lofile
, mountdir
, fstype
= None):
131 self
.diskmount
= DiskMount(LoopbackDisk(lofile
,size
= 0),mountdir
,fstype
,rmmountdir
= True)
135 self
.diskmount
.cleanup()
138 self
.diskmount
.unmount()
142 rc
= call(["/sbin/losetup", "-d", self
.loopdev
])
150 losetupProc
= subprocess
.Popen(["/sbin/losetup", "-f"],
151 stdout
=subprocess
.PIPE
)
152 losetupOutput
= losetupProc
.communicate()[0]
154 if losetupProc
.returncode
:
155 raise MountError("Failed to allocate loop device for '%s'" %
158 self
.loopdev
= losetupOutput
.split()[0]
160 rc
= call(["/sbin/losetup", self
.loopdev
, self
.lofile
])
162 raise MountError("Failed to allocate loop device for '%s'" %
168 self
.diskmount
.mount()
170 class SparseLoopbackMount(LoopbackMount
):
171 """SparseLoopbackMount compatibility layer for old API"""
172 def __init__(self
, lofile
, mountdir
, size
, fstype
= None):
173 self
.diskmount
= DiskMount(SparseLoopbackDisk(lofile
,size
),mountdir
,fstype
,rmmountdir
= True)
175 def expand(self
, create
= False, size
= None):
176 self
.diskmount
.disk
.expand(create
, size
)
178 def truncate(self
, size
= None):
179 self
.diskmount
.disk
.truncate(size
)
182 self
.diskmount
.disk
.create()
184 class SparseExtLoopbackMount(SparseLoopbackMount
):
185 """SparseExtLoopbackMount compatibility layer for old API"""
186 def __init__(self
, lofile
, mountdir
, size
, fstype
, blocksize
, fslabel
):
187 self
.diskmount
= ExtDiskMount(SparseLoopbackDisk(lofile
,size
), mountdir
, fstype
, blocksize
, fslabel
, rmmountdir
= True)
190 def __format_filesystem(self
):
191 self
.diskmount
.__format
_filesystem
()
194 self
.diskmount
.disk
.create()
196 def resize(self
, size
= None):
197 return self
.diskmount
.__resize
_filesystem
(size
)
200 self
.diskmount
.mount()
203 self
.extdiskmount
.__fsck
()
205 def __get_size_from_filesystem(self
):
206 return self
.diskmount
.__get
_size
_from
_filesystem
()
208 def __resize_to_minimal(self
):
209 return self
.diskmount
.__resize
_to
_minimal
()
211 def resparse(self
, size
= None):
212 return self
.diskmount
.resparse(size
)
215 """Generic base object for a disk
217 The 'create' method must make the disk visible as a block device - eg
218 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
219 method must undo the 'create' operation.
221 def __init__(self
, size
, device
= None):
222 self
._device
= device
231 def get_device(self
):
233 def set_device(self
, path
):
235 device
= property(get_device
, set_device
)
239 size
= property(get_size
)
243 """A Disk backed by a block device.
244 Note that create() is a no-op.
246 def __init__(self
, size
, device
):
247 Disk
.__init
__(self
, size
, device
)
255 class LoopbackDisk(Disk
):
256 """A Disk backed by a file via the loop module."""
257 def __init__(self
, lofile
, size
):
258 Disk
.__init
__(self
, size
)
265 return os
.path
.exists(self
.lofile
)
268 if self
.device
is not None:
271 losetupProc
= subprocess
.Popen(["/sbin/losetup", "-f"],
272 stdout
=subprocess
.PIPE
)
273 losetupOutput
= losetupProc
.communicate()[0]
275 if losetupProc
.returncode
:
276 raise MountError("Failed to allocate loop device for '%s'" %
279 device
= losetupOutput
.split()[0]
281 logging
.info("Losetup add %s mapping to %s" % (device
, self
.lofile
))
282 rc
= call(["/sbin/losetup", device
, self
.lofile
])
284 raise MountError("Failed to allocate loop device for '%s'" %
289 if self
.device
is None:
291 logging
.info("Losetup remove %s" % self
.device
)
292 rc
= call(["/sbin/losetup", "-d", self
.device
])
297 class SparseLoopbackDisk(LoopbackDisk
):
298 """A Disk backed by a sparse file via the loop module."""
299 def __init__(self
, lofile
, size
):
300 LoopbackDisk
.__init
__(self
, lofile
, size
)
302 def expand(self
, create
= False, size
= None):
306 makedirs(os
.path
.dirname(self
.lofile
))
311 logging
.info("Extending sparse file %s to %d" % (self
.lofile
, size
))
312 fd
= os
.open(self
.lofile
, flags
)
316 os
.lseek(fd
, size
-1, 0)
320 def truncate(self
, size
= None):
324 logging
.info("Truncating sparse file %s to %d" % (self
.lofile
, size
))
325 fd
= os
.open(self
.lofile
, os
.O_WRONLY
)
326 os
.ftruncate(fd
, size
)
330 self
.expand(create
= True)
331 LoopbackDisk
.create(self
)
334 """A generic base class to deal with mounting things."""
335 def __init__(self
, mountdir
):
336 self
.mountdir
= mountdir
347 class DiskMount(Mount
):
348 """A Mount object that handles mounting of a Disk."""
349 def __init__(self
, disk
, mountdir
, fstype
= None, rmmountdir
= True):
350 Mount
.__init
__(self
, mountdir
)
354 self
.rmmountdir
= rmmountdir
365 logging
.info("Unmounting directory %s" % self
.mountdir
)
366 rc
= call(["/bin/umount", self
.mountdir
])
370 logging
.warn("Unmounting directory %s failed, using lazy umount" % self
.mountdir
)
371 print >> sys
.stdout
, "Unmounting directory %s failed, using lazy umount" %self
.mountdir
372 rc
= call(["/bin/umount", "-l", self
.mountdir
])
374 raise MountError("Unable to unmount filesystem at %s" % self
.mountdir
)
376 logging
.info("lazy umount succeeded on %s" % self
.mountdir
)
377 print >> sys
.stdout
, "lazy umount succeeded on %s" % self
.mountdir
380 if self
.rmdir
and not self
.mounted
:
382 os
.rmdir(self
.mountdir
)
396 if not os
.path
.isdir(self
.mountdir
):
397 logging
.info("Creating mount point %s" % self
.mountdir
)
398 os
.makedirs(self
.mountdir
)
399 self
.rmdir
= self
.rmmountdir
403 logging
.info("Mounting %s at %s" % (self
.disk
.device
, self
.mountdir
))
404 args
= [ "/bin/mount", self
.disk
.device
, self
.mountdir
]
406 args
.extend(["-t", self
.fstype
])
410 raise MountError("Failed to mount '%s' to '%s'" %
411 (self
.disk
.device
, self
.mountdir
))
415 class ExtDiskMount(DiskMount
):
416 """A DiskMount object that is able to format/resize ext[23] filesystems."""
417 def __init__(self
, disk
, mountdir
, fstype
, blocksize
, fslabel
, rmmountdir
=True):
418 DiskMount
.__init
__(self
, disk
, mountdir
, fstype
, rmmountdir
)
419 self
.blocksize
= blocksize
420 self
.fslabel
= "_" + fslabel
422 def __format_filesystem(self
):
423 logging
.info("Formating %s filesystem on %s" % (self
.fstype
, self
.disk
.device
))
424 rc
= call(["/sbin/mkfs." + self
.fstype
,
425 "-F", "-L", self
.fslabel
,
426 "-m", "1", "-b", str(self
.blocksize
),
428 # str(self.disk.size / self.blocksize)])
431 raise MountError("Error creating %s filesystem" % (self
.fstype
,))
432 logging
.info("Tuning filesystem on %s" % self
.disk
.device
)
433 call(["/sbin/tune2fs", "-c0", "-i0", "-Odir_index",
434 "-ouser_xattr,acl", self
.disk
.device
])
436 def __resize_filesystem(self
, size
= None):
437 current_size
= os
.stat(self
.disk
.lofile
)[stat
.ST_SIZE
]
440 size
= self
.disk
.size
442 if size
== current_size
:
445 if size
> current_size
:
446 self
.disk
.expand(size
)
448 resize2fs(self
.disk
.lofile
, size
)
453 if not self
.disk
.fixed() and self
.disk
.exists():
459 self
.__resize
_filesystem
()
461 self
.__format
_filesystem
()
465 DiskMount
.mount(self
)
468 return e2fsck(self
.disk
.lofile
)
471 def __get_size_from_filesystem(self
):
472 def parse_field(output
, field
):
473 for line
in output
.split("\n"):
474 if line
.startswith(field
+ ":"):
475 return line
[len(field
) + 1:].strip()
477 raise KeyError("Failed to find field '%s' in output" % field
)
479 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
481 out
= subprocess
.Popen(['/sbin/dumpe2fs', '-h', self
.disk
.lofile
],
482 stdout
= subprocess
.PIPE
,
483 stderr
= dev_null
).communicate()[0]
487 return int(parse_field(out
, "Block count")) * self
.blocksize
489 def __resize_to_minimal(self
):
490 resize2fs(self
.disk
.lofile
, minimal
= True)
491 return self
.__get
_size
_from
_filesystem
()
493 def resparse(self
, size
= None):
495 minsize
= self
.__resize
_to
_minimal
()
496 self
.disk
.truncate(minsize
)
497 self
.__resize
_filesystem
(size
)
500 class DeviceMapperSnapshot(object):
501 def __init__(self
, imgloop
, cowloop
):
502 self
.imgloop
= imgloop
503 self
.cowloop
= cowloop
505 self
.__created
= False
509 if self
.__name
is None:
511 return os
.path
.join("/dev/mapper", self
.__name
)
512 path
= property(get_path
)
518 self
.imgloop
.create()
519 self
.cowloop
.create()
521 self
.__name
= "imgcreate-%d-%d" % (os
.getpid(),
522 random
.randint(0, 2**16))
524 size
= os
.stat(self
.imgloop
.lofile
)[stat
.ST_SIZE
]
526 table
= "0 %d snapshot %s %s p 8" % (size
/ 512,
530 args
= ["/sbin/dmsetup", "create", self
.__name
,
531 "--uuid", "LIVECD-%s" % self
.__name
, "--table", table
]
533 self
.cowloop
.cleanup()
534 self
.imgloop
.cleanup()
535 raise SnapshotError("Could not create snapshot device using: " +
536 string
.join(args
, " "))
538 self
.__created
= True
540 def remove(self
, ignore_errors
= False):
541 if not self
.__created
:
544 # sleep to try to avoid any dm shenanigans
546 rc
= call(["/sbin/dmsetup", "remove", self
.__name
])
547 if not ignore_errors
and rc
!= 0:
548 raise SnapshotError("Could not remove snapshot device")
551 self
.__created
= False
553 self
.cowloop
.cleanup()
554 self
.imgloop
.cleanup()
556 def get_cow_used(self
):
557 if not self
.__created
:
560 dev_null
= os
.open("/dev/null", os
.O_WRONLY
)
562 out
= subprocess
.Popen(["/sbin/dmsetup", "status", self
.__name
],
563 stdout
= subprocess
.PIPE
,
564 stderr
= dev_null
).communicate()[0]
569 # dmsetup status on a snapshot returns e.g.
570 # "0 8388608 snapshot 416/1048576"
571 # or, more generally:
573 # where C is the number of 512 byte sectors in use
576 return int((out
.split()[3]).split('/')[0]) * 512
578 raise SnapshotError("Failed to parse dmsetup status: " + out
)
580 def create_image_minimizer(path
, image
, compress_type
, target_size
= None):
582 Builds a copy-on-write image which can be used to
583 create a device-mapper snapshot of an image where
584 the image's filesystem is as small as possible
587 1) Create a sparse COW
588 2) Loopback mount the image and the COW
589 3) Create a device-mapper snapshot of the image
591 4) Resize the filesystem to the minimal size
592 5) Determine the amount of space used in the COW
593 6) Restroy the device-mapper snapshot
594 7) Truncate the COW, removing unused space
595 8) Create a squashfs of the COW
597 imgloop
= LoopbackDisk(image
, None) # Passing bogus size - doesn't matter
599 cowloop
= SparseLoopbackDisk(os
.path
.join(os
.path
.dirname(path
), "osmin"),
602 snapshot
= DeviceMapperSnapshot(imgloop
, cowloop
)
607 if target_size
is not None:
608 resize2fs(snapshot
.path
, target_size
)
610 resize2fs(snapshot
.path
, minimal
= True)
612 cow_used
= snapshot
.get_cow_used()
614 snapshot
.remove(ignore_errors
= (not sys
.exc_info()[0] is None))
616 cowloop
.truncate(cow_used
)
618 mksquashfs(cowloop
.lofile
, path
, compress_type
)
620 os
.unlink(cowloop
.lofile
)