ac2c007215b102dfcbeb339c8bf835386d7862e7
[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, lock
27
28 #####################################################################
29 ### GLOBAL CONSTANTS
30 #####################################################################
31
32 chroot_bindmounts = None
33 chroot_lock = None
34 BIND_MOUNTS = (
35                 "/proc",
36                 "/proc/sys/fs/binfmt_misc",
37                 "/sys",
38                 "/dev",
39                 "/dev/pts",
40                 "/var/lib/dbus",
41                 "/var/run/dbus",
42                 "/var/lock",
43                 "/lib/modules",
44               )
45
46 #####################################################################
47 ### GLOBAL ROUTINE
48 #####################################################################
49
50 def ELF_arch(chrootdir):
51     """ detect the architecture of an ELF file """
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     """ calculate all bind mount entries for global usage """
76     # bindmounts should be a string like '/dev:/dev'
77     # FIXME: refine the bindmounts from string to dict
78     global chroot_bindmounts
79
80     def totuple(string):
81         """ convert string contained ':' to a tuple """
82         if ':' in string:
83             src, dst = string.split(':', 1)
84         else:
85             src = string
86             dst = None
87
88         return (src or None, dst or None)
89
90     if chroot_bindmounts:
91         return chroot_bindmounts
92
93     chroot_bindmounts = []
94     bindmounts = bindmounts or ""
95     mountlist = []
96
97     for mount in bindmounts.split(";"):
98         if not mount:
99             continue
100
101         (src, dst) = totuple(mount)
102
103         if src in BIND_MOUNTS or src == '/':
104             continue
105
106         if not os.path.exists(src):
107             os.makedirs(src)
108
109         if dst and os.path.isdir("%s/%s" % (chrootdir, dst)):
110             msger.warning("%s existed in %s , skip it." % (dst, chrootdir))
111             continue
112
113         mountlist.append(totuple(mount))
114
115     for mntpoint in BIND_MOUNTS:
116         if os.path.isdir(mntpoint):
117             mountlist.append(tuple((mntpoint, None)))
118
119     for pair in mountlist:
120         if pair[0] == "/lib/modules":
121             opt = "ro"
122         else:
123             opt = None
124         bmount = fs_related.BindChrootMount(pair[0], chrootdir, pair[1], opt)
125         chroot_bindmounts.append(bmount)
126
127     return chroot_bindmounts
128
129 #####################################################################
130 ### SETUP CHROOT ENVIRONMENT
131 #####################################################################
132
133 def bind_mount(chrootmounts):
134     """ perform bind mounting """
135     for mnt in chrootmounts:
136         msger.verbose("bind_mount: %s -> %s" % (mnt.src, mnt.dest))
137         mnt.mount()
138
139 def setup_resolv(chrootdir):
140     """ resolve network """
141     try:
142         shutil.copyfile("/etc/resolv.conf", chrootdir + "/etc/resolv.conf")
143     except (OSError, IOError):
144         pass
145
146 def setup_mtab(chrootdir):
147     """ adjust mount table """
148     mtab = "/etc/mtab"
149     dstmtab = chrootdir + mtab
150     if not os.path.islink(dstmtab):
151         shutil.copyfile(mtab, dstmtab)
152
153 def setup_chrootenv(chrootdir, bindmounts = None):
154     """ setup chroot environment """
155     global chroot_lock
156
157     # acquire the lock
158     if not chroot_lock:
159         lockpath = os.path.join(chrootdir, '.chroot.lock')
160         chroot_lock = lock.SimpleLockfile(lockpath)
161     chroot_lock.acquire()
162     # bind mounting
163     bind_mount(get_bindmounts(chrootdir, bindmounts))
164     # setup resolv.conf
165     setup_resolv(chrootdir)
166     # update /etc/mtab
167     setup_mtab(chrootdir)
168
169     return None
170
171 ######################################################################
172 ### CLEANUP CHROOT ENVIRONMENT
173 ######################################################################
174
175 def bind_unmount(chrootmounts):
176     """ perform bind unmounting """
177     for mnt in reversed(chrootmounts):
178         msger.verbose("bind_unmount: %s -> %s" % (mnt.src, mnt.dest))
179         mnt.unmount()
180
181 def cleanup_resolv(chrootdir):
182     """ clear resolv.conf """
183     try:
184         fdes = open(chrootdir + "/etc/resolv.conf", "w")
185         fdes.truncate(0)
186         fdes.close()
187     except (OSError, IOError):
188         pass
189
190 def kill_proc_inchroot(chrootdir):
191     """ kill all processes running inside chrootdir """
192     import glob
193     for fpath in glob.glob("/proc/*/root"):
194         try:
195             if os.readlink(fpath) == chrootdir:
196                 pid = int(fpath.split("/")[2])
197                 os.kill(pid, 9)
198         except (OSError, ValueError):
199             pass
200
201 def cleanup_mtab(chrootdir):
202     """ remove mtab file """
203     if os.path.exists(chrootdir + "/etc/mtab"):
204         os.unlink(chrootdir + "/etc/mtab")
205
206 def cleanup_mounts(chrootdir):
207     """ clean up all mount entries owned by chrootdir """
208     umountcmd = misc.find_binary_path("umount")
209     mounts = open('/proc/mounts').readlines()
210     for line in reversed(mounts):
211         if chrootdir not in line:
212             continue
213
214         point = line.split()[1]
215
216         # '/' to avoid common name prefix
217         if chrootdir == point or point.startswith(chrootdir + '/'):
218             args = [ umountcmd, "-l", point ]
219             ret = runner.quiet(args)
220             if ret != 0:
221                 msger.warning("failed to unmount %s" % point)
222             if os.path.isdir(point) and len(os.listdir(point)) == 0:
223                 shutil.rmtree(point)
224             else:
225                 msger.warning("%s is not directory or is not empty" % point)
226
227 def cleanup_chrootenv(chrootdir, bindmounts=None, globalmounts=()):
228     """ clean up chroot environment """
229     global chroot_lock
230
231     # kill processes
232     kill_proc_inchroot(chrootdir)
233     # clean mtab
234     cleanup_mtab(chrootdir)
235     # clean resolv.conf
236     cleanup_resolv(chrootdir)
237     # bind umounting
238     bind_unmount(get_bindmounts(chrootdir, bindmounts))
239     # clean up mounts
240     cleanup_mounts(chrootdir)
241     # release the lock
242     if chroot_lock:
243         chroot_lock.release()
244         chroot_lock = None
245
246     return None
247
248 #####################################################################
249 ### CHROOT STUFF
250 #####################################################################
251
252 def savefs_before_chroot(chrootdir, saveto = None):
253     """ backup chrootdir to another directory before chrooting in """
254     if configmgr.chroot['saveto']:
255         savefs = True
256         saveto = configmgr.chroot['saveto']
257         wrnmsg = "Can't save chroot fs for dir %s exists" % saveto
258         if saveto == chrootdir:
259             savefs = False
260             wrnmsg = "Dir %s is being used to chroot" % saveto
261         elif os.path.exists(saveto):
262             if msger.ask("Dir %s already exists, cleanup and continue?" %
263                          saveto):
264                 shutil.rmtree(saveto, ignore_errors = True)
265                 savefs = True
266             else:
267                 savefs = False
268
269         if savefs:
270             msger.info("Saving image to directory %s" % saveto)
271             fs_related.makedirs(os.path.dirname(os.path.abspath(saveto)))
272             runner.quiet("cp -af %s %s" % (chrootdir, saveto))
273             devs = ['dev/fd',
274                     'dev/stdin',
275                     'dev/stdout',
276                     'dev/stderr',
277                     'etc/mtab']
278             ignlst = [os.path.join(saveto, x) for x in devs]
279             map(os.unlink, filter(os.path.exists, ignlst))
280         else:
281             msger.warning(wrnmsg)
282
283 def cleanup_after_chroot(targettype, imgmount, tmpdir, tmpmnt):
284     """ clean up all temporary directories after chrooting """
285     if imgmount and targettype == "img":
286         imgmount.cleanup()
287
288     if tmpdir:
289         shutil.rmtree(tmpdir, ignore_errors = True)
290
291     if tmpmnt:
292         shutil.rmtree(tmpmnt, ignore_errors = True)
293
294 def chroot(chrootdir, bindmounts = None, execute = "/bin/bash"):
295     """ chroot the chrootdir and execute the command """
296     def mychroot():
297         """ pre-execute function """
298         os.chroot(chrootdir)
299         os.chdir("/")
300
301     arch = ELF_arch(chrootdir)
302     if arch == "arm":
303         qemu_emulator = misc.setup_qemu_emulator(chrootdir, "arm")
304     else:
305         qemu_emulator = None
306
307     savefs_before_chroot(chrootdir, None)
308
309     try:
310         msger.info("Launching shell. Exit to continue.\n"
311                    "----------------------------------")
312         globalmounts = setup_chrootenv(chrootdir, bindmounts)
313         subprocess.call(execute, preexec_fn = mychroot, shell=True)
314
315     except OSError, err:
316         raise errors.CreatorError("chroot err: %s" % str(err))
317
318     finally:
319         cleanup_chrootenv(chrootdir, bindmounts, globalmounts)
320         if qemu_emulator:
321             os.unlink(chrootdir + qemu_emulator)