make loop image type support multiple partitions
[platform/upstream/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 shutil
20
21 from mic import kickstart, msger
22 from mic.utils.errors import CreatorError, MountError
23 from mic.utils import misc, fs_related as fs
24
25 from baseimager import BaseImageCreator
26
27 FSLABEL_MAXLEN = 32
28 """The maximum string length supported for LoopImageCreator.fslabel."""
29
30 class LoopImageCreator(BaseImageCreator):
31     """Installs a system into a loopback-mountable filesystem image.
32
33         LoopImageCreator is a straightforward ImageCreator subclass; the system
34         is installed into an ext3 filesystem on a sparse file which can be
35         subsequently loopback-mounted.
36
37         When specifying multiple partitions in kickstart file, each partition
38         will be created as a separated loop image.
39     """
40
41     def __init__(self, creatoropts = None, pkgmgr = None):
42         """Initialize a LoopImageCreator instance.
43
44             This method takes the same arguments as ImageCreator.__init__() with
45             the addition of:
46
47             fslabel -- A string used as a label for any filesystems created.
48         """
49         BaseImageCreator.__init__(self, creatoropts, pkgmgr)
50
51         self.__fslabel = None
52         self.fslabel = self.name
53
54         self.__blocksize = 4096
55         if self.ks:
56             self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
57             self.__fsopts = kickstart.get_image_fsopts(self.ks, "defaults,noatime")
58
59             allloops = []
60             for part in sorted(kickstart.get_partitions(self.ks),
61                                key = lambda p: p.mountpoint):
62                 label = part.label
63
64                 mp = part.mountpoint
65                 if mp == '/':
66                     # the base image
67                     if not label:
68                         label =  self.name
69                 else:
70                     mp = mp.rstrip('/')
71                     if not label:
72                         msger.warning('no "label" specified for loop img at %s, use the mountpoint as the name' % mp)
73                         label = mp.split('/')[-1]
74
75                 imgname = misc.strip_end(label,'.img') + '.img'
76                 allloops.append({
77                     'mountpoint': mp,
78                     'label': label,
79                     'name': imgname,
80                     'size': part.size or 4096L * 1024 * 1024,
81                     'fstype': part.fstype or 'ext4',
82                     'loop': None, # to be created in _mount_instroot
83                     })
84             self._instloops = allloops
85
86         else:
87             self.__fstype = None
88             self.__fsopts = None
89             self._instloops = []
90
91         self.__imgdir = None
92
93         if self.ks:
94             self.__image_size = kickstart.get_image_size(self.ks,
95                                                          4096L * 1024 * 1024)
96         else:
97             self.__image_size = 0
98
99         self._img_name = self.name + ".img"
100
101
102     def _set_fstype(self, fstype):
103         self.__fstype = fstype
104
105     def _set_image_size(self, imgsize):
106         self.__image_size = imgsize
107
108
109     #
110     # Properties
111     #
112     def __get_fslabel(self):
113         if self.__fslabel is None:
114             return self.name
115         else:
116             return self.__fslabel
117     def __set_fslabel(self, val):
118         if val is None:
119             self.__fslabel = None
120         else:
121             self.__fslabel = val[:FSLABEL_MAXLEN]
122     #A string used to label any filesystems created.
123     #
124     #Some filesystems impose a constraint on the maximum allowed size of the
125     #filesystem label. In the case of ext3 it's 16 characters, but in the case
126     #of ISO9660 it's 32 characters.
127     #
128     #mke2fs silently truncates the label, but mkisofs aborts if the label is too
129     #long. So, for convenience sake, any string assigned to this attribute is
130     #silently truncated to FSLABEL_MAXLEN (32) characters.
131     fslabel = property(__get_fslabel, __set_fslabel)
132
133     def __get_image(self):
134         if self.__imgdir is None:
135             raise CreatorError("_image is not valid before calling mount()")
136         return os.path.join(self.__imgdir, self._img_name)
137     #The location of the image file.
138     #
139     #This is the path to the filesystem image. Subclasses may use this path
140     #in order to package the image in _stage_final_image().
141     #
142     #Note, this directory does not exist before ImageCreator.mount() is called.
143     #
144     #Note also, this is a read-only attribute.
145     _image = property(__get_image)
146
147     def __get_blocksize(self):
148         return self.__blocksize
149     def __set_blocksize(self, val):
150         if self._instloops:
151             raise CreatorError("_blocksize must be set before calling mount()")
152         try:
153             self.__blocksize = int(val)
154         except ValueError:
155             raise CreatorError("'%s' is not a valid integer value "
156                                "for _blocksize" % val)
157     #The block size used by the image's filesystem.
158     #
159     #This is the block size used when creating the filesystem image. Subclasses
160     #may change this if they wish to use something other than a 4k block size.
161     #
162     #Note, this attribute may only be set before calling mount().
163     _blocksize = property(__get_blocksize, __set_blocksize)
164
165     def __get_fstype(self):
166         return self.__fstype
167     def __set_fstype(self, val):
168         if val != "ext2" and val != "ext3":
169             raise CreatorError("Unknown _fstype '%s' supplied" % val)
170         self.__fstype = val
171     #The type of filesystem used for the image.
172     #
173     #This is the filesystem type used when creating the filesystem image.
174     #Subclasses may change this if they wish to use something other ext3.
175     #
176     #Note, only ext2 and ext3 are currently supported.
177     #
178     #Note also, this attribute may only be set before calling mount().
179     _fstype = property(__get_fstype, __set_fstype)
180
181     def __get_fsopts(self):
182         return self.__fsopts
183     def __set_fsopts(self, val):
184         self.__fsopts = val
185     #Mount options of filesystem used for the image.
186     #
187     #This can be specified by --fsoptions="xxx,yyy" in part command in
188     #kickstart file.
189     _fsopts = property(__get_fsopts, __set_fsopts)
190
191
192     #
193     # Helpers for subclasses
194     #
195     def _resparse(self, size = None):
196         """Rebuild the filesystem image to be as sparse as possible.
197
198             This method should be used by subclasses when staging the final image
199             in order to reduce the actual space taken up by the sparse image file
200             to be as little as possible.
201
202             This is done by resizing the filesystem to the minimal size (thereby
203             eliminating any space taken up by deleted files) and then resizing it
204             back to the supplied size.
205
206             size -- the size in, in bytes, which the filesystem image should be
207                     resized to after it has been minimized; this defaults to None,
208                     causing the original size specified by the kickstart file to
209                     be used (or 4GiB if not specified in the kickstart).
210         """
211         minsize = 0
212         for item in self._instloops:
213             if item['name'] == self._img_name:
214                 minsize = item['loop'].resparse(size)
215             else:
216                 item['loop'].resparse(size)
217
218         return minsize
219
220     def _base_on(self, base_on):
221         if base_on is not None:
222             shutil.copyfile(base_on, self._image)
223
224     def _check_imgdir(self):
225         if self.__imgdir is None:
226             self.__imgdir = self._mkdtemp()
227
228     #
229     # Actual implementation
230     #
231     def _mount_instroot(self, base_on = None):
232         self._check_imgdir()
233         self._base_on(base_on)
234         imgdir = os.path.dirname(self._image)
235
236         for loop in self._instloops:
237             fstype = loop['fstype']
238             mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
239             size = loop['size'] * 1024L * 1024L
240             imgname = loop['name']
241
242             if fstype in ("ext2", "ext3", "ext4"):
243                 MyDiskMount = fs.ExtDiskMount
244             elif fstype == "btrfs":
245                 MyDiskMount = fs.BtrfsDiskMount
246             elif fstype in ("vfat", "msdos"):
247                 MyDiskMount = fs.VfatDiskMount
248             else:
249                 msger.error('Cannot support fstype: %s' % fstype)
250
251             loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(os.path.join(imgdir, imgname), size),
252                                        mp,
253                                        fstype,
254                                        self._blocksize,
255                                        loop['label'])
256
257             try:
258                 msger.verbose('Mounting image "%s" on "%s"' %(imgname, mp))
259                 fs.makedirs(mp)
260                 loop['loop'].mount()
261             except errors.MountError, e:
262                 raise
263
264     def _unmount_instroot(self):
265         for item in reversed(self._instloops):
266             item['loop'].cleanup()
267
268     def _stage_final_image(self):
269         self._resparse()
270         for item in self._instloops:
271             shutil.move(os.path.join(self.__imgdir, item['name']),
272                         os.path.join(self._outdir, item['name']))