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
33 def save_mountpoints(fpath, loops, arch = None):
34 """Save mount points mapping to file
36 :fpath, the xml file to store partition info
37 :loops, dict of partition info
41 if not fpath or not loops:
44 from xml.dom import minidom
45 doc = minidom.Document()
46 imgroot = doc.createElement("image")
47 doc.appendChild(imgroot)
49 imgroot.setAttribute('arch', arch)
51 part = doc.createElement("partition")
52 imgroot.appendChild(part)
53 for (key, val) in loop.items():
54 if isinstance(val, fs.Mount):
56 part.setAttribute(key, str(val))
58 with open(fpath, 'w') as wf:
59 wf.write(doc.toprettyxml(indent=' '))
63 def load_mountpoints(fpath):
64 """Load mount points mapping from file
66 :fpath, file path to load
72 from xml.dom import minidom
74 with open(fpath, 'r') as rf:
75 dom = minidom.parse(rf)
76 imgroot = dom.documentElement
77 for part in imgroot.getElementsByTagName("partition"):
78 p = dict(part.attributes.items())
81 mp = (p['mountpoint'], p['label'], p['name'],
82 int(p['size']), p['fstype'])
84 msger.warning("Wrong format line in file: %s" % fpath)
86 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
92 class LoopImageCreator(BaseImageCreator):
93 """Installs a system into a loopback-mountable filesystem image.
95 LoopImageCreator is a straightforward ImageCreator subclass; the system
96 is installed into an ext3 filesystem on a sparse file which can be
97 subsequently loopback-mounted.
99 When specifying multiple partitions in kickstart file, each partition
100 will be created as a separated loop image.
104 def __init__(self, creatoropts=None, pkgmgr=None,
107 """Initialize a LoopImageCreator instance.
109 This method takes the same arguments as ImageCreator.__init__()
110 with the addition of:
112 fslabel -- A string used as a label for any filesystems created.
115 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
117 self.compress_image = compress_image
118 self.shrink_image = shrink_image
120 self.__fslabel = None
121 self.fslabel = self.name
123 self.__blocksize = 4096
125 self.__fstype = kickstart.get_image_fstype(self.ks,
127 self.__fsopts = kickstart.get_image_fsopts(self.ks,
131 for part in sorted(kickstart.get_partitions(self.ks),
132 key=lambda p: p.mountpoint):
133 if part.fstype == "swap":
145 msger.warning('no "label" specified for loop img at %s'
146 ', use the mountpoint as the name' % mp)
147 label = mp.split('/')[-1]
149 imgname = misc.strip_end(label, '.img') + '.img'
154 'size': part.size or 4096L * 1024 * 1024,
155 'fstype': part.fstype or 'ext3',
156 'extopts': part.extopts or None,
157 'loop': None, # to be created in _mount_instroot
158 'uuid': part.uuid or None,
160 'exclude_image' : part.exclude_image or None,
162 self._instloops = allloops
172 self.__image_size = kickstart.get_image_size(self.ks,
175 self.__image_size = 0
177 self._img_name = self.name + ".img"
179 def get_image_names(self):
180 if not self._instloops:
183 return [lo['name'] for lo in self._instloops]
185 def _set_fstype(self, fstype):
186 self.__fstype = fstype
188 def _set_image_size(self, imgsize):
189 self.__image_size = imgsize
195 def __get_fslabel(self):
196 if self.__fslabel is None:
199 return self.__fslabel
200 def __set_fslabel(self, val):
202 self.__fslabel = None
204 self.__fslabel = val[:FSLABEL_MAXLEN]
205 #A string used to label any filesystems created.
207 #Some filesystems impose a constraint on the maximum allowed size of the
208 #filesystem label. In the case of ext3 it's 16 characters, but in the case
209 #of ISO9660 it's 32 characters.
211 #mke2fs silently truncates the label, but mkisofs aborts if the label is
212 #too long. So, for convenience sake, any string assigned to this attribute
213 #is silently truncated to FSLABEL_MAXLEN (32) characters.
214 fslabel = property(__get_fslabel, __set_fslabel)
216 def __get_image(self):
217 if self._imgdir is None:
218 raise CreatorError("_image is not valid before calling mount()")
219 return os.path.join(self._imgdir, self._img_name)
220 #The location of the image file.
222 #This is the path to the filesystem image. Subclasses may use this path
223 #in order to package the image in _stage_final_image().
225 #Note, this directory does not exist before ImageCreator.mount() is called.
227 #Note also, this is a read-only attribute.
228 _image = property(__get_image)
230 def __get_blocksize(self):
231 return self.__blocksize
232 def __set_blocksize(self, val):
234 raise CreatorError("_blocksize must be set before calling mount()")
236 self.__blocksize = int(val)
238 raise CreatorError("'%s' is not a valid integer value "
239 "for _blocksize" % val)
240 #The block size used by the image's filesystem.
242 #This is the block size used when creating the filesystem image. Subclasses
243 #may change this if they wish to use something other than a 4k block size.
245 #Note, this attribute may only be set before calling mount().
246 _blocksize = property(__get_blocksize, __set_blocksize)
248 def __get_fstype(self):
250 def __set_fstype(self, val):
251 if val != "ext2" and val != "ext3":
252 raise CreatorError("Unknown _fstype '%s' supplied" % val)
254 #The type of filesystem used for the image.
256 #This is the filesystem type used when creating the filesystem image.
257 #Subclasses may change this if they wish to use something other ext3.
259 #Note, only ext2 and ext3 are currently supported.
261 #Note also, this attribute may only be set before calling mount().
262 _fstype = property(__get_fstype, __set_fstype)
264 def __get_fsopts(self):
266 def __set_fsopts(self, val):
268 #Mount options of filesystem used for the image.
270 #This can be specified by --fsoptions="xxx,yyy" in part command in
272 _fsopts = property(__get_fsopts, __set_fsopts)
276 # Helpers for subclasses
278 def _resparse(self, size=None):
279 """Rebuild the filesystem image to be as sparse as possible.
281 This method should be used by subclasses when staging the final image
282 in order to reduce the actual space taken up by the sparse image file
283 to be as little as possible.
285 This is done by resizing the filesystem to the minimal size (thereby
286 eliminating any space taken up by deleted files) and then resizing it
287 back to the supplied size.
289 size -- the size in, in bytes, which the filesystem image should be
290 resized to after it has been minimized; this defaults to None,
291 causing the original size specified by the kickstart file to
292 be used (or 4GiB if not specified in the kickstart).
295 for item in self._instloops:
296 if item['name'] == self._img_name:
297 minsize = item['loop'].resparse(size)
299 item['loop'].resparse(size)
303 def _base_on(self, base_on=None):
304 if base_on and self._image != base_on:
305 shutil.copyfile(base_on, self._image)
307 def _check_imgdir(self):
308 if self._imgdir is None:
309 self._imgdir = self._mkdtemp()
313 # Actual implementation
315 def _mount_instroot(self, base_on=None):
317 if base_on and os.path.isfile(base_on):
318 self._imgdir = os.path.dirname(base_on)
319 imgname = os.path.basename(base_on)
320 self._base_on(base_on)
321 self._set_image_size(misc.get_file_size(self._image))
323 # here, self._instloops must be []
324 self._instloops.append({
328 "size": self.__image_size or 4096L,
329 "fstype": self.__fstype or "ext3",
334 "exclude_image" : None
339 for loop in self._instloops:
340 fstype = loop['fstype']
341 mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
342 size = loop['size'] * 1024L * 1024L
343 imgname = loop['name']
345 if fstype in ("ext2", "ext3", "ext4"):
346 MyDiskMount = fs.ExtDiskMount
347 elif fstype == "btrfs":
348 MyDiskMount = fs.BtrfsDiskMount
349 elif fstype in ("vfat", "msdos"):
350 MyDiskMount = fs.VfatDiskMount
352 raise MountError('Cannot support fstype: %s' % fstype)
354 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
355 os.path.join(self._imgdir, imgname),
361 fsuuid = loop['uuid'])
363 if fstype in ("ext2", "ext3", "ext4"):
364 loop['loop'].extopts = loop['extopts']
367 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
370 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
371 if loop['kspart'] and loop['kspart'].uuid is None and \
373 loop['kspart'].uuid = loop['loop'].uuid
375 except MountError, e:
378 def _unmount_instroot(self):
379 for item in reversed(self._instloops):
381 item['loop'].cleanup()
385 def _stage_final_image(self):
387 if self.pack_to or self.shrink_image:
392 for item in self._instloops:
393 imgfile = os.path.join(self._imgdir, item['name'])
394 if item['fstype'] == "ext4":
395 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
397 self.image_files.setdefault('partitions', {}).update(
398 {item['mountpoint']: item['label']})
399 if self.compress_image:
400 compressing(imgfile, self.compress_image)
401 self.image_files.setdefault('image_files', []).append(
402 '.'.join([item['name'], self.compress_image]))
404 self.image_files.setdefault('image_files', []).append(item['name'])
407 for item in os.listdir(self._imgdir):
408 shutil.move(os.path.join(self._imgdir, item),
409 os.path.join(self._outdir, item))
411 msger.info("Pack all loop images together to %s" % self.pack_to)
412 dstfile = os.path.join(self._outdir, self.pack_to)
413 packing(dstfile, self._imgdir)
414 self.image_files['image_files'] = [self.pack_to]
418 mountfp_xml = os.path.splitext(self.pack_to)[0]
419 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
421 mountfp_xml = self.name + ".xml"
422 # save mount points mapping file to xml
423 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
427 def copy_attachment(self):
428 if not hasattr(self, '_attachment') or not self._attachment:
433 msger.info("Copying attachment files...")
434 for item in self._attachment:
435 if not os.path.exists(item):
437 dpath = os.path.join(self._imgdir, os.path.basename(item))
438 msger.verbose("Copy attachment %s to %s" % (item, dpath))
439 shutil.copy(item, dpath)
441 def create_manifest(self):
442 if self.compress_image:
443 self.image_files.update({'compress': self.compress_image})
444 super(LoopImageCreator, self).create_manifest()