a3fbfbc2286a5bc60dc4784718321965b15980a1
[tools/mic.git] / mic / chroot.py
1 #!/usr/bin/python -tt
2 #
3 # Copyright (c) 2009, 2010, 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 from __future__ import with_statement
19 import os
20 import re
21 import shutil
22 import subprocess
23
24 from mic import msger
25 from mic.conf import configmgr
26 from mic.utils import misc, errors, runner, fs_related
27
28 #####################################################################
29 ### GLOBAL CONSTANTS
30 #####################################################################
31
32 chroot_bindmounts = None
33 chroot_lockfd = -1
34 chroot_lock = ""
35 BIND_MOUNTS = (
36                 "/proc",
37                 "/proc/sys/fs/binfmt_misc",
38                 "/sys",
39                 "/dev",
40                 "/dev/pts",
41                 "/var/lib/dbus",
42                 "/var/run/dbus",
43                 "/var/lock",
44                 "/lib/modules",
45               )
46
47 #####################################################################
48 ### GLOBAL ROUTINE
49 #####################################################################
50
51 def ELF_arch(chrootdir):
52     #FIXME: if chkfiles are symlink, it will be complex
53     chkfiles = ('/bin/bash', '/sbin/init')
54     # regular expression to arch mapping
55     mapping = {
56                 r"Intel 80[0-9]86": "i686",
57                 r"x86-64": "x86_64",
58                 r"ARM": "arm",
59               }
60
61     for path in chkfiles:
62         cpath = os.path.join(chrootdir, path.lstrip('/'))
63         if not os.path.exists(cpath):
64             continue
65
66         outs = runner.outs(['file', cpath])
67         for ptn in mapping.keys():
68             if re.search(ptn, outs):
69                 return mapping[ptn]
70
71     raise errors.CreatorError("Failed to detect architecture of chroot: %s" %
72                               chrootdir)
73
74 def get_bindmounts(chrootdir, bindmounts = None):
75     # bindmounts should be a string like '/dev:/dev'
76     # FIXME: refine the bindmounts from string to dict
77     global chroot_bindmounts
78
79     def totuple(string):
80         if ':' in string:
81             src, dst = string.split(':', 1)
82         else:
83             src = string
84             dst = None
85
86         return (src or None, dst or None)
87
88     if chroot_bindmounts:
89         return chroot_bindmounts
90
91     chroot_bindmounts = []
92     bindmounts = bindmounts or ""
93     mountlist = []
94
95     for mount in bindmounts.split(";"):
96         if not mount:
97             continue
98
99         (src, dst) = totuple(mount)
100
101         if src in BIND_MOUNTS or src == '/':
102             continue
103
104         if not os.path.exists(src):
105             os.makedirs(src)
106
107         if dst and os.path.isdir("%s/%s" % (chrootdir, dst)):
108             msger.warning("%s existed in %s , skip it." % (dst, chrootdir))
109             continue
110
111         mountlist.append(totuple(mount))
112
113     for mntpoint in BIND_MOUNTS:
114         if os.path.isdir(mntpoint):
115             mountlist.append(tuple((mntpoint, None)))
116
117     for pair in mountlist:
118         bmount = fs_related.BindChrootMount(pair[0], chrootdir, pair[1])
119         chroot_bindmounts.append(bmount)
120
121     return chroot_bindmounts
122
123 #####################################################################
124 ### SETUP CHROOT ENVIRONMENT
125 #####################################################################
126
127 def bind_mount(chrootmounts):
128     for b in chrootmounts:
129         msger.verbose("bind_mount: %s -> %s" % (b.src, b.dest))
130         b.mount()
131
132 def setup_resolv(chrootdir):
133     try:
134         shutil.copyfile("/etc/resolv.conf", chrootdir + "/etc/resolv.conf")
135     except:
136         pass
137
138 def setup_mtab(chrootdir):
139     mtab = "/etc/mtab"
140     dstmtab = chrootdir + mtab
141     if not os.path.islink(dstmtab):
142         shutil.copyfile(mtab, dstmtab)
143
144 def setup_chrootenv(chrootdir, bindmounts = None):
145     # bind mounting
146     bind_mount(get_bindmounts(chrootdir, bindmounts))
147     # setup resolv.conf
148     setup_resolv(chrootdir)
149     # update /etc/mtab
150     setup_mtab(chrootdir)
151
152     # lock
153     chroot_lock = os.path.join(chrootdir, ".chroot.lock")
154     chroot_lockfd = open(chroot_lock, "w")
155
156     return None
157
158 ######################################################################
159 ### CLEANUP CHROOT ENVIRONMENT
160 ######################################################################
161
162 def bind_unmount(chrootmounts):
163     for b in reversed(chrootmounts):
164         msger.verbose("bind_unmount: %s -> %s" % (b.src, b.dest))
165         b.unmount()
166
167 def cleanup_resolv(chrootdir):
168     try:
169         fd = open(chrootdir + "/etc/resolv.conf", "w")
170         fd.truncate(0)
171         fd.close()
172     except:
173         pass
174
175 def kill_processes(chrootdir):
176     import glob
177     for fp in glob.glob("/proc/*/root"):
178         try:
179             if os.readlink(fp) == chrootdir:
180                 pid = int(fp.split("/")[2])
181                 os.kill(pid, 9)
182         except:
183             pass
184
185 def cleanup_mtab(chrootdir):
186     if os.path.exists(chrootdir + "/etc/mtab"):
187         os.unlink(chrootdir + "/etc/mtab")
188
189 def cleanup_mounts(chrootdir):
190     umountcmd = misc.find_binary_path("umount")
191     mounts = open('/proc/mounts').readlines()
192     for line in reversed(mounts):
193         if chrootdir not in line:
194             continue
195
196         point = line.split()[1]
197
198         # '/' to avoid common name prefix
199         if chrootdir == point or point.startswith(chrootdir + '/'):
200             args = [ umountcmd, "-l", point ]
201             ret = runner.quiet(args)
202             if ret != 0:
203                 msger.warning("failed to unmount %s" % point)
204             if os.path.isdir(point) and len(os.listdir(point)) == 0:
205                 shutil.rmtree(point)
206             else:
207                 msger.warning("%s is not directory or is not empty" % point)
208
209 def cleanup_chrootenv(chrootdir, bindmounts=None, globalmounts=()):
210     # unlock
211     chroot_lockfd.close()
212     # kill processes
213     kill_processes(chrootdir)
214     # clean mtab
215     cleanup_mtab(chrootdir)
216     # clean resolv.conf
217     cleanup_resolv(chrootdir)
218     # bind umounting
219     bind_unmount(get_bindmounts(chrootdir, bindmounts))
220     # clean up mounts
221     cleanup_mounts(chrootdir)
222
223     return None
224
225 #####################################################################
226 ### CHROOT STUFF
227 #####################################################################
228
229 def savefs_before_chroot(chrootdir, saveto = None):
230     if configmgr.chroot['saveto']:
231         savefs = True
232         saveto = configmgr.chroot['saveto']
233         wrnmsg = "Can't save chroot fs for dir %s exists" % saveto
234         if saveto == chrootdir:
235             savefs = False
236             wrnmsg = "Dir %s is being used to chroot" % saveto
237         elif os.path.exists(saveto):
238             if msger.ask("Dir %s already exists, cleanup and continue?" %
239                          saveto):
240                 shutil.rmtree(saveto, ignore_errors = True)
241                 savefs = True
242             else:
243                 savefs = False
244
245         if savefs:
246             msger.info("Saving image to directory %s" % saveto)
247             fs_related.makedirs(os.path.dirname(os.path.abspath(saveto)))
248             runner.quiet("cp -af %s %s" % (chrootdir, saveto))
249             devs = ['dev/fd',
250                     'dev/stdin',
251                     'dev/stdout',
252                     'dev/stderr',
253                     'etc/mtab']
254             ignlst = [os.path.join(saveto, x) for x in devs]
255             map(os.unlink, filter(os.path.exists, ignlst))
256         else:
257             msger.warning(wrnmsg)
258
259 def cleanup_after_chroot(targettype, imgmount, tmpdir, tmpmnt):
260     if imgmount and targettype == "img":
261         imgmount.cleanup()
262
263     if tmpdir:
264         shutil.rmtree(tmpdir, ignore_errors = True)
265
266     if tmpmnt:
267         shutil.rmtree(tmpmnt, ignore_errors = True)
268
269 def chroot(chrootdir, bindmounts = None, execute = "/bin/bash"):
270     def mychroot():
271         os.chroot(chrootdir)
272         os.chdir("/")
273
274     arch = ELF_arch(chrootdir)
275     if arch == "arm":
276         qemu_emulator = misc.setup_qemu_emulator(chrootdir, "arm")
277     else:
278         qemu_emulator = None
279
280     savefs_before_chroot(chrootdir, None)
281
282     try:
283         msger.info("Launching shell. Exit to continue.\n"
284                    "----------------------------------")
285         globalmounts = setup_chrootenv(chrootdir, bindmounts)
286         subprocess.call(execute, preexec_fn = mychroot, shell=True)
287
288     except OSError, err:
289         raise errors.CreatorError("chroot err: %s" % str(err))
290
291     finally:
292         cleanup_chrootenv(chrootdir, bindmounts, globalmounts)
293         if qemu_emulator:
294             os.unlink(chrootdir + qemu_emulator)