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