0943ec746dc75323cf8e0d8fa4901497a047b7c4
[tools/mic.git] / mic / imager / loop.py
1 #!/usr/bin/python -tt
2 #
3 # Copyright (c) 2011 Intel, Inc.
4 #
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
8 #
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
12 # for more details.
13 #
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.
17
18 import os
19 import glob
20 import shutil
21
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
27
28
29 # The maximum string length supported for LoopImageCreator.fslabel
30 FSLABEL_MAXLEN = 32
31
32
33 def save_mountpoints(fpath, loops, arch = None):
34     """Save mount points mapping to file
35
36     :fpath, the xml file to store partition info
37     :loops, dict of partition info
38     :arch, image arch
39     """
40
41     if not fpath or not loops:
42         return
43
44     from xml.dom import minidom
45     doc = minidom.Document()
46     imgroot = doc.createElement("image")
47     doc.appendChild(imgroot)
48     if arch:
49         imgroot.setAttribute('arch', arch)
50     for loop in loops:
51         part = doc.createElement("partition")
52         imgroot.appendChild(part)
53         for (key, val) in loop.items():
54             if isinstance(val, fs.Mount):
55                 continue
56             part.setAttribute(key, str(val))
57
58     with open(fpath, 'w') as wf:
59         wf.write(doc.toprettyxml(indent='  '))
60
61     return
62
63 def load_mountpoints(fpath):
64     """Load mount points mapping from file
65
66     :fpath, file path to load
67     """
68
69     if not fpath:
70         return
71
72     from xml.dom import minidom
73     mount_maps = []
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())
79
80         try:
81             mp = (p['mountpoint'], p['label'], p['name'],
82                   int(p['size']), p['fstype'])
83         except KeyError:
84             msger.warning("Wrong format line in file: %s" % fpath)
85         except ValueError:
86             msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
87         else:
88             mount_maps.append(mp)
89
90     return mount_maps
91
92 class LoopImageCreator(BaseImageCreator):
93     """Installs a system into a loopback-mountable filesystem image.
94
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.
98
99     When specifying multiple partitions in kickstart file, each partition
100     will be created as a separated loop image.
101     """
102     img_format = 'loop'
103
104     def __init__(self, creatoropts=None, pkgmgr=None,
105                  compress_image=None,
106                  shrink_image=False):
107         """Initialize a LoopImageCreator instance.
108
109         This method takes the same arguments as ImageCreator.__init__()
110         with the addition of:
111
112         fslabel -- A string used as a label for any filesystems created.
113         """
114
115         BaseImageCreator.__init__(self, creatoropts, pkgmgr)
116
117         self.compress_image = compress_image
118         self.shrink_image = shrink_image
119
120         self.__fslabel = None
121         self.fslabel = self.name
122
123         self.__blocksize = 4096
124         if self.ks:
125             self.__fstype = kickstart.get_image_fstype(self.ks,
126                                                        "ext3")
127             self.__fsopts = kickstart.get_image_fsopts(self.ks,
128                                                        "defaults,noatime")
129
130             allloops = []
131             for part in sorted(kickstart.get_partitions(self.ks),
132                                key=lambda p: p.mountpoint):
133                 if part.fstype == "swap":
134                     continue
135
136                 label = part.label
137                 mp = part.mountpoint
138                 if mp == '/':
139                     # the base image
140                     if not label:
141                         label = self.name
142                 else:
143                     mp = mp.rstrip('/')
144                     if not label:
145                         msger.warning('no "label" specified for loop img at %s'
146                                       ', use the mountpoint as the name' % mp)
147                         label = mp.split('/')[-1]
148
149                 imgname = misc.strip_end(label, '.img') + '.img'
150                 allloops.append({
151                     'mountpoint': mp,
152                     'label': label,
153                     'name': imgname,
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,
159                     'kspart' : part,
160                     'exclude_image' : part.exclude_image or None,
161                     })
162             self._instloops = allloops
163
164         else:
165             self.__fstype = None
166             self.__fsopts = None
167             self._instloops = []
168
169         self._imgdir = None
170
171         if self.ks:
172             self.__image_size = kickstart.get_image_size(self.ks,
173                                                          4096L * 1024 * 1024)
174         else:
175             self.__image_size = 0
176
177         self._img_name = self.name + ".img"
178
179     def get_image_names(self):
180         if not self._instloops:
181             return None
182
183         return [lo['name'] for lo in self._instloops]
184
185     def _set_fstype(self, fstype):
186         self.__fstype = fstype
187
188     def _set_image_size(self, imgsize):
189         self.__image_size = imgsize
190
191
192     #
193     # Properties
194     #
195     def __get_fslabel(self):
196         if self.__fslabel is None:
197             return self.name
198         else:
199             return self.__fslabel
200     def __set_fslabel(self, val):
201         if val is None:
202             self.__fslabel = None
203         else:
204             self.__fslabel = val[:FSLABEL_MAXLEN]
205     #A string used to label any filesystems created.
206     #
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.
210     #
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)
215
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.
221     #
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().
224     #
225     #Note, this directory does not exist before ImageCreator.mount() is called.
226     #
227     #Note also, this is a read-only attribute.
228     _image = property(__get_image)
229
230     def __get_blocksize(self):
231         return self.__blocksize
232     def __set_blocksize(self, val):
233         if self._instloops:
234             raise CreatorError("_blocksize must be set before calling mount()")
235         try:
236             self.__blocksize = int(val)
237         except ValueError:
238             raise CreatorError("'%s' is not a valid integer value "
239                                "for _blocksize" % val)
240     #The block size used by the image's filesystem.
241     #
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.
244     #
245     #Note, this attribute may only be set before calling mount().
246     _blocksize = property(__get_blocksize, __set_blocksize)
247
248     def __get_fstype(self):
249         return self.__fstype
250     def __set_fstype(self, val):
251         if val != "ext2" and val != "ext3":
252             raise CreatorError("Unknown _fstype '%s' supplied" % val)
253         self.__fstype = val
254     #The type of filesystem used for the image.
255     #
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.
258     #
259     #Note, only ext2 and ext3 are currently supported.
260     #
261     #Note also, this attribute may only be set before calling mount().
262     _fstype = property(__get_fstype, __set_fstype)
263
264     def __get_fsopts(self):
265         return self.__fsopts
266     def __set_fsopts(self, val):
267         self.__fsopts = val
268     #Mount options of filesystem used for the image.
269     #
270     #This can be specified by --fsoptions="xxx,yyy" in part command in
271     #kickstart file.
272     _fsopts = property(__get_fsopts, __set_fsopts)
273
274
275     #
276     # Helpers for subclasses
277     #
278     def _resparse(self, size=None):
279         """Rebuild the filesystem image to be as sparse as possible.
280
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.
284
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.
288
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).
293         """
294         minsize = 0
295         for item in self._instloops:
296             if item['name'] == self._img_name:
297                 minsize = item['loop'].resparse(size)
298             else:
299                 item['loop'].resparse(size)
300
301         return minsize
302
303     def _base_on(self, base_on=None):
304         if base_on and self._image != base_on:
305             shutil.copyfile(base_on, self._image)
306
307     def _check_imgdir(self):
308         if self._imgdir is None:
309             self._imgdir = self._mkdtemp()
310
311
312     #
313     # Actual implementation
314     #
315     def _mount_instroot(self, base_on=None):
316
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))
322
323             # here, self._instloops must be []
324             self._instloops.append({
325                  "mountpoint": "/",
326                  "label": self.name,
327                  "name": imgname,
328                  "size": self.__image_size or 4096L,
329                  "fstype": self.__fstype or "ext3",
330                  "extopts": None,
331                  "loop": None,
332                  "uuid": None,
333                  "kspart": None,
334                  "exclude_image" : None
335                  })
336
337         self._check_imgdir()
338
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']
344
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
351             else:
352                 raise MountError('Cannot support fstype: %s' % fstype)
353
354             loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
355                                            os.path.join(self._imgdir, imgname),
356                                            size),
357                                        mp,
358                                        fstype,
359                                        self._blocksize,
360                                        loop['label'],
361                                        fsuuid = loop['uuid'])
362
363             if fstype in ("ext2", "ext3", "ext4"):
364                 loop['loop'].extopts = loop['extopts']
365
366             try:
367                 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
368                 fs.makedirs(mp)
369                 loop['loop'].mount()
370                 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
371                 if loop['kspart'] and loop['kspart'].uuid is None and \
372                    loop['loop'].uuid:
373                     loop['kspart'].uuid = loop['loop'].uuid
374
375             except MountError, e:
376                 raise
377
378     def _unmount_instroot(self):
379         for item in reversed(self._instloops):
380             try:
381                 item['loop'].cleanup()
382             except:
383                 pass
384
385     def _stage_final_image(self):
386
387         if self.pack_to or self.shrink_image:
388             self._resparse(0)
389         else:
390             self._resparse()
391
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 '
396                             % imgfile)
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]))
403             else:
404                 self.image_files.setdefault('image_files', []).append(item['name'])
405
406         if not self.pack_to:
407             for item in os.listdir(self._imgdir):
408                 shutil.move(os.path.join(self._imgdir, item),
409                             os.path.join(self._outdir, item))
410         else:
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]
415
416
417         if self.pack_to:
418             mountfp_xml = os.path.splitext(self.pack_to)[0]
419             mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
420         else:
421             mountfp_xml = self.name + ".xml"
422         # save mount points mapping file to xml
423         save_mountpoints(os.path.join(self._outdir, mountfp_xml),
424                          self._instloops,
425                          self.target_arch)
426
427     def copy_attachment(self):
428         if not hasattr(self, '_attachment') or not self._attachment:
429             return
430
431         self._check_imgdir()
432
433         msger.info("Copying attachment files...")
434         for item in self._attachment:
435             if not os.path.exists(item):
436                 continue
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)
440
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()