fix chroot_lock reference
[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         bmount = fs_related.BindChrootMount(pair[0], chrootdir, pair[1])
121         chroot_bindmounts.append(bmount)
122
123     return chroot_bindmounts
124
125 #####################################################################
126 ### SETUP CHROOT ENVIRONMENT
127 #####################################################################
128
129 def bind_mount(chrootmounts):
130     """ perform bind mounting """
131     for mnt in chrootmounts:
132         msger.verbose("bind_mount: %s -> %s" % (mnt.src, mnt.dest))
133         mnt.mount()
134
135 def setup_resolv(chrootdir):
136     """ resolve network """
137     try:
138         shutil.copyfile("/etc/resolv.conf", chrootdir + "/etc/resolv.conf")
139     except (OSError, IOError):
140         pass
141
142 def setup_mtab(chrootdir):
143     """ adjust mount table """
144     mtab = "/etc/mtab"
145     dstmtab = chrootdir + mtab
146     if not os.path.islink(dstmtab):
147         shutil.copyfile(mtab, dstmtab)
148
149 def setup_chrootenv(chrootdir, bindmounts = None):
150     """ setup chroot environment """
151     global chroot_lock
152
153     # acquire the lock
154     if not chroot_lock:
155         lockpath = os.path.join(chrootdir, '.chroot.lock')
156         chroot_lock = lock.SimpleLockfile(lockpath)
157     chroot_lock.acquire()
158     # bind mounting
159     bind_mount(get_bindmounts(chrootdir, bindmounts))
160     # setup resolv.conf
161     setup_resolv(chrootdir)
162     # update /etc/mtab
163     setup_mtab(chrootdir)
164
165     return None
166
167 ######################################################################
168 ### CLEANUP CHROOT ENVIRONMENT
169 ######################################################################
170
171 def bind_unmount(chrootmounts):
172     """ perform bind unmounting """
173     for mnt in reversed(chrootmounts):
174         msger.verbose("bind_unmount: %s -> %s" % (mnt.src, mnt.dest))
175         mnt.unmount()
176
177 def cleanup_resolv(chrootdir):
178     """ clear resolv.conf """
179     try:
180         fdes = open(chrootdir + "/etc/resolv.conf", "w")
181         fdes.truncate(0)
182         fdes.close()
183     except (OSError, IOError):
184         pass
185
186 def kill_proc_inchroot(chrootdir):
187     """ kill all processes running inside chrootdir """
188     import glob
189     for fpath in glob.glob("/proc/*/root"):
190         try:
191             if os.readlink(fpath) == chrootdir:
192                 pid = int(fpath.split("/")[2])
193                 os.kill(pid, 9)
194         except (OSError, ValueError):
195             pass
196
197 def cleanup_mtab(chrootdir):
198     """ remove mtab file """
199     if os.path.exists(chrootdir + "/etc/mtab"):
200         os.unlink(chrootdir + "/etc/mtab")
201
202 def cleanup_mounts(chrootdir):
203     """ clean up all mount entries owned by chrootdir """
204     umountcmd = misc.find_binary_path("umount")
205     mounts = open('/proc/mounts').readlines()
206     for line in reversed(mounts):
207         if chrootdir not in line:
208             continue
209
210         point = line.split()[1]
211
212         # '/' to avoid common name prefix
213         if chrootdir == point or point.startswith(chrootdir + '/'):
214             args = [ umountcmd, "-l", point ]
215             ret = runner.quiet(args)
216             if ret != 0:
217                 msger.warning("failed to unmount %s" % point)
218             if os.path.isdir(point) and len(os.listdir(point)) == 0:
219                 shutil.rmtree(point)
220             else:
221                 msger.warning("%s is not directory or is not empty" % point)
222
223 def cleanup_chrootenv(chrootdir, bindmounts=None, globalmounts=()):
224     """ clean up chroot environment """
225     global chroot_lock
226
227     # kill processes
228     kill_proc_inchroot(chrootdir)
229     # clean mtab
230     cleanup_mtab(chrootdir)
231     # clean resolv.conf
232     cleanup_resolv(chrootdir)
233     # bind umounting
234     bind_unmount(get_bindmounts(chrootdir, bindmounts))
235     # clean up mounts
236     cleanup_mounts(chrootdir)
237     # release the lock
238     if chroot_lock:
239         chroot_lock.release()
240         chroot_lock = None
241
242     return None
243
244 #####################################################################
245 ### CHROOT STUFF
246 #####################################################################
247
248 def savefs_before_chroot(chrootdir, saveto = None):
249     """ backup chrootdir to another directory before chrooting in """
250     if configmgr.chroot['saveto']:
251         savefs = True
252         saveto = configmgr.chroot['saveto']
253         wrnmsg = "Can't save chroot fs for dir %s exists" % saveto
254         if saveto == chrootdir:
255             savefs = False
256             wrnmsg = "Dir %s is being used to chroot" % saveto
257         elif os.path.exists(saveto):
258             if msger.ask("Dir %s already exists, cleanup and continue?" %
259                          saveto):
260                 shutil.rmtree(saveto, ignore_errors = True)
261                 savefs = True
262             else:
263                 savefs = False
264
265         if savefs:
266             msger.info("Saving image to directory %s" % saveto)
267             fs_related.makedirs(os.path.dirname(os.path.abspath(saveto)))
268             runner.quiet("cp -af %s %s" % (chrootdir, saveto))
269             devs = ['dev/fd',
270                     'dev/stdin',
271                     'dev/stdout',
272                     'dev/stderr',
273                     'etc/mtab']
274             ignlst = [os.path.join(saveto, x) for x in devs]
275             map(os.unlink, filter(os.path.exists, ignlst))
276         else:
277             msger.warning(wrnmsg)
278
279 def cleanup_after_chroot(targettype, imgmount, tmpdir, tmpmnt):
280     """ clean up all temporary directories after chrooting """
281     if imgmount and targettype == "img":
282         imgmount.cleanup()
283
284     if tmpdir:
285         shutil.rmtree(tmpdir, ignore_errors = True)
286
287     if tmpmnt:
288         shutil.rmtree(tmpmnt, ignore_errors = True)
289
290 def chroot(chrootdir, bindmounts = None, execute = "/bin/bash"):
291     """ chroot the chrootdir and execute the command """
292     def mychroot():
293         """ pre-execute function """
294         os.chroot(chrootdir)
295         os.chdir("/")
296
297     arch = ELF_arch(chrootdir)
298     if arch == "arm":
299         qemu_emulator = misc.setup_qemu_emulator(chrootdir, "arm")
300     else:
301         qemu_emulator = None
302
303     savefs_before_chroot(chrootdir, None)
304
305     try:
306         msger.info("Launching shell. Exit to continue.\n"
307                    "----------------------------------")
308         globalmounts = setup_chrootenv(chrootdir, bindmounts)
309         subprocess.call(execute, preexec_fn = mychroot, shell=True)
310
311     except OSError, err:
312         raise errors.CreatorError("chroot err: %s" % str(err))
313
314     finally:
315         cleanup_chrootenv(chrootdir, bindmounts, globalmounts)
316         if qemu_emulator:
317             os.unlink(chrootdir + qemu_emulator)