diff options
Diffstat (limited to 'scripts/lib/mic/utils/fs_related.py')
-rw-r--r-- | scripts/lib/mic/utils/fs_related.py | 1029 |
1 files changed, 1029 insertions, 0 deletions
diff --git a/scripts/lib/mic/utils/fs_related.py b/scripts/lib/mic/utils/fs_related.py new file mode 100644 index 0000000000..b9b9a97175 --- /dev/null +++ b/scripts/lib/mic/utils/fs_related.py | |||
@@ -0,0 +1,1029 @@ | |||
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 | from __future__ import with_statement | ||
20 | import os | ||
21 | import sys | ||
22 | import errno | ||
23 | import stat | ||
24 | import random | ||
25 | import string | ||
26 | import time | ||
27 | import uuid | ||
28 | |||
29 | from mic import msger | ||
30 | from mic.utils import runner | ||
31 | from mic.utils.errors import * | ||
32 | |||
33 | |||
34 | def find_binary_inchroot(binary, chroot): | ||
35 | paths = ["/usr/sbin", | ||
36 | "/usr/bin", | ||
37 | "/sbin", | ||
38 | "/bin" | ||
39 | ] | ||
40 | |||
41 | for path in paths: | ||
42 | bin_path = "%s/%s" % (path, binary) | ||
43 | if os.path.exists("%s/%s" % (chroot, bin_path)): | ||
44 | return bin_path | ||
45 | return None | ||
46 | |||
47 | def find_binary_path(binary): | ||
48 | if os.environ.has_key("PATH"): | ||
49 | paths = os.environ["PATH"].split(":") | ||
50 | else: | ||
51 | paths = [] | ||
52 | if os.environ.has_key("HOME"): | ||
53 | paths += [os.environ["HOME"] + "/bin"] | ||
54 | paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"] | ||
55 | |||
56 | for path in paths: | ||
57 | bin_path = "%s/%s" % (path, binary) | ||
58 | if os.path.exists(bin_path): | ||
59 | return bin_path | ||
60 | raise CreatorError("Command '%s' is not available." % binary) | ||
61 | |||
62 | def makedirs(dirname): | ||
63 | """A version of os.makedirs() that doesn't throw an | ||
64 | exception if the leaf directory already exists. | ||
65 | """ | ||
66 | try: | ||
67 | os.makedirs(dirname) | ||
68 | except OSError, err: | ||
69 | if err.errno != errno.EEXIST: | ||
70 | raise | ||
71 | |||
72 | def mksquashfs(in_img, out_img): | ||
73 | fullpathmksquashfs = find_binary_path("mksquashfs") | ||
74 | args = [fullpathmksquashfs, in_img, out_img] | ||
75 | |||
76 | if not sys.stdout.isatty(): | ||
77 | args.append("-no-progress") | ||
78 | |||
79 | ret = runner.show(args) | ||
80 | if ret != 0: | ||
81 | raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret)) | ||
82 | |||
83 | def resize2fs(fs, size): | ||
84 | resize2fs = find_binary_path("resize2fs") | ||
85 | if size == 0: | ||
86 | # it means to minimalize it | ||
87 | return runner.show([resize2fs, '-M', fs]) | ||
88 | else: | ||
89 | return runner.show([resize2fs, fs, "%sK" % (size / 1024,)]) | ||
90 | |||
91 | def my_fuser(fp): | ||
92 | fuser = find_binary_path("fuser") | ||
93 | if not os.path.exists(fp): | ||
94 | return False | ||
95 | |||
96 | rc = runner.quiet([fuser, "-s", fp]) | ||
97 | if rc == 0: | ||
98 | for pid in runner.outs([fuser, fp]).split(): | ||
99 | fd = open("/proc/%s/cmdline" % pid, "r") | ||
100 | cmdline = fd.read() | ||
101 | fd.close() | ||
102 | if cmdline[:-1] == "/bin/bash": | ||
103 | return True | ||
104 | |||
105 | # not found | ||
106 | return False | ||
107 | |||
108 | class BindChrootMount: | ||
109 | """Represents a bind mount of a directory into a chroot.""" | ||
110 | def __init__(self, src, chroot, dest = None, option = None): | ||
111 | self.root = os.path.abspath(os.path.expanduser(chroot)) | ||
112 | self.option = option | ||
113 | |||
114 | self.orig_src = self.src = src | ||
115 | if os.path.islink(src): | ||
116 | self.src = os.readlink(src) | ||
117 | if not self.src.startswith('/'): | ||
118 | self.src = os.path.abspath(os.path.join(os.path.dirname(src), | ||
119 | self.src)) | ||
120 | |||
121 | if not dest: | ||
122 | dest = self.src | ||
123 | self.dest = os.path.join(self.root, dest.lstrip('/')) | ||
124 | |||
125 | self.mounted = False | ||
126 | self.mountcmd = find_binary_path("mount") | ||
127 | self.umountcmd = find_binary_path("umount") | ||
128 | |||
129 | def ismounted(self): | ||
130 | with open('/proc/mounts') as f: | ||
131 | for line in f: | ||
132 | if line.split()[1] == os.path.abspath(self.dest): | ||
133 | return True | ||
134 | |||
135 | return False | ||
136 | |||
137 | def has_chroot_instance(self): | ||
138 | lock = os.path.join(self.root, ".chroot.lock") | ||
139 | return my_fuser(lock) | ||
140 | |||
141 | def mount(self): | ||
142 | if self.mounted or self.ismounted(): | ||
143 | return | ||
144 | |||
145 | makedirs(self.dest) | ||
146 | rc = runner.show([self.mountcmd, "--bind", self.src, self.dest]) | ||
147 | if rc != 0: | ||
148 | raise MountError("Bind-mounting '%s' to '%s' failed" % | ||
149 | (self.src, self.dest)) | ||
150 | if self.option: | ||
151 | rc = runner.show([self.mountcmd, "--bind", "-o", "remount,%s" % self.option, self.dest]) | ||
152 | if rc != 0: | ||
153 | raise MountError("Bind-remounting '%s' failed" % self.dest) | ||
154 | |||
155 | self.mounted = True | ||
156 | if os.path.islink(self.orig_src): | ||
157 | dest = os.path.join(self.root, self.orig_src.lstrip('/')) | ||
158 | if not os.path.exists(dest): | ||
159 | os.symlink(self.src, dest) | ||
160 | |||
161 | def unmount(self): | ||
162 | if self.has_chroot_instance(): | ||
163 | return | ||
164 | |||
165 | if self.ismounted(): | ||
166 | runner.show([self.umountcmd, "-l", self.dest]) | ||
167 | self.mounted = False | ||
168 | |||
169 | class LoopbackMount: | ||
170 | """LoopbackMount compatibility layer for old API""" | ||
171 | def __init__(self, lofile, mountdir, fstype = None): | ||
172 | self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True) | ||
173 | self.losetup = False | ||
174 | self.losetupcmd = find_binary_path("losetup") | ||
175 | |||
176 | def cleanup(self): | ||
177 | self.diskmount.cleanup() | ||
178 | |||
179 | def unmount(self): | ||
180 | self.diskmount.unmount() | ||
181 | |||
182 | def lounsetup(self): | ||
183 | if self.losetup: | ||
184 | runner.show([self.losetupcmd, "-d", self.loopdev]) | ||
185 | self.losetup = False | ||
186 | self.loopdev = None | ||
187 | |||
188 | def loopsetup(self): | ||
189 | if self.losetup: | ||
190 | return | ||
191 | |||
192 | self.loopdev = get_loop_device(self.losetupcmd, self.lofile) | ||
193 | self.losetup = True | ||
194 | |||
195 | def mount(self): | ||
196 | self.diskmount.mount() | ||
197 | |||
198 | class SparseLoopbackMount(LoopbackMount): | ||
199 | """SparseLoopbackMount compatibility layer for old API""" | ||
200 | def __init__(self, lofile, mountdir, size, fstype = None): | ||
201 | self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True) | ||
202 | |||
203 | def expand(self, create = False, size = None): | ||
204 | self.diskmount.disk.expand(create, size) | ||
205 | |||
206 | def truncate(self, size = None): | ||
207 | self.diskmount.disk.truncate(size) | ||
208 | |||
209 | def create(self): | ||
210 | self.diskmount.disk.create() | ||
211 | |||
212 | class SparseExtLoopbackMount(SparseLoopbackMount): | ||
213 | """SparseExtLoopbackMount compatibility layer for old API""" | ||
214 | def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel): | ||
215 | self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True) | ||
216 | |||
217 | |||
218 | def __format_filesystem(self): | ||
219 | self.diskmount.__format_filesystem() | ||
220 | |||
221 | def create(self): | ||
222 | self.diskmount.disk.create() | ||
223 | |||
224 | def resize(self, size = None): | ||
225 | return self.diskmount.__resize_filesystem(size) | ||
226 | |||
227 | def mount(self): | ||
228 | self.diskmount.mount() | ||
229 | |||
230 | def __fsck(self): | ||
231 | self.extdiskmount.__fsck() | ||
232 | |||
233 | def __get_size_from_filesystem(self): | ||
234 | return self.diskmount.__get_size_from_filesystem() | ||
235 | |||
236 | def __resize_to_minimal(self): | ||
237 | return self.diskmount.__resize_to_minimal() | ||
238 | |||
239 | def resparse(self, size = None): | ||
240 | return self.diskmount.resparse(size) | ||
241 | |||
242 | class Disk: | ||
243 | """Generic base object for a disk | ||
244 | |||
245 | The 'create' method must make the disk visible as a block device - eg | ||
246 | by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup' | ||
247 | method must undo the 'create' operation. | ||
248 | """ | ||
249 | def __init__(self, size, device = None): | ||
250 | self._device = device | ||
251 | self._size = size | ||
252 | |||
253 | def create(self): | ||
254 | pass | ||
255 | |||
256 | def cleanup(self): | ||
257 | pass | ||
258 | |||
259 | def get_device(self): | ||
260 | return self._device | ||
261 | def set_device(self, path): | ||
262 | self._device = path | ||
263 | device = property(get_device, set_device) | ||
264 | |||
265 | def get_size(self): | ||
266 | return self._size | ||
267 | size = property(get_size) | ||
268 | |||
269 | |||
270 | class RawDisk(Disk): | ||
271 | """A Disk backed by a block device. | ||
272 | Note that create() is a no-op. | ||
273 | """ | ||
274 | def __init__(self, size, device): | ||
275 | Disk.__init__(self, size, device) | ||
276 | |||
277 | def fixed(self): | ||
278 | return True | ||
279 | |||
280 | def exists(self): | ||
281 | return True | ||
282 | |||
283 | class LoopbackDisk(Disk): | ||
284 | """A Disk backed by a file via the loop module.""" | ||
285 | def __init__(self, lofile, size): | ||
286 | Disk.__init__(self, size) | ||
287 | self.lofile = lofile | ||
288 | self.losetupcmd = find_binary_path("losetup") | ||
289 | |||
290 | def fixed(self): | ||
291 | return False | ||
292 | |||
293 | def exists(self): | ||
294 | return os.path.exists(self.lofile) | ||
295 | |||
296 | def create(self): | ||
297 | if self.device is not None: | ||
298 | return | ||
299 | |||
300 | self.device = get_loop_device(self.losetupcmd, self.lofile) | ||
301 | |||
302 | def cleanup(self): | ||
303 | if self.device is None: | ||
304 | return | ||
305 | msger.debug("Losetup remove %s" % self.device) | ||
306 | rc = runner.show([self.losetupcmd, "-d", self.device]) | ||
307 | self.device = None | ||
308 | |||
309 | class SparseLoopbackDisk(LoopbackDisk): | ||
310 | """A Disk backed by a sparse file via the loop module.""" | ||
311 | def __init__(self, lofile, size): | ||
312 | LoopbackDisk.__init__(self, lofile, size) | ||
313 | |||
314 | def expand(self, create = False, size = None): | ||
315 | flags = os.O_WRONLY | ||
316 | if create: | ||
317 | flags |= os.O_CREAT | ||
318 | if not os.path.exists(self.lofile): | ||
319 | makedirs(os.path.dirname(self.lofile)) | ||
320 | |||
321 | if size is None: | ||
322 | size = self.size | ||
323 | |||
324 | msger.debug("Extending sparse file %s to %d" % (self.lofile, size)) | ||
325 | if create: | ||
326 | fd = os.open(self.lofile, flags, 0644) | ||
327 | else: | ||
328 | fd = os.open(self.lofile, flags) | ||
329 | |||
330 | if size <= 0: | ||
331 | size = 1 | ||
332 | try: | ||
333 | os.ftruncate(fd, size) | ||
334 | except: | ||
335 | # may be limited by 2G in 32bit env | ||
336 | os.ftruncate(fd, 2**31L) | ||
337 | |||
338 | os.close(fd) | ||
339 | |||
340 | def truncate(self, size = None): | ||
341 | if size is None: | ||
342 | size = self.size | ||
343 | |||
344 | msger.debug("Truncating sparse file %s to %d" % (self.lofile, size)) | ||
345 | fd = os.open(self.lofile, os.O_WRONLY) | ||
346 | os.ftruncate(fd, size) | ||
347 | os.close(fd) | ||
348 | |||
349 | def create(self): | ||
350 | self.expand(create = True) | ||
351 | LoopbackDisk.create(self) | ||
352 | |||
353 | class Mount: | ||
354 | """A generic base class to deal with mounting things.""" | ||
355 | def __init__(self, mountdir): | ||
356 | self.mountdir = mountdir | ||
357 | |||
358 | def cleanup(self): | ||
359 | self.unmount() | ||
360 | |||
361 | def mount(self, options = None): | ||
362 | pass | ||
363 | |||
364 | def unmount(self): | ||
365 | pass | ||
366 | |||
367 | class DiskMount(Mount): | ||
368 | """A Mount object that handles mounting of a Disk.""" | ||
369 | def __init__(self, disk, mountdir, fstype = None, rmmountdir = True): | ||
370 | Mount.__init__(self, mountdir) | ||
371 | |||
372 | self.disk = disk | ||
373 | self.fstype = fstype | ||
374 | self.rmmountdir = rmmountdir | ||
375 | |||
376 | self.mounted = False | ||
377 | self.rmdir = False | ||
378 | if fstype: | ||
379 | self.mkfscmd = find_binary_path("mkfs." + self.fstype) | ||
380 | else: | ||
381 | self.mkfscmd = None | ||
382 | self.mountcmd = find_binary_path("mount") | ||
383 | self.umountcmd = find_binary_path("umount") | ||
384 | |||
385 | def cleanup(self): | ||
386 | Mount.cleanup(self) | ||
387 | self.disk.cleanup() | ||
388 | |||
389 | def unmount(self): | ||
390 | if self.mounted: | ||
391 | msger.debug("Unmounting directory %s" % self.mountdir) | ||
392 | runner.quiet('sync') # sync the data on this mount point | ||
393 | rc = runner.show([self.umountcmd, "-l", self.mountdir]) | ||
394 | if rc == 0: | ||
395 | self.mounted = False | ||
396 | else: | ||
397 | raise MountError("Failed to umount %s" % self.mountdir) | ||
398 | if self.rmdir and not self.mounted: | ||
399 | try: | ||
400 | os.rmdir(self.mountdir) | ||
401 | except OSError, e: | ||
402 | pass | ||
403 | self.rmdir = False | ||
404 | |||
405 | |||
406 | def __create(self): | ||
407 | self.disk.create() | ||
408 | |||
409 | |||
410 | def mount(self, options = None): | ||
411 | if self.mounted: | ||
412 | return | ||
413 | |||
414 | if not os.path.isdir(self.mountdir): | ||
415 | msger.debug("Creating mount point %s" % self.mountdir) | ||
416 | os.makedirs(self.mountdir) | ||
417 | self.rmdir = self.rmmountdir | ||
418 | |||
419 | self.__create() | ||
420 | |||
421 | msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir)) | ||
422 | if options: | ||
423 | args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ] | ||
424 | else: | ||
425 | args = [ self.mountcmd, self.disk.device, self.mountdir ] | ||
426 | if self.fstype: | ||
427 | args.extend(["-t", self.fstype]) | ||
428 | |||
429 | rc = runner.show(args) | ||
430 | if rc != 0: | ||
431 | raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" % | ||
432 | (self.disk.device, self.mountdir, " ".join(args), rc)) | ||
433 | |||
434 | self.mounted = True | ||
435 | |||
436 | class ExtDiskMount(DiskMount): | ||
437 | """A DiskMount object that is able to format/resize ext[23] filesystems.""" | ||
438 | def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None): | ||
439 | DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir) | ||
440 | self.blocksize = blocksize | ||
441 | self.fslabel = fslabel.replace("/", "") | ||
442 | self.uuid = str(uuid.uuid4()) | ||
443 | self.skipformat = skipformat | ||
444 | self.fsopts = fsopts | ||
445 | self.extopts = None | ||
446 | self.dumpe2fs = find_binary_path("dumpe2fs") | ||
447 | self.tune2fs = find_binary_path("tune2fs") | ||
448 | |||
449 | def __parse_field(self, output, field): | ||
450 | for line in output.split("\n"): | ||
451 | if line.startswith(field + ":"): | ||
452 | return line[len(field) + 1:].strip() | ||
453 | |||
454 | raise KeyError("Failed to find field '%s' in output" % field) | ||
455 | |||
456 | def __format_filesystem(self): | ||
457 | if self.skipformat: | ||
458 | msger.debug("Skip filesystem format.") | ||
459 | return | ||
460 | |||
461 | msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device)) | ||
462 | cmdlist = [self.mkfscmd, "-F", "-L", self.fslabel, "-m", "1", "-b", | ||
463 | str(self.blocksize), "-U", self.uuid] | ||
464 | if self.extopts: | ||
465 | cmdlist.extend(self.extopts.split()) | ||
466 | cmdlist.extend([self.disk.device]) | ||
467 | |||
468 | rc, errout = runner.runtool(cmdlist, catch=2) | ||
469 | if rc != 0: | ||
470 | raise MountError("Error creating %s filesystem on disk %s:\n%s" % | ||
471 | (self.fstype, self.disk.device, errout)) | ||
472 | |||
473 | if not self.extopts: | ||
474 | msger.debug("Tuning filesystem on %s" % self.disk.device) | ||
475 | runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device]) | ||
476 | |||
477 | def __resize_filesystem(self, size = None): | ||
478 | current_size = os.stat(self.disk.lofile)[stat.ST_SIZE] | ||
479 | |||
480 | if size is None: | ||
481 | size = self.disk.size | ||
482 | |||
483 | if size == current_size: | ||
484 | return | ||
485 | |||
486 | if size > current_size: | ||
487 | self.disk.expand(size) | ||
488 | |||
489 | self.__fsck() | ||
490 | |||
491 | resize2fs(self.disk.lofile, size) | ||
492 | return size | ||
493 | |||
494 | def __create(self): | ||
495 | resize = False | ||
496 | if not self.disk.fixed() and self.disk.exists(): | ||
497 | resize = True | ||
498 | |||
499 | self.disk.create() | ||
500 | |||
501 | if resize: | ||
502 | self.__resize_filesystem() | ||
503 | else: | ||
504 | self.__format_filesystem() | ||
505 | |||
506 | def mount(self, options = None): | ||
507 | self.__create() | ||
508 | DiskMount.mount(self, options) | ||
509 | |||
510 | def __fsck(self): | ||
511 | msger.info("Checking filesystem %s" % self.disk.lofile) | ||
512 | runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile]) | ||
513 | |||
514 | def __get_size_from_filesystem(self): | ||
515 | return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]), | ||
516 | "Block count")) * self.blocksize | ||
517 | |||
518 | def __resize_to_minimal(self): | ||
519 | self.__fsck() | ||
520 | |||
521 | # | ||
522 | # Use a binary search to find the minimal size | ||
523 | # we can resize the image to | ||
524 | # | ||
525 | bot = 0 | ||
526 | top = self.__get_size_from_filesystem() | ||
527 | while top != (bot + 1): | ||
528 | t = bot + ((top - bot) / 2) | ||
529 | |||
530 | if not resize2fs(self.disk.lofile, t): | ||
531 | top = t | ||
532 | else: | ||
533 | bot = t | ||
534 | return top | ||
535 | |||
536 | def resparse(self, size = None): | ||
537 | self.cleanup() | ||
538 | if size == 0: | ||
539 | minsize = 0 | ||
540 | else: | ||
541 | minsize = self.__resize_to_minimal() | ||
542 | self.disk.truncate(minsize) | ||
543 | |||
544 | self.__resize_filesystem(size) | ||
545 | return minsize | ||
546 | |||
547 | class VfatDiskMount(DiskMount): | ||
548 | """A DiskMount object that is able to format vfat/msdos filesystems.""" | ||
549 | def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None): | ||
550 | DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir) | ||
551 | self.blocksize = blocksize | ||
552 | self.fslabel = fslabel.replace("/", "") | ||
553 | rand1 = random.randint(0, 2**16 - 1) | ||
554 | rand2 = random.randint(0, 2**16 - 1) | ||
555 | self.uuid = "%04X-%04X" % (rand1, rand2) | ||
556 | self.skipformat = skipformat | ||
557 | self.fsopts = fsopts | ||
558 | self.fsckcmd = find_binary_path("fsck." + self.fstype) | ||
559 | |||
560 | def __format_filesystem(self): | ||
561 | if self.skipformat: | ||
562 | msger.debug("Skip filesystem format.") | ||
563 | return | ||
564 | |||
565 | msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device)) | ||
566 | rc = runner.show([self.mkfscmd, "-n", self.fslabel, | ||
567 | "-i", self.uuid.replace("-", ""), self.disk.device]) | ||
568 | if rc != 0: | ||
569 | raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device)) | ||
570 | |||
571 | msger.verbose("Tuning filesystem on %s" % self.disk.device) | ||
572 | |||
573 | def __resize_filesystem(self, size = None): | ||
574 | current_size = os.stat(self.disk.lofile)[stat.ST_SIZE] | ||
575 | |||
576 | if size is None: | ||
577 | size = self.disk.size | ||
578 | |||
579 | if size == current_size: | ||
580 | return | ||
581 | |||
582 | if size > current_size: | ||
583 | self.disk.expand(size) | ||
584 | |||
585 | self.__fsck() | ||
586 | |||
587 | #resize2fs(self.disk.lofile, size) | ||
588 | return size | ||
589 | |||
590 | def __create(self): | ||
591 | resize = False | ||
592 | if not self.disk.fixed() and self.disk.exists(): | ||
593 | resize = True | ||
594 | |||
595 | self.disk.create() | ||
596 | |||
597 | if resize: | ||
598 | self.__resize_filesystem() | ||
599 | else: | ||
600 | self.__format_filesystem() | ||
601 | |||
602 | def mount(self, options = None): | ||
603 | self.__create() | ||
604 | DiskMount.mount(self, options) | ||
605 | |||
606 | def __fsck(self): | ||
607 | msger.debug("Checking filesystem %s" % self.disk.lofile) | ||
608 | runner.show([self.fsckcmd, "-y", self.disk.lofile]) | ||
609 | |||
610 | def __get_size_from_filesystem(self): | ||
611 | return self.disk.size | ||
612 | |||
613 | def __resize_to_minimal(self): | ||
614 | self.__fsck() | ||
615 | |||
616 | # | ||
617 | # Use a binary search to find the minimal size | ||
618 | # we can resize the image to | ||
619 | # | ||
620 | bot = 0 | ||
621 | top = self.__get_size_from_filesystem() | ||
622 | return top | ||
623 | |||
624 | def resparse(self, size = None): | ||
625 | self.cleanup() | ||
626 | minsize = self.__resize_to_minimal() | ||
627 | self.disk.truncate(minsize) | ||
628 | self.__resize_filesystem(size) | ||
629 | return minsize | ||
630 | |||
631 | class BtrfsDiskMount(DiskMount): | ||
632 | """A DiskMount object that is able to format/resize btrfs filesystems.""" | ||
633 | def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None): | ||
634 | self.__check_btrfs() | ||
635 | DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir) | ||
636 | self.blocksize = blocksize | ||
637 | self.fslabel = fslabel.replace("/", "") | ||
638 | self.uuid = None | ||
639 | self.skipformat = skipformat | ||
640 | self.fsopts = fsopts | ||
641 | self.blkidcmd = find_binary_path("blkid") | ||
642 | self.btrfsckcmd = find_binary_path("btrfsck") | ||
643 | |||
644 | def __check_btrfs(self): | ||
645 | found = False | ||
646 | """ Need to load btrfs module to mount it """ | ||
647 | load_module("btrfs") | ||
648 | for line in open("/proc/filesystems").xreadlines(): | ||
649 | if line.find("btrfs") > -1: | ||
650 | found = True | ||
651 | break | ||
652 | if not found: | ||
653 | 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.") | ||
654 | |||
655 | # disable selinux, selinux will block write | ||
656 | if os.path.exists("/usr/sbin/setenforce"): | ||
657 | runner.show(["/usr/sbin/setenforce", "0"]) | ||
658 | |||
659 | def __parse_field(self, output, field): | ||
660 | for line in output.split(" "): | ||
661 | if line.startswith(field + "="): | ||
662 | return line[len(field) + 1:].strip().replace("\"", "") | ||
663 | |||
664 | raise KeyError("Failed to find field '%s' in output" % field) | ||
665 | |||
666 | def __format_filesystem(self): | ||
667 | if self.skipformat: | ||
668 | msger.debug("Skip filesystem format.") | ||
669 | return | ||
670 | |||
671 | msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device)) | ||
672 | rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device]) | ||
673 | if rc != 0: | ||
674 | raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device)) | ||
675 | |||
676 | self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID") | ||
677 | |||
678 | def __resize_filesystem(self, size = None): | ||
679 | current_size = os.stat(self.disk.lofile)[stat.ST_SIZE] | ||
680 | |||
681 | if size is None: | ||
682 | size = self.disk.size | ||
683 | |||
684 | if size == current_size: | ||
685 | return | ||
686 | |||
687 | if size > current_size: | ||
688 | self.disk.expand(size) | ||
689 | |||
690 | self.__fsck() | ||
691 | return size | ||
692 | |||
693 | def __create(self): | ||
694 | resize = False | ||
695 | if not self.disk.fixed() and self.disk.exists(): | ||
696 | resize = True | ||
697 | |||
698 | self.disk.create() | ||
699 | |||
700 | if resize: | ||
701 | self.__resize_filesystem() | ||
702 | else: | ||
703 | self.__format_filesystem() | ||
704 | |||
705 | def mount(self, options = None): | ||
706 | self.__create() | ||
707 | DiskMount.mount(self, options) | ||
708 | |||
709 | def __fsck(self): | ||
710 | msger.debug("Checking filesystem %s" % self.disk.lofile) | ||
711 | runner.quiet([self.btrfsckcmd, self.disk.lofile]) | ||
712 | |||
713 | def __get_size_from_filesystem(self): | ||
714 | return self.disk.size | ||
715 | |||
716 | def __resize_to_minimal(self): | ||
717 | self.__fsck() | ||
718 | |||
719 | return self.__get_size_from_filesystem() | ||
720 | |||
721 | def resparse(self, size = None): | ||
722 | self.cleanup() | ||
723 | minsize = self.__resize_to_minimal() | ||
724 | self.disk.truncate(minsize) | ||
725 | self.__resize_filesystem(size) | ||
726 | return minsize | ||
727 | |||
728 | class DeviceMapperSnapshot(object): | ||
729 | def __init__(self, imgloop, cowloop): | ||
730 | self.imgloop = imgloop | ||
731 | self.cowloop = cowloop | ||
732 | |||
733 | self.__created = False | ||
734 | self.__name = None | ||
735 | self.dmsetupcmd = find_binary_path("dmsetup") | ||
736 | |||
737 | """Load dm_snapshot if it isn't loaded""" | ||
738 | load_module("dm_snapshot") | ||
739 | |||
740 | def get_path(self): | ||
741 | if self.__name is None: | ||
742 | return None | ||
743 | return os.path.join("/dev/mapper", self.__name) | ||
744 | path = property(get_path) | ||
745 | |||
746 | def create(self): | ||
747 | if self.__created: | ||
748 | return | ||
749 | |||
750 | self.imgloop.create() | ||
751 | self.cowloop.create() | ||
752 | |||
753 | self.__name = "imgcreate-%d-%d" % (os.getpid(), | ||
754 | random.randint(0, 2**16)) | ||
755 | |||
756 | size = os.stat(self.imgloop.lofile)[stat.ST_SIZE] | ||
757 | |||
758 | table = "0 %d snapshot %s %s p 8" % (size / 512, | ||
759 | self.imgloop.device, | ||
760 | self.cowloop.device) | ||
761 | |||
762 | args = [self.dmsetupcmd, "create", self.__name, "--table", table] | ||
763 | if runner.show(args) != 0: | ||
764 | self.cowloop.cleanup() | ||
765 | self.imgloop.cleanup() | ||
766 | raise SnapshotError("Could not create snapshot device using: " + ' '.join(args)) | ||
767 | |||
768 | self.__created = True | ||
769 | |||
770 | def remove(self, ignore_errors = False): | ||
771 | if not self.__created: | ||
772 | return | ||
773 | |||
774 | time.sleep(2) | ||
775 | rc = runner.show([self.dmsetupcmd, "remove", self.__name]) | ||
776 | if not ignore_errors and rc != 0: | ||
777 | raise SnapshotError("Could not remove snapshot device") | ||
778 | |||
779 | self.__name = None | ||
780 | self.__created = False | ||
781 | |||
782 | self.cowloop.cleanup() | ||
783 | self.imgloop.cleanup() | ||
784 | |||
785 | def get_cow_used(self): | ||
786 | if not self.__created: | ||
787 | return 0 | ||
788 | |||
789 | # | ||
790 | # dmsetup status on a snapshot returns e.g. | ||
791 | # "0 8388608 snapshot 416/1048576" | ||
792 | # or, more generally: | ||
793 | # "A B snapshot C/D" | ||
794 | # where C is the number of 512 byte sectors in use | ||
795 | # | ||
796 | out = runner.outs([self.dmsetupcmd, "status", self.__name]) | ||
797 | try: | ||
798 | return int((out.split()[3]).split('/')[0]) * 512 | ||
799 | except ValueError: | ||
800 | raise SnapshotError("Failed to parse dmsetup status: " + out) | ||
801 | |||
802 | def create_image_minimizer(path, image, minimal_size): | ||
803 | """ | ||
804 | Builds a copy-on-write image which can be used to | ||
805 | create a device-mapper snapshot of an image where | ||
806 | the image's filesystem is as small as possible | ||
807 | |||
808 | The steps taken are: | ||
809 | 1) Create a sparse COW | ||
810 | 2) Loopback mount the image and the COW | ||
811 | 3) Create a device-mapper snapshot of the image | ||
812 | using the COW | ||
813 | 4) Resize the filesystem to the minimal size | ||
814 | 5) Determine the amount of space used in the COW | ||
815 | 6) Restroy the device-mapper snapshot | ||
816 | 7) Truncate the COW, removing unused space | ||
817 | 8) Create a squashfs of the COW | ||
818 | """ | ||
819 | imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter | ||
820 | |||
821 | cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"), | ||
822 | 64L * 1024L * 1024L) | ||
823 | |||
824 | snapshot = DeviceMapperSnapshot(imgloop, cowloop) | ||
825 | |||
826 | try: | ||
827 | snapshot.create() | ||
828 | |||
829 | resize2fs(snapshot.path, minimal_size) | ||
830 | |||
831 | cow_used = snapshot.get_cow_used() | ||
832 | finally: | ||
833 | snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None)) | ||
834 | |||
835 | cowloop.truncate(cow_used) | ||
836 | |||
837 | mksquashfs(cowloop.lofile, path) | ||
838 | |||
839 | os.unlink(cowloop.lofile) | ||
840 | |||
841 | def load_module(module): | ||
842 | found = False | ||
843 | for line in open('/proc/modules').xreadlines(): | ||
844 | if line.startswith("%s " % module): | ||
845 | found = True | ||
846 | break | ||
847 | if not found: | ||
848 | msger.info("Loading %s..." % module) | ||
849 | runner.quiet(['modprobe', module]) | ||
850 | |||
851 | class LoopDevice(object): | ||
852 | def __init__(self, loopid=None): | ||
853 | self.device = None | ||
854 | self.loopid = loopid | ||
855 | self.created = False | ||
856 | self.kpartxcmd = find_binary_path("kpartx") | ||
857 | self.losetupcmd = find_binary_path("losetup") | ||
858 | |||
859 | def register(self, device): | ||
860 | self.device = device | ||
861 | self.loopid = None | ||
862 | self.created = True | ||
863 | |||
864 | def reg_atexit(self): | ||
865 | import atexit | ||
866 | atexit.register(self.close) | ||
867 | |||
868 | def _genloopid(self): | ||
869 | import glob | ||
870 | if not glob.glob("/dev/loop[0-9]*"): | ||
871 | return 10 | ||
872 | |||
873 | fint = lambda x: x[9:].isdigit() and int(x[9:]) or 0 | ||
874 | maxid = 1 + max(filter(lambda x: x<100, | ||
875 | map(fint, glob.glob("/dev/loop[0-9]*")))) | ||
876 | if maxid < 10: maxid = 10 | ||
877 | if maxid >= 100: raise | ||
878 | return maxid | ||
879 | |||
880 | def _kpseek(self, device): | ||
881 | rc, out = runner.runtool([self.kpartxcmd, '-l', '-v', device]) | ||
882 | if rc != 0: | ||
883 | raise MountError("Can't query dm snapshot on %s" % device) | ||
884 | for line in out.splitlines(): | ||
885 | if line and line.startswith("loop"): | ||
886 | return True | ||
887 | return False | ||
888 | |||
889 | def _loseek(self, device): | ||
890 | import re | ||
891 | rc, out = runner.runtool([self.losetupcmd, '-a']) | ||
892 | if rc != 0: | ||
893 | raise MountError("Failed to run 'losetup -a'") | ||
894 | for line in out.splitlines(): | ||
895 | m = re.match("([^:]+): .*", line) | ||
896 | if m and m.group(1) == device: | ||
897 | return True | ||
898 | return False | ||
899 | |||
900 | def create(self): | ||
901 | if not self.created: | ||
902 | if not self.loopid: | ||
903 | self.loopid = self._genloopid() | ||
904 | self.device = "/dev/loop%d" % self.loopid | ||
905 | if os.path.exists(self.device): | ||
906 | if self._loseek(self.device): | ||
907 | raise MountError("Device busy: %s" % self.device) | ||
908 | else: | ||
909 | self.created = True | ||
910 | return | ||
911 | |||
912 | mknod = find_binary_path('mknod') | ||
913 | rc = runner.show([mknod, '-m664', self.device, 'b', '7', str(self.loopid)]) | ||
914 | if rc != 0: | ||
915 | raise MountError("Failed to create device %s" % self.device) | ||
916 | else: | ||
917 | self.created = True | ||
918 | |||
919 | def close(self): | ||
920 | if self.created: | ||
921 | try: | ||
922 | self.cleanup() | ||
923 | self.device = None | ||
924 | except MountError, e: | ||
925 | msger.error("%s" % e) | ||
926 | |||
927 | def cleanup(self): | ||
928 | |||
929 | if self.device is None: | ||
930 | return | ||
931 | |||
932 | |||
933 | if self._kpseek(self.device): | ||
934 | if self.created: | ||
935 | for i in range(3, os.sysconf("SC_OPEN_MAX")): | ||
936 | try: | ||
937 | os.close(i) | ||
938 | except: | ||
939 | pass | ||
940 | runner.quiet([self.kpartxcmd, "-d", self.device]) | ||
941 | if self._loseek(self.device): | ||
942 | runner.quiet([self.losetupcmd, "-d", self.device]) | ||
943 | # FIXME: should sleep a while between two loseek | ||
944 | if self._loseek(self.device): | ||
945 | msger.warning("Can't cleanup loop device %s" % self.device) | ||
946 | elif self.loopid: | ||
947 | os.unlink(self.device) | ||
948 | |||
949 | DEVICE_PIDFILE_DIR = "/var/tmp/mic/device" | ||
950 | DEVICE_LOCKFILE = "/var/lock/__mic_loopdev.lock" | ||
951 | |||
952 | def get_loop_device(losetupcmd, lofile): | ||
953 | global DEVICE_PIDFILE_DIR | ||
954 | global DEVICE_LOCKFILE | ||
955 | |||
956 | import fcntl | ||
957 | makedirs(os.path.dirname(DEVICE_LOCKFILE)) | ||
958 | fp = open(DEVICE_LOCKFILE, 'w') | ||
959 | fcntl.flock(fp, fcntl.LOCK_EX) | ||
960 | try: | ||
961 | loopdev = None | ||
962 | devinst = LoopDevice() | ||
963 | |||
964 | # clean up left loop device first | ||
965 | clean_loop_devices() | ||
966 | |||
967 | # provide an avaible loop device | ||
968 | rc, out = runner.runtool([losetupcmd, "--find"]) | ||
969 | if rc == 0: | ||
970 | loopdev = out.split()[0] | ||
971 | devinst.register(loopdev) | ||
972 | if not loopdev or not os.path.exists(loopdev): | ||
973 | devinst.create() | ||
974 | loopdev = devinst.device | ||
975 | |||
976 | # setup a loop device for image file | ||
977 | rc = runner.show([losetupcmd, loopdev, lofile]) | ||
978 | if rc != 0: | ||
979 | raise MountError("Failed to setup loop device for '%s'" % lofile) | ||
980 | |||
981 | devinst.reg_atexit() | ||
982 | |||
983 | # try to save device and pid | ||
984 | makedirs(DEVICE_PIDFILE_DIR) | ||
985 | pidfile = os.path.join(DEVICE_PIDFILE_DIR, os.path.basename(loopdev)) | ||
986 | if os.path.exists(pidfile): | ||
987 | os.unlink(pidfile) | ||
988 | with open(pidfile, 'w') as wf: | ||
989 | wf.write(str(os.getpid())) | ||
990 | |||
991 | except MountError, err: | ||
992 | raise CreatorError("%s" % str(err)) | ||
993 | except: | ||
994 | raise | ||
995 | finally: | ||
996 | try: | ||
997 | fcntl.flock(fp, fcntl.LOCK_UN) | ||
998 | fp.close() | ||
999 | os.unlink(DEVICE_LOCKFILE) | ||
1000 | except: | ||
1001 | pass | ||
1002 | |||
1003 | return loopdev | ||
1004 | |||
1005 | def clean_loop_devices(piddir=DEVICE_PIDFILE_DIR): | ||
1006 | if not os.path.exists(piddir) or not os.path.isdir(piddir): | ||
1007 | return | ||
1008 | |||
1009 | for loopdev in os.listdir(piddir): | ||
1010 | pidfile = os.path.join(piddir, loopdev) | ||
1011 | try: | ||
1012 | with open(pidfile, 'r') as rf: | ||
1013 | devpid = int(rf.read()) | ||
1014 | except: | ||
1015 | devpid = None | ||
1016 | |||
1017 | # if the process using this device is alive, skip it | ||
1018 | if not devpid or os.path.exists(os.path.join('/proc', str(devpid))): | ||
1019 | continue | ||
1020 | |||
1021 | # try to clean it up | ||
1022 | try: | ||
1023 | devinst = LoopDevice() | ||
1024 | devinst.register(os.path.join('/dev', loopdev)) | ||
1025 | devinst.cleanup() | ||
1026 | os.unlink(pidfile) | ||
1027 | except: | ||
1028 | pass | ||
1029 | |||