don't need to clean up extra mounts present
[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     # FIXME: need to clean up mounts?
240     #cleanup_mounts(chrootdir)
241
242     # release the lock
243     if chroot_lock:
244         chroot_lock.release()
245         chroot_lock = None
246
247     return None
248
249 #####################################################################
250 ### CHROOT STUFF
251 #####################################################################
252
253 def savefs_before_chroot(chrootdir, saveto = None):
254     """ backup chrootdir to another directory before chrooting in """
255     if configmgr.chroot['saveto']:
256         savefs = True
257         saveto = configmgr.chroot['saveto']
258         wrnmsg = "Can't save chroot fs for dir %s exists" % saveto
259         if saveto == chrootdir:
260             savefs = False
261             wrnmsg = "Dir %s is being used to chroot" % saveto
262         elif os.path.exists(saveto):
263             if msger.ask("Dir %s already exists, cleanup and continue?" %
264                          saveto):
265                 shutil.rmtree(saveto, ignore_errors = True)
266                 savefs = True
267             else:
268                 savefs = False
269
270         if savefs:
271             msger.info("Saving image to directory %s" % saveto)
272             fs_related.makedirs(os.path.dirname(os.path.abspath(saveto)))
273             runner.quiet("cp -af %s %s" % (chrootdir, saveto))
274             devs = ['dev/fd',
275                     'dev/stdin',
276                     'dev/stdout',
277                     'dev/stderr',
278                     'etc/mtab']
279             ignlst = [os.path.join(saveto, x) for x in devs]
280             map(os.unlink, filter(os.path.exists, ignlst))
281         else:
282             msger.warning(wrnmsg)
283
284 def cleanup_after_chroot(targettype, imgmount, tmpdir, tmpmnt):
285     """ clean up all temporary directories after chrooting """
286     if imgmount and targettype == "img":
287         imgmount.cleanup()
288
289     if tmpdir:
290         shutil.rmtree(tmpdir, ignore_errors = True)
291
292     if tmpmnt:
293         shutil.rmtree(tmpmnt, ignore_errors = True)
294
295 def chroot(chrootdir, bindmounts = None, execute = "/bin/bash"):
296     """ chroot the chrootdir and execute the command """
297     def mychroot():
298         """ pre-execute function """
299         os.chroot(chrootdir)
300         os.chdir("/")
301
302     arch = ELF_arch(chrootdir)
303     if arch == "arm":
304         qemu_emulator = misc.setup_qemu_emulator(chrootdir, "arm")
305     else:
306         qemu_emulator = None
307
308     savefs_before_chroot(chrootdir, None)
309
310     try:
311         msger.info("Launching shell. Exit to continue.\n"
312                    "----------------------------------")
313         globalmounts = setup_chrootenv(chrootdir, bindmounts)
314         subprocess.call(execute, preexec_fn = mychroot, shell=True)
315
316     except OSError, err:
317         raise errors.CreatorError("chroot err: %s" % str(err))
318
319     finally:
320         cleanup_chrootenv(chrootdir, bindmounts, globalmounts)
321         if qemu_emulator:
322             os.unlink(chrootdir + qemu_emulator)