Another method of install tpk.
[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 # Support for Read-only file system list
32 AFTER_MNT_FS = {"squashfs":".sqsh", "vdfs":".vdfs"}
33
34 def save_mountpoints(fpath, loops, arch = None):
35     """Save mount points mapping to file
36
37     :fpath, the xml file to store partition info
38     :loops, dict of partition info
39     :arch, image arch
40     """
41
42     if not fpath or not loops:
43         return
44
45     from xml.dom import minidom
46     doc = minidom.Document()
47     imgroot = doc.createElement("image")
48     doc.appendChild(imgroot)
49     if arch:
50         imgroot.setAttribute('arch', arch)
51     for loop in loops:
52         part = doc.createElement("partition")
53         imgroot.appendChild(part)
54         for (key, val) in loop.items():
55             if isinstance(val, fs.Mount):
56                 continue
57             part.setAttribute(key, str(val))
58
59     with open(fpath, 'w') as wf:
60         wf.write(doc.toprettyxml(indent='  '))
61
62     return
63
64 def load_mountpoints(fpath):
65     """Load mount points mapping from file
66
67     :fpath, file path to load
68     """
69
70     if not fpath:
71         return
72
73     from xml.dom import minidom
74     mount_maps = []
75     with open(fpath, 'r') as rf:
76         dom = minidom.parse(rf)
77     imgroot = dom.documentElement
78     for part in imgroot.getElementsByTagName("partition"):
79         p  = dict(part.attributes.items())
80
81         try:
82             mp = (p['mountpoint'], p['label'], p['name'],
83                   int(p['size']), p['fstype'])
84         except KeyError:
85             msger.warning("Wrong format line in file: %s" % fpath)
86         except ValueError:
87             msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
88         else:
89             mount_maps.append(mp)
90
91     return mount_maps
92
93 class LoopImageCreator(BaseImageCreator):
94     """Installs a system into a loopback-mountable filesystem image.
95
96     LoopImageCreator is a straightforward ImageCreator subclass; the system
97     is installed into an ext3 filesystem on a sparse file which can be
98     subsequently loopback-mounted.
99
100     When specifying multiple partitions in kickstart file, each partition
101     will be created as a separated loop image.
102     """
103     img_format = 'loop'
104
105     def __init__(self, creatoropts=None, pkgmgr=None,
106                  compress_image=None,
107                  shrink_image=False):
108         """Initialize a LoopImageCreator instance.
109
110         This method takes the same arguments as ImageCreator.__init__()
111         with the addition of:
112
113         fslabel -- A string used as a label for any filesystems created.
114         """
115
116         BaseImageCreator.__init__(self, creatoropts, pkgmgr)
117
118         self.compress_image = compress_image
119         self.shrink_image = shrink_image
120
121         self.__fslabel = None
122         self.fslabel = self.name
123
124         self.__blocksize = 4096
125         if self.ks:
126             self.__fstype = kickstart.get_image_fstype(self.ks,
127                                                        "ext3")
128             self.__fsopts = kickstart.get_image_fsopts(self.ks,
129                                                        "defaults,noatime")
130             if self.__fstype in AFTER_MNT_FS.keys():
131                 self.__fstype = "ext4"
132
133             allloops = []
134             for part in sorted(kickstart.get_partitions(self.ks),
135                                key=lambda p: p.mountpoint):
136                 aft_fstype = None
137                 if part.fstype == "swap":
138                     continue
139                 elif part.fstype in AFTER_MNT_FS.keys():
140                     aft_fstype = part.fstype
141                     part.fstype = "ext4"
142
143                 label = part.label
144                 mp = part.mountpoint
145                 if mp == '/':
146                     # the base image
147                     if not label:
148                         label = self.name
149                 else:
150                     mp = mp.rstrip('/')
151                     if not label:
152                         msger.warning('no "label" specified for loop img at %s'
153                                       ', use the mountpoint as the name' % mp)
154                         label = mp.split('/')[-1]
155
156                 imgname = misc.strip_end(label, '.img') + '.img'
157                 allloops.append({
158                     'mountpoint': mp,
159                     'label': label,
160                     'name': imgname,
161                     'size': part.size or 4096L * 1024 * 1024,
162                     'fstype': part.fstype or 'ext3',
163                     'aft_fstype': aft_fstype or None,
164                     'extopts': part.extopts or None,
165                     'vdfsopts': part.vdfsopts or None,
166                     'squashfsopts': part.squashfsopts or None,
167                     'cpioopts': part.cpioopts or None,
168                     'loop': None,  # to be created in _mount_instroot
169                     'uuid': part.uuid or None,
170                     'kspart' : part,
171                     'exclude_image' : part.exclude_image or None,
172                     'no_shrink': part.no_shrink or False,
173                     'init_expand': part.init_expand or False,
174                     })
175             self._instloops = allloops
176
177         else:
178             self.__fstype = None
179             self.__fsopts = None
180             self._instloops = []
181
182         self._imgdir = None
183
184         if self.ks:
185             self.__image_size = kickstart.get_image_size(self.ks,
186                                                          4096L * 1024 * 1024)
187         else:
188             self.__image_size = 0
189
190         self._img_name = self.name + ".img"
191
192     def get_image_names(self):
193         if not self._instloops:
194             return None
195
196         names = []
197         for lo in self._instloops :
198             names.append(lo['name'])
199             for ro in AFTER_MNT_FS.values():
200                 names.append(lo['name'].replace('.img',ro))
201         return list(set(names))
202
203     def _set_fstype(self, fstype):
204         self.__fstype = fstype
205
206     def _set_image_size(self, imgsize):
207         self.__image_size = imgsize
208
209
210     #
211     # Properties
212     #
213     def __get_fslabel(self):
214         if self.__fslabel is None:
215             return self.name
216         else:
217             return self.__fslabel
218     def __set_fslabel(self, val):
219         if val is None:
220             self.__fslabel = None
221         else:
222             self.__fslabel = val[:FSLABEL_MAXLEN]
223     #A string used to label any filesystems created.
224     #
225     #Some filesystems impose a constraint on the maximum allowed size of the
226     #filesystem label. In the case of ext3 it's 16 characters, but in the case
227     #of ISO9660 it's 32 characters.
228     #
229     #mke2fs silently truncates the label, but mkisofs aborts if the label is
230     #too long. So, for convenience sake, any string assigned to this attribute
231     #is silently truncated to FSLABEL_MAXLEN (32) characters.
232     fslabel = property(__get_fslabel, __set_fslabel)
233
234     def __get_image(self):
235         if self._imgdir is None:
236             raise CreatorError("_image is not valid before calling mount()")
237         return os.path.join(self._imgdir, self._img_name)
238     #The location of the image file.
239     #
240     #This is the path to the filesystem image. Subclasses may use this path
241     #in order to package the image in _stage_final_image().
242     #
243     #Note, this directory does not exist before ImageCreator.mount() is called.
244     #
245     #Note also, this is a read-only attribute.
246     _image = property(__get_image)
247
248     def __get_blocksize(self):
249         return self.__blocksize
250     def __set_blocksize(self, val):
251         if self._instloops:
252             raise CreatorError("_blocksize must be set before calling mount()")
253         try:
254             self.__blocksize = int(val)
255         except ValueError:
256             raise CreatorError("'%s' is not a valid integer value "
257                                "for _blocksize" % val)
258     #The block size used by the image's filesystem.
259     #
260     #This is the block size used when creating the filesystem image. Subclasses
261     #may change this if they wish to use something other than a 4k block size.
262     #
263     #Note, this attribute may only be set before calling mount().
264     _blocksize = property(__get_blocksize, __set_blocksize)
265
266     def __get_fstype(self):
267         return self.__fstype
268     def __set_fstype(self, val):
269         if val != "ext2" and val != "ext3":
270             raise CreatorError("Unknown _fstype '%s' supplied" % val)
271         self.__fstype = val
272     #The type of filesystem used for the image.
273     #
274     #This is the filesystem type used when creating the filesystem image.
275     #Subclasses may change this if they wish to use something other ext3.
276     #
277     #Note, only ext2 and ext3 are currently supported.
278     #
279     #Note also, this attribute may only be set before calling mount().
280     _fstype = property(__get_fstype, __set_fstype)
281
282     def __get_fsopts(self):
283         return self.__fsopts
284     def __set_fsopts(self, val):
285         self.__fsopts = val
286     #Mount options of filesystem used for the image.
287     #
288     #This can be specified by --fsoptions="xxx,yyy" in part command in
289     #kickstart file.
290     _fsopts = property(__get_fsopts, __set_fsopts)
291
292
293     #
294     # Helpers for subclasses
295     #
296     def _resparse(self, size=None):
297         """Rebuild the filesystem image to be as sparse as possible.
298
299         This method should be used by subclasses when staging the final image
300         in order to reduce the actual space taken up by the sparse image file
301         to be as little as possible.
302
303         This is done by resizing the filesystem to the minimal size (thereby
304         eliminating any space taken up by deleted files) and then resizing it
305         back to the supplied size.
306
307         size -- the size in, in bytes, which the filesystem image should be
308                 resized to after it has been minimized; this defaults to None,
309                 causing the original size specified by the kickstart file to
310                 be used (or 4GiB if not specified in the kickstart).
311         """
312         minsize = 0
313         for item in self._instloops:
314             if not item['cpioopts']:
315                 if item['no_shrink']:
316                     item['loop'].resparse()
317                     continue
318                 if item['name'] == self._img_name:
319                     minsize = item['loop'].resparse(size)
320                 else:
321                     item['loop'].resparse(size)
322
323         return minsize
324
325     def _base_on(self, base_on=None):
326         if base_on and self._image != base_on:
327             shutil.copyfile(base_on, self._image)
328
329     def _check_imgdir(self):
330         if self._imgdir is None:
331             self._imgdir = self._mkdtemp()
332
333
334     #
335     # Actual implementation
336     #
337     def _mount_instroot(self, base_on=None):
338
339         if base_on and os.path.isfile(base_on):
340             self._imgdir = os.path.dirname(base_on)
341             imgname = os.path.basename(base_on)
342             self._base_on(base_on)
343             self._set_image_size(misc.get_file_size(self._image))
344
345             # here, self._instloops must be []
346             self._instloops.append({
347                  "mountpoint": "/",
348                  "label": self.name,
349                  "name": imgname,
350                  "size": self.__image_size or 4096L,
351                  "fstype": self.__fstype or "ext3",
352                  "extopts": None,
353                  "loop": None,
354                  "uuid": None,
355                  "kspart": None,
356                  "exclude_image" : None
357                  })
358
359         self._check_imgdir()
360
361         for loop in self._instloops:
362             fstype = loop['fstype']
363             mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
364             size = loop['size'] * 1024L * 1024L
365             imgname = loop['name']
366
367             if fstype in ("ext2", "ext3", "ext4"):
368                 MyDiskMount = fs.ExtDiskMount
369             elif fstype == "btrfs":
370                 MyDiskMount = fs.BtrfsDiskMount
371             elif fstype in ("vfat", "msdos"):
372                 MyDiskMount = fs.VfatDiskMount
373             else:
374                 raise MountError('Cannot support fstype: %s' % fstype)
375
376             loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
377                                            os.path.join(self._imgdir, imgname),
378                                            size),
379                                        mp,
380                                        fstype,
381                                        self._blocksize,
382                                        loop['label'],
383                                        fsuuid = loop['uuid'])
384
385             if fstype in ("ext2", "ext3", "ext4"):
386                 loop['loop'].extopts = loop['extopts']
387
388             try:
389                 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
390                 fs.makedirs(mp)
391                 loop['loop'].mount(init_expand=loop['init_expand'])
392                 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
393                 if loop['kspart'] and loop['kspart'].uuid is None and \
394                    loop['loop'].uuid:
395                     loop['kspart'].uuid = loop['loop'].uuid
396
397             except MountError, e:
398                 raise
399
400     def _unmount_instroot(self):
401         for item in reversed(self._instloops):
402             try:
403                 item['loop'].cleanup()
404             except:
405                 pass
406
407     def _stage_final_image(self):
408
409         if self.pack_to or self.shrink_image:
410             self._resparse(0)
411         else:
412             self._resparse()
413
414         for item in self._instloops:
415             imgfile = os.path.join(self._imgdir, item['name'])
416
417             if item['aft_fstype'] in AFTER_MNT_FS.keys():
418                 mountpoint = misc.mkdtemp()
419                 ext4img = os.path.join(self._imgdir, item['name'])
420                 runner.show('mount -t ext4 %s %s' % (ext4img, mountpoint))
421                 runner.show('ls -al %s' % (mountpoint))
422 #                item['loop'].mount(None, 'not_create')
423 #                point_mnt = os.path.join(self._instroot, item['mountpoint'].lstrip('/'))
424
425                 fs_suffix = AFTER_MNT_FS[item['aft_fstype']]
426                 if item['aft_fstype'] == "squashfs":
427 #                    fs.mksquashfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix)
428                     args = "mksquashfs " + mountpoint + " " + self._imgdir+"/"+item['label']+fs_suffix
429                     if item['squashfsopts']:
430                         squashfsopts=item['squashfsopts'].replace(',', ' ')
431                         runner.show("mksquashfs --help")
432                         runner.show("%s %s" % (args, squashfsopts))
433                     else:
434                         runner.show("%s " % args)
435
436                 if item['aft_fstype'] == "vdfs":
437                     ##FIXME temporary code - replace this with fs.mkvdfs()
438                     if item['vdfsopts']:
439                         vdfsopts=item['vdfsopts'].replace(',', ' ')
440                     else:
441                         vdfsopts="-i -z 1024M"
442
443                     fullpathmkvdfs = "mkfs.vdfs" #find_binary_path("mkfs.vdfs")
444                     runner.show("%s --help" % fullpathmkvdfs)
445 #                    fs.mkvdfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix, vdfsopts)
446                     runner.show('%s %s -r %s %s' % (fullpathmkvdfs, vdfsopts, mountpoint, self._imgdir+"/"+item['label']+fs_suffix))
447
448                 runner.show('umount %s' % mountpoint)
449 #               os.unlink(mountpoint)
450                 runner.show('mv %s %s' % (self._imgdir+"/"+item['label']+fs_suffix, self._imgdir+"/"+item['label']+".img") )
451                 runner.show('ls -al %s' % self._imgdir)
452
453             if item['fstype'] == "ext4":
454                 if not item['cpioopts']:
455                     runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
456                             % imgfile)
457                     runner.quiet(["/sbin/e2fsck", "-f", "-y", imgfile])
458             self.image_files.setdefault('partitions', {}).update(
459                     {item['mountpoint']: item['label']})
460             if self.compress_image:
461                 compressing(imgfile, self.compress_image)
462                 self.image_files.setdefault('image_files', []).append(
463                                 '.'.join([item['name'], self.compress_image]))
464             else:
465                 self.image_files.setdefault('image_files', []).append(item['name'])
466
467         for item in os.listdir(self._imgdir):
468             imgfile = os.path.join(self._imgdir, item)
469             imgsize = os.path.getsize(imgfile)
470             msger.info("filesystem size of %s : %s bytes" % (item, imgsize))
471
472         self.run_sign_scripts()
473         if not self.pack_to:
474             for item in os.listdir(self._imgdir):
475                 shutil.move(os.path.join(self._imgdir, item),
476                             os.path.join(self._outdir, item))
477         else:
478             msger.info("Pack all loop images together to %s" % self.pack_to)
479             dstfile = os.path.join(self._outdir, self.pack_to)
480             packing(dstfile, self._imgdir)
481             self.image_files['image_files'] = [self.pack_to]
482
483
484         if self.pack_to:
485             mountfp_xml = os.path.splitext(self.pack_to)[0]
486             mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
487         else:
488             mountfp_xml = self.name + ".xml"
489         # save mount points mapping file to xml
490         save_mountpoints(os.path.join(self._outdir, mountfp_xml),
491                          self._instloops,
492                          self.target_arch)
493
494     def copy_attachment(self):
495         if not hasattr(self, '_attachment') or not self._attachment:
496             return
497
498         self._check_imgdir()
499
500         msger.info("Copying attachment files...")
501         for item in self._attachment:
502             if not os.path.exists(item):
503                 continue
504             dpath = os.path.join(self._imgdir, os.path.basename(item))
505             msger.verbose("Copy attachment %s to %s" % (item, dpath))
506             shutil.copy(item, dpath)
507
508     def create_manifest(self):
509         if self.compress_image:
510             self.image_files.update({'compress': self.compress_image})
511         super(LoopImageCreator, self).create_manifest()