3 # Copyright (c) 2011 Intel, Inc.
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; version 2 of the License
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc., 59
16 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22 from mic import kickstart, msger
23 from mic.utils.errors import CreatorError, MountError
24 from mic.utils import misc, runner, fs_related as fs
25 from mic.imager.baseimager import BaseImageCreator
26 from mic.archive import packing, compressing
29 # The maximum string length supported for LoopImageCreator.fslabel
31 # Support for Read-only file system list
32 AFTER_MNT_FS = {"squashfs":".sqsh", "vdfs":".vdfs"}
34 def save_mountpoints(fpath, loops, arch = None):
35 """Save mount points mapping to file
37 :fpath, the xml file to store partition info
38 :loops, dict of partition info
42 if not fpath or not loops:
45 from xml.dom import minidom
46 doc = minidom.Document()
47 imgroot = doc.createElement("image")
48 doc.appendChild(imgroot)
50 imgroot.setAttribute('arch', arch)
52 part = doc.createElement("partition")
53 imgroot.appendChild(part)
54 for (key, val) in loop.items():
55 if isinstance(val, fs.Mount):
57 part.setAttribute(key, str(val))
59 with open(fpath, 'w') as wf:
60 wf.write(doc.toprettyxml(indent=' '))
64 def load_mountpoints(fpath):
65 """Load mount points mapping from file
67 :fpath, file path to load
73 from xml.dom import minidom
75 with open(fpath, 'r') as rf:
76 dom = minidom.parse(rf)
77 imgroot = dom.documentElement
78 for part in imgroot.getElementsByTagName("partition"):
79 p = dict(part.attributes.items())
82 mp = (p['mountpoint'], p['label'], p['name'],
83 int(p['size']), p['fstype'])
85 msger.warning("Wrong format line in file: %s" % fpath)
87 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
93 class LoopImageCreator(BaseImageCreator):
94 """Installs a system into a loopback-mountable filesystem image.
96 LoopImageCreator is a straightforward ImageCreator subclass; the system
97 is installed into an ext3 filesystem on a sparse file which can be
98 subsequently loopback-mounted.
100 When specifying multiple partitions in kickstart file, each partition
101 will be created as a separated loop image.
105 def __init__(self, creatoropts=None, pkgmgr=None,
108 """Initialize a LoopImageCreator instance.
110 This method takes the same arguments as ImageCreator.__init__()
111 with the addition of:
113 fslabel -- A string used as a label for any filesystems created.
116 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
118 self.compress_image = compress_image
119 self.shrink_image = shrink_image
121 self.__fslabel = None
122 self.fslabel = self.name
124 self.__blocksize = 4096
126 self.__fstype = kickstart.get_image_fstype(self.ks,
128 self.__fsopts = kickstart.get_image_fsopts(self.ks,
130 if self.__fstype in AFTER_MNT_FS.keys():
131 self.__fstype = "ext4"
134 for part in sorted(kickstart.get_partitions(self.ks),
135 key=lambda p: p.mountpoint):
137 if part.fstype == "swap":
139 elif part.fstype in AFTER_MNT_FS.keys():
140 aft_fstype = part.fstype
152 msger.warning('no "label" specified for loop img at %s'
153 ', use the mountpoint as the name' % mp)
154 label = mp.split('/')[-1]
156 imgname = misc.strip_end(label, '.img') + '.img'
161 'size': part.size or 4096L * 1024 * 1024,
162 'fstype': part.fstype or 'ext3',
163 'aft_fstype': aft_fstype or None,
164 'extopts': part.extopts or None,
165 'vdfsopts': part.vdfsopts or None,
166 'squashfsopts': part.squashfsopts or None,
167 'loop': None, # to be created in _mount_instroot
168 'uuid': part.uuid or None,
170 'exclude_image' : part.exclude_image or None,
172 self._instloops = allloops
182 self.__image_size = kickstart.get_image_size(self.ks,
185 self.__image_size = 0
187 self._img_name = self.name + ".img"
189 def get_image_names(self):
190 if not self._instloops:
194 for lo in self._instloops :
195 names.append(lo['name'])
196 for ro in AFTER_MNT_FS.values():
197 names.append(lo['name'].replace('.img',ro))
198 return list(set(names))
200 def _set_fstype(self, fstype):
201 self.__fstype = fstype
203 def _set_image_size(self, imgsize):
204 self.__image_size = imgsize
210 def __get_fslabel(self):
211 if self.__fslabel is None:
214 return self.__fslabel
215 def __set_fslabel(self, val):
217 self.__fslabel = None
219 self.__fslabel = val[:FSLABEL_MAXLEN]
220 #A string used to label any filesystems created.
222 #Some filesystems impose a constraint on the maximum allowed size of the
223 #filesystem label. In the case of ext3 it's 16 characters, but in the case
224 #of ISO9660 it's 32 characters.
226 #mke2fs silently truncates the label, but mkisofs aborts if the label is
227 #too long. So, for convenience sake, any string assigned to this attribute
228 #is silently truncated to FSLABEL_MAXLEN (32) characters.
229 fslabel = property(__get_fslabel, __set_fslabel)
231 def __get_image(self):
232 if self._imgdir is None:
233 raise CreatorError("_image is not valid before calling mount()")
234 return os.path.join(self._imgdir, self._img_name)
235 #The location of the image file.
237 #This is the path to the filesystem image. Subclasses may use this path
238 #in order to package the image in _stage_final_image().
240 #Note, this directory does not exist before ImageCreator.mount() is called.
242 #Note also, this is a read-only attribute.
243 _image = property(__get_image)
245 def __get_blocksize(self):
246 return self.__blocksize
247 def __set_blocksize(self, val):
249 raise CreatorError("_blocksize must be set before calling mount()")
251 self.__blocksize = int(val)
253 raise CreatorError("'%s' is not a valid integer value "
254 "for _blocksize" % val)
255 #The block size used by the image's filesystem.
257 #This is the block size used when creating the filesystem image. Subclasses
258 #may change this if they wish to use something other than a 4k block size.
260 #Note, this attribute may only be set before calling mount().
261 _blocksize = property(__get_blocksize, __set_blocksize)
263 def __get_fstype(self):
265 def __set_fstype(self, val):
266 if val != "ext2" and val != "ext3":
267 raise CreatorError("Unknown _fstype '%s' supplied" % val)
269 #The type of filesystem used for the image.
271 #This is the filesystem type used when creating the filesystem image.
272 #Subclasses may change this if they wish to use something other ext3.
274 #Note, only ext2 and ext3 are currently supported.
276 #Note also, this attribute may only be set before calling mount().
277 _fstype = property(__get_fstype, __set_fstype)
279 def __get_fsopts(self):
281 def __set_fsopts(self, val):
283 #Mount options of filesystem used for the image.
285 #This can be specified by --fsoptions="xxx,yyy" in part command in
287 _fsopts = property(__get_fsopts, __set_fsopts)
291 # Helpers for subclasses
293 def _resparse(self, size=None):
294 """Rebuild the filesystem image to be as sparse as possible.
296 This method should be used by subclasses when staging the final image
297 in order to reduce the actual space taken up by the sparse image file
298 to be as little as possible.
300 This is done by resizing the filesystem to the minimal size (thereby
301 eliminating any space taken up by deleted files) and then resizing it
302 back to the supplied size.
304 size -- the size in, in bytes, which the filesystem image should be
305 resized to after it has been minimized; this defaults to None,
306 causing the original size specified by the kickstart file to
307 be used (or 4GiB if not specified in the kickstart).
310 for item in self._instloops:
311 if item['name'] == self._img_name:
312 minsize = item['loop'].resparse(size)
314 item['loop'].resparse(size)
318 def _base_on(self, base_on=None):
319 if base_on and self._image != base_on:
320 shutil.copyfile(base_on, self._image)
322 def _check_imgdir(self):
323 if self._imgdir is None:
324 self._imgdir = self._mkdtemp()
328 # Actual implementation
330 def _mount_instroot(self, base_on=None):
332 if base_on and os.path.isfile(base_on):
333 self._imgdir = os.path.dirname(base_on)
334 imgname = os.path.basename(base_on)
335 self._base_on(base_on)
336 self._set_image_size(misc.get_file_size(self._image))
338 # here, self._instloops must be []
339 self._instloops.append({
343 "size": self.__image_size or 4096L,
344 "fstype": self.__fstype or "ext3",
349 "exclude_image" : None
354 for loop in self._instloops:
355 fstype = loop['fstype']
356 mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
357 size = loop['size'] * 1024L * 1024L
358 imgname = loop['name']
360 if fstype in ("ext2", "ext3", "ext4"):
361 MyDiskMount = fs.ExtDiskMount
362 elif fstype == "btrfs":
363 MyDiskMount = fs.BtrfsDiskMount
364 elif fstype in ("vfat", "msdos"):
365 MyDiskMount = fs.VfatDiskMount
367 raise MountError('Cannot support fstype: %s' % fstype)
369 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
370 os.path.join(self._imgdir, imgname),
376 fsuuid = loop['uuid'])
378 if fstype in ("ext2", "ext3", "ext4"):
379 loop['loop'].extopts = loop['extopts']
382 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
385 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
386 if loop['kspart'] and loop['kspart'].uuid is None and \
388 loop['kspart'].uuid = loop['loop'].uuid
390 except MountError, e:
393 def _unmount_instroot(self):
394 for item in reversed(self._instloops):
396 item['loop'].cleanup()
400 def _stage_final_image(self):
402 if self.pack_to or self.shrink_image:
407 for item in self._instloops:
408 imgfile = os.path.join(self._imgdir, item['name'])
410 if item['aft_fstype'] in AFTER_MNT_FS.keys():
411 mountpoint = misc.mkdtemp()
412 ext4img = os.path.join(self._imgdir, item['name'])
413 runner.show('mount -t ext4 %s %s' % (ext4img, mountpoint))
414 runner.show('ls -al %s' % (mountpoint))
415 # item['loop'].mount(None, 'not_create')
416 # point_mnt = os.path.join(self._instroot, item['mountpoint'].lstrip('/'))
418 fs_suffix = AFTER_MNT_FS[item['aft_fstype']]
419 if item['aft_fstype'] == "squashfs":
420 # fs.mksquashfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix)
421 args = "mksquashfs " + mountpoint + " " + self._imgdir+"/"+item['label']+fs_suffix
422 if item['squashfsopts']:
423 squashfsopts=item['squashfsopts'].replace(',', ' ')
424 runner.show("mksquashfs --help")
425 runner.show("%s %s" % (args, squashfsopts))
427 runner.show("%s " % args)
429 if item['aft_fstype'] == "vdfs":
430 ##FIXME temporary code - replace this with fs.mkvdfs()
432 vdfsopts=item['vdfsopts'].replace(',', ' ')
434 vdfsopts="-i -z 1024M"
436 fullpathmkvdfs = "mkfs.vdfs" #find_binary_path("mkfs.vdfs")
437 runner.show("%s --help" % fullpathmkvdfs)
438 # fs.mkvdfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix, vdfsopts)
439 runner.show('%s %s -r %s %s' % (fullpathmkvdfs, vdfsopts, mountpoint, self._imgdir+"/"+item['label']+fs_suffix))
441 runner.show('umount %s' % mountpoint)
442 # os.unlink(mountpoint)
443 runner.show('mv %s %s' % (self._imgdir+"/"+item['label']+fs_suffix, self._imgdir+"/"+item['label']+".img") )
444 runner.show('ls -al %s' % self._imgdir)
446 if item['fstype'] == "ext4":
447 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
449 self.image_files.setdefault('partitions', {}).update(
450 {item['mountpoint']: item['label']})
451 if self.compress_image:
452 compressing(imgfile, self.compress_image)
453 self.image_files.setdefault('image_files', []).append(
454 '.'.join([item['name'], self.compress_image]))
456 self.image_files.setdefault('image_files', []).append(item['name'])
459 for item in os.listdir(self._imgdir):
460 shutil.move(os.path.join(self._imgdir, item),
461 os.path.join(self._outdir, item))
463 msger.info("Pack all loop images together to %s" % self.pack_to)
464 dstfile = os.path.join(self._outdir, self.pack_to)
465 packing(dstfile, self._imgdir)
466 self.image_files['image_files'] = [self.pack_to]
470 mountfp_xml = os.path.splitext(self.pack_to)[0]
471 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
473 mountfp_xml = self.name + ".xml"
474 # save mount points mapping file to xml
475 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
479 def copy_attachment(self):
480 if not hasattr(self, '_attachment') or not self._attachment:
485 msger.info("Copying attachment files...")
486 for item in self._attachment:
487 if not os.path.exists(item):
489 dpath = os.path.join(self._imgdir, os.path.basename(item))
490 msger.verbose("Copy attachment %s to %s" % (item, dpath))
491 shutil.copy(item, dpath)
493 def create_manifest(self):
494 if self.compress_image:
495 self.image_files.update({'compress': self.compress_image})
496 super(LoopImageCreator, self).create_manifest()