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
28 # The maximum string length supported for LoopImageCreator.fslabel
32 def save_mountpoints(fpath, loops, arch = None):
33 """Save mount points mapping to file
35 :fpath, the xml file to store partition info
36 :loops, dict of partition info
40 if not fpath or not loops:
43 from xml.dom import minidom
44 doc = minidom.Document()
45 imgroot = doc.createElement("image")
46 doc.appendChild(imgroot)
48 imgroot.setAttribute('arch', arch)
50 part = doc.createElement("partition")
51 imgroot.appendChild(part)
52 for (key, val) in loop.items():
53 if isinstance(val, fs.Mount):
55 part.setAttribute(key, str(val))
57 with open(fpath, 'w') as wf:
58 wf.write(doc.toprettyxml(indent=' '))
62 def load_mountpoints(fpath):
63 """Load mount points mapping from file
65 :fpath, file path to load
71 from xml.dom import minidom
73 with open(fpath, 'r') as rf:
74 dom = minidom.parse(rf)
75 imgroot = dom.documentElement
76 for part in imgroot.getElementsByTagName("partition"):
77 p = dict(part.attributes.items())
80 mp = (p['mountpoint'], p['label'], p['name'],
81 int(p['size']), p['fstype'])
83 msger.warning("Wrong format line in file: %s" % fpath)
85 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
91 class LoopImageCreator(BaseImageCreator):
92 """Installs a system into a loopback-mountable filesystem image.
94 LoopImageCreator is a straightforward ImageCreator subclass; the system
95 is installed into an ext3 filesystem on a sparse file which can be
96 subsequently loopback-mounted.
98 When specifying multiple partitions in kickstart file, each partition
99 will be created as a separated loop image.
103 def __init__(self, creatoropts=None, pkgmgr=None,
106 """Initialize a LoopImageCreator instance.
108 This method takes the same arguments as ImageCreator.__init__()
109 with the addition of:
111 fslabel -- A string used as a label for any filesystems created.
114 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
116 self.compress_image = compress_image
117 self.shrink_image = shrink_image
119 self.__fslabel = None
120 self.fslabel = self.name
122 self.__blocksize = 4096
124 self.__fstype = kickstart.get_image_fstype(self.ks,
126 self.__fsopts = kickstart.get_image_fsopts(self.ks,
130 for part in sorted(kickstart.get_partitions(self.ks),
131 key=lambda p: p.mountpoint):
132 if part.fstype == "swap":
144 msger.warning('no "label" specified for loop img at %s'
145 ', use the mountpoint as the name' % mp)
146 label = mp.split('/')[-1]
148 imgname = misc.strip_end(label, '.img') + '.img'
153 'size': part.size or 4096L * 1024 * 1024,
154 'fstype': part.fstype or 'ext3',
155 'extopts': part.extopts or None,
156 'loop': None, # to be created in _mount_instroot
157 'uuid': part.uuid or None,
160 self._instloops = allloops
170 self.__image_size = kickstart.get_image_size(self.ks,
173 self.__image_size = 0
175 self._img_name = self.name + ".img"
177 def get_image_names(self):
178 if not self._instloops:
181 return [lo['name'] for lo in self._instloops]
183 def _set_fstype(self, fstype):
184 self.__fstype = fstype
186 def _set_image_size(self, imgsize):
187 self.__image_size = imgsize
193 def __get_fslabel(self):
194 if self.__fslabel is None:
197 return self.__fslabel
198 def __set_fslabel(self, val):
200 self.__fslabel = None
202 self.__fslabel = val[:FSLABEL_MAXLEN]
203 #A string used to label any filesystems created.
205 #Some filesystems impose a constraint on the maximum allowed size of the
206 #filesystem label. In the case of ext3 it's 16 characters, but in the case
207 #of ISO9660 it's 32 characters.
209 #mke2fs silently truncates the label, but mkisofs aborts if the label is
210 #too long. So, for convenience sake, any string assigned to this attribute
211 #is silently truncated to FSLABEL_MAXLEN (32) characters.
212 fslabel = property(__get_fslabel, __set_fslabel)
214 def __get_image(self):
215 if self.__imgdir is None:
216 raise CreatorError("_image is not valid before calling mount()")
217 return os.path.join(self.__imgdir, self._img_name)
218 #The location of the image file.
220 #This is the path to the filesystem image. Subclasses may use this path
221 #in order to package the image in _stage_final_image().
223 #Note, this directory does not exist before ImageCreator.mount() is called.
225 #Note also, this is a read-only attribute.
226 _image = property(__get_image)
228 def __get_blocksize(self):
229 return self.__blocksize
230 def __set_blocksize(self, val):
232 raise CreatorError("_blocksize must be set before calling mount()")
234 self.__blocksize = int(val)
236 raise CreatorError("'%s' is not a valid integer value "
237 "for _blocksize" % val)
238 #The block size used by the image's filesystem.
240 #This is the block size used when creating the filesystem image. Subclasses
241 #may change this if they wish to use something other than a 4k block size.
243 #Note, this attribute may only be set before calling mount().
244 _blocksize = property(__get_blocksize, __set_blocksize)
246 def __get_fstype(self):
248 def __set_fstype(self, val):
249 if val != "ext2" and val != "ext3":
250 raise CreatorError("Unknown _fstype '%s' supplied" % val)
252 #The type of filesystem used for the image.
254 #This is the filesystem type used when creating the filesystem image.
255 #Subclasses may change this if they wish to use something other ext3.
257 #Note, only ext2 and ext3 are currently supported.
259 #Note also, this attribute may only be set before calling mount().
260 _fstype = property(__get_fstype, __set_fstype)
262 def __get_fsopts(self):
264 def __set_fsopts(self, val):
266 #Mount options of filesystem used for the image.
268 #This can be specified by --fsoptions="xxx,yyy" in part command in
270 _fsopts = property(__get_fsopts, __set_fsopts)
274 # Helpers for subclasses
276 def _resparse(self, size=None):
277 """Rebuild the filesystem image to be as sparse as possible.
279 This method should be used by subclasses when staging the final image
280 in order to reduce the actual space taken up by the sparse image file
281 to be as little as possible.
283 This is done by resizing the filesystem to the minimal size (thereby
284 eliminating any space taken up by deleted files) and then resizing it
285 back to the supplied size.
287 size -- the size in, in bytes, which the filesystem image should be
288 resized to after it has been minimized; this defaults to None,
289 causing the original size specified by the kickstart file to
290 be used (or 4GiB if not specified in the kickstart).
293 for item in self._instloops:
294 if item['name'] == self._img_name:
295 minsize = item['loop'].resparse(size)
297 item['loop'].resparse(size)
301 def _base_on(self, base_on=None):
302 if base_on and self._image != base_on:
303 shutil.copyfile(base_on, self._image)
305 def _check_imgdir(self):
306 if self.__imgdir is None:
307 self.__imgdir = self._mkdtemp()
311 # Actual implementation
313 def _mount_instroot(self, base_on=None):
315 if base_on and os.path.isfile(base_on):
316 self.__imgdir = os.path.dirname(base_on)
317 imgname = os.path.basename(base_on)
318 self._base_on(base_on)
319 self._set_image_size(misc.get_file_size(self._image))
321 # here, self._instloops must be []
322 self._instloops.append({
326 "size": self.__image_size or 4096L,
327 "fstype": self.__fstype or "ext3",
336 for loop in self._instloops:
337 fstype = loop['fstype']
338 mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
339 size = loop['size'] * 1024L * 1024L
340 imgname = loop['name']
342 if fstype in ("ext2", "ext3", "ext4"):
343 MyDiskMount = fs.ExtDiskMount
344 elif fstype == "btrfs":
345 MyDiskMount = fs.BtrfsDiskMount
346 elif fstype in ("vfat", "msdos"):
347 MyDiskMount = fs.VfatDiskMount
349 raise MountError('Cannot support fstype: %s' % fstype)
351 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
352 os.path.join(self.__imgdir, imgname),
358 fsuuid = loop['uuid'])
360 if fstype in ("ext2", "ext3", "ext4"):
361 loop['loop'].extopts = loop['extopts']
364 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
367 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
368 if loop['kspart'] and loop['kspart'].uuid is None and \
370 loop['kspart'].uuid = loop['loop'].uuid
372 except MountError, e:
375 def _unmount_instroot(self):
376 for item in reversed(self._instloops):
378 item['loop'].cleanup()
382 def _stage_final_image(self):
384 if self.pack_to or self.shrink_image:
389 for item in self._instloops:
390 imgfile = os.path.join(self.__imgdir, item['name'])
391 if item['fstype'] == "ext4":
392 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
394 self.image_files.setdefault('partitions', {}).update(
395 {item['mountpoint']: item['label']})
396 if self.compress_image:
397 misc.compressing(imgfile, self.compress_image)
398 self.image_files.setdefault('image_files', []).append(
399 '.'.join([item['name'], self.compress_image]))
401 self.image_files.setdefault('image_files', []).append(item['name'])
404 for item in os.listdir(self.__imgdir):
405 shutil.move(os.path.join(self.__imgdir, item),
406 os.path.join(self._outdir, item))
408 msger.info("Pack all loop images together to %s" % self.pack_to)
409 dstfile = os.path.join(self._outdir, self.pack_to)
410 misc.packing(dstfile, self.__imgdir)
411 self.image_files['image_files'] = [self.pack_to]
415 mountfp_xml = os.path.splitext(self.pack_to)[0]
416 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
418 mountfp_xml = self.name + ".xml"
419 # save mount points mapping file to xml
420 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
424 def copy_attachment(self):
425 if not hasattr(self, '_attachment') or not self._attachment:
430 msger.info("Copying attachment files...")
431 for item in self._attachment:
432 if not os.path.exists(item):
434 dpath = os.path.join(self.__imgdir, os.path.basename(item))
435 msger.verbose("Copy attachment %s to %s" % (item, dpath))
436 shutil.copy(item, dpath)
438 def create_manifest(self):
439 if self.compress_image:
440 self.image_files.update({'compress': self.compress_image})
441 super(LoopImageCreator, self).create_manifest()