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
26 from baseimager import BaseImageCreator
28 # The maximum string length supported for LoopImageCreator.fslabel
31 def save_mountpoints(fpath, loops, arch = None):
32 """Save mount points mapping to file
34 :fpath, the xml file to store partition info
35 :loops, dict of partition info
39 if not fpath or not loops:
42 from xml.dom import minidom
43 doc = minidom.Document()
44 imgroot = doc.createElement("image")
45 doc.appendChild(imgroot)
47 imgroot.setAttribute('arch', arch)
49 part = doc.createElement("partition")
50 imgroot.appendChild(part)
51 for (key, val) in loop.items():
52 if isinstance(val, fs.Mount):
54 part.setAttribute(key, str(val))
56 with open(fpath, 'w') as wf:
57 wf.write(doc.toprettyxml(indent=' '))
61 def load_mountpoints(fpath):
62 """Load mount points mapping from file
64 :fpath, file path to load
70 from xml.dom import minidom
72 with open(fpath, 'r') as rf:
73 dom = minidom.parse(rf)
74 imgroot = dom.documentElement
75 for part in imgroot.getElementsByTagName("partition"):
76 p = dict(part.attributes.items())
79 mp = (p['mountpoint'], p['label'], p['name'],
80 int(p['size']), p['fstype'])
82 msger.warning("Wrong format line in file: %s" % fpath)
84 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
90 class LoopImageCreator(BaseImageCreator):
91 """Installs a system into a loopback-mountable filesystem image.
93 LoopImageCreator is a straightforward ImageCreator subclass; the system
94 is installed into an ext3 filesystem on a sparse file which can be
95 subsequently loopback-mounted.
97 When specifying multiple partitions in kickstart file, each partition
98 will be created as a separated loop image.
101 def __init__(self, creatoropts=None, pkgmgr=None, compress_image=None):
102 """Initialize a LoopImageCreator instance.
104 This method takes the same arguments as ImageCreator.__init__()
105 with the addition of:
107 fslabel -- A string used as a label for any filesystems created.
110 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
112 self.compress_image = compress_image
114 self.__fslabel = None
115 self.fslabel = self.name
117 self.__blocksize = 4096
119 self.__fstype = kickstart.get_image_fstype(self.ks,
121 self.__fsopts = kickstart.get_image_fsopts(self.ks,
125 for part in sorted(kickstart.get_partitions(self.ks),
126 key=lambda p: p.mountpoint):
127 if part.fstype == "swap":
139 msger.warning('no "label" specified for loop img at %s'
140 ', use the mountpoint as the name' % mp)
141 label = mp.split('/')[-1]
143 imgname = misc.strip_end(label, '.img') + '.img'
148 'size': part.size or 4096L * 1024 * 1024,
149 'fstype': part.fstype or 'ext3',
150 'loop': None, # to be created in _mount_instroot
152 self._instloops = allloops
162 self.__image_size = kickstart.get_image_size(self.ks,
165 self.__image_size = 0
167 self._img_name = self.name + ".img"
169 def _set_fstype(self, fstype):
170 self.__fstype = fstype
172 def _set_image_size(self, imgsize):
173 self.__image_size = imgsize
179 def __get_fslabel(self):
180 if self.__fslabel is None:
183 return self.__fslabel
184 def __set_fslabel(self, val):
186 self.__fslabel = None
188 self.__fslabel = val[:FSLABEL_MAXLEN]
189 #A string used to label any filesystems created.
191 #Some filesystems impose a constraint on the maximum allowed size of the
192 #filesystem label. In the case of ext3 it's 16 characters, but in the case
193 #of ISO9660 it's 32 characters.
195 #mke2fs silently truncates the label, but mkisofs aborts if the label is
196 #too long. So, for convenience sake, any string assigned to this attribute
197 #is silently truncated to FSLABEL_MAXLEN (32) characters.
198 fslabel = property(__get_fslabel, __set_fslabel)
200 def __get_image(self):
201 if self.__imgdir is None:
202 raise CreatorError("_image is not valid before calling mount()")
203 return os.path.join(self.__imgdir, self._img_name)
204 #The location of the image file.
206 #This is the path to the filesystem image. Subclasses may use this path
207 #in order to package the image in _stage_final_image().
209 #Note, this directory does not exist before ImageCreator.mount() is called.
211 #Note also, this is a read-only attribute.
212 _image = property(__get_image)
214 def __get_blocksize(self):
215 return self.__blocksize
216 def __set_blocksize(self, val):
218 raise CreatorError("_blocksize must be set before calling mount()")
220 self.__blocksize = int(val)
222 raise CreatorError("'%s' is not a valid integer value "
223 "for _blocksize" % val)
224 #The block size used by the image's filesystem.
226 #This is the block size used when creating the filesystem image. Subclasses
227 #may change this if they wish to use something other than a 4k block size.
229 #Note, this attribute may only be set before calling mount().
230 _blocksize = property(__get_blocksize, __set_blocksize)
232 def __get_fstype(self):
234 def __set_fstype(self, val):
235 if val != "ext2" and val != "ext3":
236 raise CreatorError("Unknown _fstype '%s' supplied" % val)
238 #The type of filesystem used for the image.
240 #This is the filesystem type used when creating the filesystem image.
241 #Subclasses may change this if they wish to use something other ext3.
243 #Note, only ext2 and ext3 are currently supported.
245 #Note also, this attribute may only be set before calling mount().
246 _fstype = property(__get_fstype, __set_fstype)
248 def __get_fsopts(self):
250 def __set_fsopts(self, val):
252 #Mount options of filesystem used for the image.
254 #This can be specified by --fsoptions="xxx,yyy" in part command in
256 _fsopts = property(__get_fsopts, __set_fsopts)
260 # Helpers for subclasses
262 def _resparse(self, size=None):
263 """Rebuild the filesystem image to be as sparse as possible.
265 This method should be used by subclasses when staging the final image
266 in order to reduce the actual space taken up by the sparse image file
267 to be as little as possible.
269 This is done by resizing the filesystem to the minimal size (thereby
270 eliminating any space taken up by deleted files) and then resizing it
271 back to the supplied size.
273 size -- the size in, in bytes, which the filesystem image should be
274 resized to after it has been minimized; this defaults to None,
275 causing the original size specified by the kickstart file to
276 be used (or 4GiB if not specified in the kickstart).
279 for item in self._instloops:
280 if item['name'] == self._img_name:
281 minsize = item['loop'].resparse(size)
283 item['loop'].resparse(size)
287 def _base_on(self, base_on=None):
288 if base_on and self._image != base_on:
289 shutil.copyfile(base_on, self._image)
291 def _check_imgdir(self):
292 if self.__imgdir is None:
293 self.__imgdir = self._mkdtemp()
297 # Actual implementation
299 def _mount_instroot(self, base_on=None):
301 if base_on and os.path.isfile(base_on):
302 self.__imgdir = os.path.dirname(base_on)
303 imgname = os.path.basename(base_on)
304 self._base_on(base_on)
305 self._set_image_size(misc.get_file_size(self._image))
307 # here, self._instloops must be []
308 self._instloops.append({
312 "size": self.__image_size or 4096L,
313 "fstype": self.__fstype or "ext3",
319 for loop in self._instloops:
320 fstype = loop['fstype']
321 mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
322 size = loop['size'] * 1024L * 1024L
323 imgname = loop['name']
325 if fstype in ("ext2", "ext3", "ext4"):
326 MyDiskMount = fs.ExtDiskMount
327 elif fstype == "btrfs":
328 MyDiskMount = fs.BtrfsDiskMount
329 elif fstype in ("vfat", "msdos"):
330 MyDiskMount = fs.VfatDiskMount
332 msger.error('Cannot support fstype: %s' % fstype)
334 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
335 os.path.join(self.__imgdir, imgname),
343 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
346 except MountError, e:
349 def _unmount_instroot(self):
350 for item in reversed(self._instloops):
351 item['loop'].cleanup()
353 def _stage_final_image(self):
361 for item in self._instloops:
362 imgfile = os.path.join(self.__imgdir, item['name'])
363 if item['fstype'] == "ext4":
364 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
366 if self.compress_image:
367 misc.compressing(imgfile, self.compress_image)
370 for item in os.listdir(self.__imgdir):
371 shutil.move(os.path.join(self.__imgdir, item),
372 os.path.join(self._outdir, item))
374 msger.info("Pack all loop images together to %s" % self.pack_to)
375 dstfile = os.path.join(self._outdir, self.pack_to)
376 misc.packing(dstfile, self.__imgdir)
379 mountfp_xml = os.path.splitext(self.pack_to)[0].rstrip('.tar') + ".xml"
381 mountfp_xml = self.name + ".xml"
382 # save mount points mapping file to xml
383 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
387 def copy_attachment(self):
388 if not hasattr(self, '_attachment') or not self._attachment:
393 msger.info("Copying attachment files...")
394 for item in self._attachment:
395 if not os.path.exists(item):
397 dpath = os.path.join(self.__imgdir, os.path.basename(item))
398 msger.verbose("Copy attachment %s to %s" % (item, dpath))
399 shutil.copy(item, dpath)