pack images together and support compressed file format
[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
26 from baseimager import BaseImageCreator
27
28 # The maximum string length supported for LoopImageCreator.fslabel
29 FSLABEL_MAXLEN = 32
30
31 def save_mountpoints(fpath, loops, arch = None):
32     """Save mount points mapping to file
33
34     :fpath, the xml file to store partition info
35     :loops, dict of partition info
36     :arch, image arch
37     """
38
39     if not fpath or not loops:
40         return
41
42     from xml.dom import minidom
43     doc = minidom.Document()
44     imgroot = doc.createElement("image")
45     doc.appendChild(imgroot)
46     if arch:
47         imgroot.setAttribute('arch', arch)
48     for loop in loops:
49         part = doc.createElement("partition")
50         imgroot.appendChild(part)
51         for (key, val) in loop.items():
52             if isinstance(val, fs.Mount):
53                 continue
54             part.setAttribute(key, str(val))
55
56     with open(fpath, 'w') as wf:
57         wf.write(doc.toprettyxml(indent='  '))
58
59     return
60
61 def load_mountpoints(fpath):
62     """Load mount points mapping from file
63
64     :fpath, file path to load
65     """
66
67     if not fpath:
68         return
69
70     from xml.dom import minidom
71     mount_maps = []
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())
77
78         try:
79             mp = (p['mountpoint'], p['label'], p['name'],
80                   int(p['size']), p['fstype'])
81         except KeyError:
82             msger.warning("Wrong format line in file: %s" % fpath)
83         except ValueError:
84             msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
85         else:
86             mount_maps.append(mp)
87
88     return mount_maps
89
90 class LoopImageCreator(BaseImageCreator):
91     """Installs a system into a loopback-mountable filesystem image.
92
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.
96
97     When specifying multiple partitions in kickstart file, each partition
98     will be created as a separated loop image.
99     """
100
101     def __init__(self, creatoropts=None, pkgmgr=None, compress_image=None):
102         """Initialize a LoopImageCreator instance.
103
104         This method takes the same arguments as ImageCreator.__init__()
105         with the addition of:
106
107         fslabel -- A string used as a label for any filesystems created.
108         """
109
110         BaseImageCreator.__init__(self, creatoropts, pkgmgr)
111
112         self.compress_image = compress_image
113
114         self.__fslabel = None
115         self.fslabel = self.name
116
117         self.__blocksize = 4096
118         if self.ks:
119             self.__fstype = kickstart.get_image_fstype(self.ks,
120                                                        "ext3")
121             self.__fsopts = kickstart.get_image_fsopts(self.ks,
122                                                        "defaults,noatime")
123
124             allloops = []
125             for part in sorted(kickstart.get_partitions(self.ks),
126                                key=lambda p: p.mountpoint):
127                 if part.fstype == "swap":
128                     continue
129
130                 label = part.label
131                 mp = part.mountpoint
132                 if mp == '/':
133                     # the base image
134                     if not label:
135                         label = self.name
136                 else:
137                     mp = mp.rstrip('/')
138                     if not label:
139                         msger.warning('no "label" specified for loop img at %s'
140                                       ', use the mountpoint as the name' % mp)
141                         label = mp.split('/')[-1]
142
143                 imgname = misc.strip_end(label, '.img') + '.img'
144                 allloops.append({
145                     'mountpoint': mp,
146                     'label': label,
147                     'name': imgname,
148                     'size': part.size or 4096L * 1024 * 1024,
149                     'fstype': part.fstype or 'ext3',
150                     'loop': None,  # to be created in _mount_instroot
151                     })
152             self._instloops = allloops
153
154         else:
155             self.__fstype = None
156             self.__fsopts = None
157             self._instloops = []
158
159         self.__imgdir = None
160
161         if self.ks:
162             self.__image_size = kickstart.get_image_size(self.ks,
163                                                          4096L * 1024 * 1024)
164         else:
165             self.__image_size = 0
166
167         self._img_name = self.name + ".img"
168
169     def _set_fstype(self, fstype):
170         self.__fstype = fstype
171
172     def _set_image_size(self, imgsize):
173         self.__image_size = imgsize
174
175
176     #
177     # Properties
178     #
179     def __get_fslabel(self):
180         if self.__fslabel is None:
181             return self.name
182         else:
183             return self.__fslabel
184     def __set_fslabel(self, val):
185         if val is None:
186             self.__fslabel = None
187         else:
188             self.__fslabel = val[:FSLABEL_MAXLEN]
189     #A string used to label any filesystems created.
190     #
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.
194     #
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)
199
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.
205     #
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().
208     #
209     #Note, this directory does not exist before ImageCreator.mount() is called.
210     #
211     #Note also, this is a read-only attribute.
212     _image = property(__get_image)
213
214     def __get_blocksize(self):
215         return self.__blocksize
216     def __set_blocksize(self, val):
217         if self._instloops:
218             raise CreatorError("_blocksize must be set before calling mount()")
219         try:
220             self.__blocksize = int(val)
221         except ValueError:
222             raise CreatorError("'%s' is not a valid integer value "
223                                "for _blocksize" % val)
224     #The block size used by the image's filesystem.
225     #
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.
228     #
229     #Note, this attribute may only be set before calling mount().
230     _blocksize = property(__get_blocksize, __set_blocksize)
231
232     def __get_fstype(self):
233         return self.__fstype
234     def __set_fstype(self, val):
235         if val != "ext2" and val != "ext3":
236             raise CreatorError("Unknown _fstype '%s' supplied" % val)
237         self.__fstype = val
238     #The type of filesystem used for the image.
239     #
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.
242     #
243     #Note, only ext2 and ext3 are currently supported.
244     #
245     #Note also, this attribute may only be set before calling mount().
246     _fstype = property(__get_fstype, __set_fstype)
247
248     def __get_fsopts(self):
249         return self.__fsopts
250     def __set_fsopts(self, val):
251         self.__fsopts = val
252     #Mount options of filesystem used for the image.
253     #
254     #This can be specified by --fsoptions="xxx,yyy" in part command in
255     #kickstart file.
256     _fsopts = property(__get_fsopts, __set_fsopts)
257
258
259     #
260     # Helpers for subclasses
261     #
262     def _resparse(self, size=None):
263         """Rebuild the filesystem image to be as sparse as possible.
264
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.
268
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.
272
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).
277         """
278         minsize = 0
279         for item in self._instloops:
280             if item['name'] == self._img_name:
281                 minsize = item['loop'].resparse(size)
282             else:
283                 item['loop'].resparse(size)
284
285         return minsize
286
287     def _base_on(self, base_on=None):
288         if base_on and self._image != base_on:
289             shutil.copyfile(base_on, self._image)
290
291     def _check_imgdir(self):
292         if self.__imgdir is None:
293             self.__imgdir = self._mkdtemp()
294
295
296     #
297     # Actual implementation
298     #
299     def _mount_instroot(self, base_on=None):
300
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))
306
307             # here, self._instloops must be []
308             self._instloops.append({
309                  "mountpoint": "/",
310                  "label": self.name,
311                  "name": imgname,
312                  "size": self.__image_size or 4096L,
313                  "fstype": self.__fstype or "ext3",
314                  "loop": None
315                  })
316
317         self._check_imgdir()
318
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']
324
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
331             else:
332                 msger.error('Cannot support fstype: %s' % fstype)
333
334             loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
335                                            os.path.join(self.__imgdir, imgname),
336                                            size),
337                                        mp,
338                                        fstype,
339                                        self._blocksize,
340                                        loop['label'])
341
342             try:
343                 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
344                 fs.makedirs(mp)
345                 loop['loop'].mount()
346             except MountError, e:
347                 raise
348
349     def _unmount_instroot(self):
350         for item in reversed(self._instloops):
351             item['loop'].cleanup()
352
353     def _stage_final_image(self):
354
355         if self.pack_to:
356             self._resparse(0)
357         else:
358             self._resparse()
359
360
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 '
365                             % imgfile)
366             if self.compress_image:
367                 misc.compressing(imgfile, self.compress_image)
368
369         if not self.pack_to:
370             for item in os.listdir(self.__imgdir):
371                 shutil.move(os.path.join(self.__imgdir, item),
372                             os.path.join(self._outdir, item))
373         else:
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)
377
378         if self.pack_to:
379             mountfp_xml = os.path.splitext(self.pack_to)[0].rstrip('.tar') + ".xml"
380         else:
381             mountfp_xml = self.name + ".xml"
382         # save mount points mapping file to xml
383         save_mountpoints(os.path.join(self._outdir, mountfp_xml),
384                          self._instloops,
385                          self.target_arch)
386
387     def copy_attachment(self):
388         if not hasattr(self, '_attachment') or not self._attachment:
389             return
390
391         self._check_imgdir()
392
393         msger.info("Copying attachment files...")
394         for item in self._attachment:
395             if not os.path.exists(item):
396                 continue
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)
400