refactor mic/chroot.py
[tools/mic.git] / mic / bootstrap.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 sys
21 import tempfile
22 import shutil
23 import subprocess
24 import rpm
25 from mic import msger
26 from mic.utils import errors, proxy, misc
27 from mic.utils.rpmmisc import readRpmHeader, RPMInstallCallback
28 from mic.chroot import cleanup_mounts, setup_chrootenv, cleanup_chrootenv
29
30 PATH_BOOTSTRAP = "/usr/sbin:/usr/bin:/sbin:/bin"
31
32 RPMTRANS_FLAGS = [
33                    rpm.RPMTRANS_FLAG_ALLFILES,
34                    rpm.RPMTRANS_FLAG_NOSCRIPTS,
35                    rpm.RPMTRANS_FLAG_NOTRIGGERS,
36                  ]
37
38 RPMVSF_FLAGS = [
39                  rpm._RPMVSF_NOSIGNATURES,
40                  rpm._RPMVSF_NODIGESTS
41                ]
42
43 RPMPROB_FLAGS = [
44                   rpm.RPMPROB_FILTER_OLDPACKAGE,
45                   rpm.RPMPROB_FILTER_REPLACEPKG,
46                   rpm.RPMPROB_FILTER_IGNOREARCH
47                 ]
48
49 class MiniBackend(object):
50     def __init__(self, rootdir, arch=None, repomd=None):
51         self._ts = None
52         self.rootdir = os.path.abspath(rootdir)
53         self.arch = arch
54         self.repomd = repomd
55         self.dlpkgs = []
56         self.localpkgs = {}
57         self.optionals = []
58         self.preins = {}
59         self.postins = {}
60         self.scriptlets = False
61
62     def __del__(self):
63         try:
64             del self.ts
65         except:
66             pass
67
68     def get_ts(self):
69         if not self._ts:
70             self._ts = rpm.TransactionSet(self.rootdir)
71             self._ts.setFlags(reduce(lambda x, y: x|y, RPMTRANS_FLAGS))
72             self._ts.setVSFlags(reduce(lambda x, y: x|y, RPMVSF_FLAGS))
73             self._ts.setProbFilter(reduce(lambda x, y: x|y, RPMPROB_FLAGS))
74
75         return self._ts
76
77     def del_ts(self):
78         if self._ts:
79             self._ts.closeDB()
80             self._ts = None
81
82     ts = property(fget = lambda self: self.get_ts(),
83                   fdel = lambda self: self.del_ts(),
84                   doc="TransactionSet object")
85
86     def selectPackage(self, pkg):
87         if not pkg in self.dlpkgs:
88             self.dlpkgs.append(pkg)
89
90     def runInstall(self):
91         # FIXME: check space
92         self.downloadPkgs()
93         self.installPkgs()
94
95         if not self.scriptlets:
96             return
97
98         for pkg in self.preins.keys():
99             prog, script = self.preins[pkg]
100             self.run_pkg_script(pkg, prog, script, '0')
101         for pkg in self.postins.keys():
102             prog, script = self.postins[pkg]
103             self.run_pkg_script(pkg, prog, script, '1')
104
105     def downloadPkgs(self):
106         nonexist = []
107         for pkg in self.dlpkgs:
108             localpth = misc.get_package(pkg, self.repomd, self.arch)
109             if localpth:
110                 self.localpkgs[pkg] = localpth
111             elif pkg in self.optionals:
112                 # skip optional rpm
113                 continue
114             else:
115                 # mark nonexist rpm
116                 nonexist.append(pkg)
117
118         if nonexist:
119             raise errors.BootstrapError("Can't get rpm binary: %s" %
120                                         ','.join(nonexist))
121
122     def installPkgs(self):
123         for pkg in self.localpkgs.keys():
124             rpmpath = self.localpkgs[pkg]
125
126             hdr = readRpmHeader(self.ts, rpmpath)
127
128             # save prein and postin scripts
129             self.preins[pkg] = (hdr['PREINPROG'], hdr['PREIN'])
130             self.postins[pkg] = (hdr['POSTINPROG'], hdr['POSTIN'])
131
132             # mark pkg as install
133             self.ts.addInstall(hdr, rpmpath, 'u')
134
135         # run transaction
136         self.ts.order()
137         cb = RPMInstallCallback(self.ts)
138         self.ts.run(cb.callback, '')
139
140     def run_pkg_script(self, pkg, prog, script, arg):
141         mychroot = lambda: os.chroot(self.rootdir)
142
143         if not script:
144             return
145
146         if prog == "<lua>":
147             prog = "/usr/bin/lua"
148
149         tmpdir = os.path.join(self.rootdir, "tmp")
150         if not os.path.exists(tmpdir):
151             os.makedirs(tmpdir)
152         tmpfd, tmpfp = tempfile.mkstemp(dir=tmpdir, prefix="%s.pre-" % pkg)
153         script = script.replace('\r', '')
154         os.write(tmpfd, script)
155         os.close(tmpfd)
156         os.chmod(tmpfp, 0700)
157
158         try:
159             script_fp = os.path.join('/tmp', os.path.basename(tmpfp))
160             subprocess.call([prog, script_fp, arg], preexec_fn=mychroot)
161         except (OSError, IOError), err:
162             msger.warning(str(err))
163         finally:
164             os.unlink(tmpfp)
165
166 class Bootstrap(object):
167     def __init__(self, rootdir, distro, arch=None):
168         self.rootdir = misc.mkdtemp(dir=rootdir, prefix=distro)
169         self.distro = distro
170         self.arch = arch
171         self.logfile = None
172         self.pkgslist = []
173         self.repomd = None
174
175     def __del__(self):
176         self.cleanup()
177
178     def get_rootdir(self):
179         if os.path.exists(self.rootdir):
180             shutil.rmtree(self.rootdir, ignore_errors=True)
181         os.makedirs(self.rootdir)
182         return self.rootdir
183
184     def dirsetup(self, rootdir=None):
185         _path = lambda pth: os.path.join(rootdir, pth.lstrip('/'))
186
187         if not rootdir:
188             rootdir = self.rootdir
189
190         try:
191             # make /tmp and /etc path
192             tmpdir = _path('/tmp')
193             if not os.path.exists(tmpdir):
194                 os.makedirs(tmpdir)
195             etcdir = _path('/etc')
196             if not os.path.exists(etcdir):
197                 os.makedirs(etcdir)
198
199             # touch distro file
200             tzdist = _path('/etc/%s-release' % self.distro)
201             if not os.path.exists(tzdist):
202                 with open(tzdist, 'w') as wf:
203                     wf.write("bootstrap")
204         except:
205             pass
206
207     def create(self, repomd, pkglist, optlist=()):
208         try:
209             pkgmgr = MiniBackend(self.get_rootdir())
210             pkgmgr.arch = self.arch
211             pkgmgr.repomd = repomd
212             pkgmgr.optionals = list(optlist)
213             map(pkgmgr.selectPackage, pkglist + list(optlist))
214             pkgmgr.runInstall()
215         except (OSError, IOError, errors.CreatorError), err:
216             raise errors.BootstrapError("%s" % err)
217
218     def run(self, cmd, chdir, rootdir=None, bindmounts=None):
219         def mychroot():
220             os.chroot(rootdir)
221             os.chdir(chdir)
222
223         def sync_timesetting(rootdir):
224             try:
225                 # sync time and zone info to bootstrap
226                 if os.path.exists(rootdir + "/etc/localtime"):
227                     os.unlink(rootdir + "/etc/localtime")
228                 shutil.copyfile("/etc/localtime", rootdir + "/etc/localtime")
229             except:
230                 pass
231
232         def sync_passwdfile(rootdir):
233             try:
234                 # sync passwd file to bootstrap, saving the user info
235                 if os.path.exists(rootdir + "/etc/passwd"):
236                     os.unlink(rootdir + "/etc/passwd")
237                 shutil.copyfile("/etc/passwd", rootdir + "/etc/passwd")
238             except:
239                 pass
240
241         if not rootdir:
242             rootdir = self.rootdir
243
244         if isinstance(cmd, list):
245             shell = False
246         else:
247             shell = True
248
249         env = os.environ
250         env['PATH'] = "%s:%s" % (PATH_BOOTSTRAP, env['PATH'])
251
252         retcode = 0
253         gloablmounts = None
254         try:
255             proxy.set_proxy_environ()
256             gloablmounts = setup_chrootenv(rootdir, bindmounts)
257             sync_timesetting(rootdir)
258             sync_passwdfile(rootdir)
259             retcode = subprocess.call(cmd, preexec_fn=mychroot, env=env, shell=shell)
260         except (OSError, IOError):
261             # add additional information to original exception
262             value, tb = sys.exc_info()[1:]
263             value = '%s: %s' % (value, ' '.join(cmd))
264             raise RuntimeError, value, tb
265         finally:
266             if self.logfile and os.path.isfile(self.logfile):
267                 msger.log(file(self.logfile).read())
268             cleanup_chrootenv(rootdir, bindmounts, gloablmounts)
269             proxy.unset_proxy_environ()
270         return retcode
271
272     def cleanup(self):
273         try:
274             # clean mounts
275             cleanup_mounts(self.rootdir)
276             # remove rootdir
277             shutil.rmtree(self.rootdir, ignore_errors=True)
278         except:
279             pass