c3e847187a102d5426af503ae51640bdba0da7a2
[platform/upstream/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 import glob
26
27 from mic import msger
28 from mic.utils import errors, proxy, misc
29 from mic.utils.rpmmisc import readRpmHeader, RPMInstallCallback
30 from mic.chroot import cleanup_mounts, setup_chrootenv, cleanup_chrootenv
31 from mic.conf import configmgr
32
33 PATH_BOOTSTRAP = "/usr/sbin:/usr/bin:/sbin:/bin"
34
35 RPMTRANS_FLAGS = [
36                    rpm.RPMTRANS_FLAG_ALLFILES,
37                    rpm.RPMTRANS_FLAG_NOSCRIPTS,
38                    rpm.RPMTRANS_FLAG_NOTRIGGERS,
39                  ]
40
41 RPMVSF_FLAGS = [
42                  rpm._RPMVSF_NOSIGNATURES,
43                  rpm._RPMVSF_NODIGESTS
44                ]
45
46 RPMPROB_FLAGS = [
47                   rpm.RPMPROB_FILTER_OLDPACKAGE,
48                   rpm.RPMPROB_FILTER_REPLACEPKG,
49                   rpm.RPMPROB_FILTER_IGNOREARCH
50                 ]
51
52 class MiniBackend(object):
53     def __init__(self, rootdir, arch=None, repomd=None):
54         self._ts = None
55         self.rootdir = os.path.abspath(rootdir)
56         self.arch = arch
57         self.repomd = repomd
58         self.dlpkgs = []
59         self.localpkgs = {}
60         self.optionals = []
61         self.preins = {}
62         self.postins = {}
63         self.scriptlets = False
64
65     def __del__(self):
66         try:
67             del self.ts
68         except:
69             pass
70
71     def get_ts(self):
72         if not self._ts:
73             self._ts = rpm.TransactionSet(self.rootdir)
74             self._ts.setFlags(reduce(lambda x, y: x|y, RPMTRANS_FLAGS))
75             self._ts.setVSFlags(reduce(lambda x, y: x|y, RPMVSF_FLAGS))
76             self._ts.setProbFilter(reduce(lambda x, y: x|y, RPMPROB_FLAGS))
77
78         return self._ts
79
80     def del_ts(self):
81         if self._ts:
82             self._ts.closeDB()
83             self._ts = None
84
85     ts = property(fget = lambda self: self.get_ts(),
86                   fdel = lambda self: self.del_ts(),
87                   doc="TransactionSet object")
88
89     def selectPackage(self, pkg):
90         if not pkg in self.dlpkgs:
91             self.dlpkgs.append(pkg)
92
93     def _get_local_packages(self, pkg):
94         """Return local mic-bootstrap rpm path."""
95         cropts = configmgr.create
96         if cropts['local_pkgs_path']:
97             if os.path.isdir(cropts['local_pkgs_path']):
98                 pkglist = glob.glob(
99                            os.path.join(cropts['local_pkgs_path'], pkg + '*.rpm'))
100                 if len(pkglist) > 1:
101                     raise errors.BootstrapError("Many %s packages in folder, put only one %s package in it." % (pkg, pkg))
102                 elif len(pkglist) == 1:
103                     return ''.join(pkglist)
104             elif os.path.splitext(cropts['local_pkgs_path'])[-1] == '.rpm':
105                 if cropts['local_pkgs_path'].find(pkg) >= 0:
106                     return cropts['local_pkgs_path']
107         return None
108     def runInstall(self):
109         # FIXME: check space
110         self.downloadPkgs()
111         self.installPkgs()
112
113         if not self.scriptlets:
114             return
115
116         for pkg in self.preins.keys():
117             prog, script = self.preins[pkg]
118             self.run_pkg_script(pkg, prog, script, '0')
119         for pkg in self.postins.keys():
120             prog, script = self.postins[pkg]
121             self.run_pkg_script(pkg, prog, script, '1')
122
123     def downloadPkgs(self):
124         nonexist = []
125         for pkg in self.dlpkgs:
126             localpth = self._get_local_packages(pkg)
127             if localpth is None:
128                 localpth = misc.get_package(pkg, self.repomd, self.arch)
129             if localpth:
130                 self.localpkgs[pkg] = localpth
131             elif pkg in self.optionals:
132                 # skip optional rpm
133                 continue
134             else:
135                 # mark nonexist rpm
136                 nonexist.append(pkg)
137
138         if nonexist:
139             raise errors.BootstrapError("Can't get rpm binary: %s" %
140                                         ','.join(nonexist))
141
142     def installPkgs(self):
143         for pkg in self.localpkgs.keys():
144             rpmpath = self.localpkgs[pkg]
145
146             hdr = readRpmHeader(self.ts, rpmpath)
147
148             # save prein and postin scripts
149             self.preins[pkg] = (hdr['PREINPROG'], hdr['PREIN'])
150             self.postins[pkg] = (hdr['POSTINPROG'], hdr['POSTIN'])
151
152             # mark pkg as install
153             self.ts.addInstall(hdr, rpmpath, 'u')
154
155         # run transaction
156         self.ts.order()
157         cb = RPMInstallCallback(self.ts)
158         errs = self.ts.run(cb.callback, '')
159
160         # ts.run() exit codes are, hmm, "creative": None means all ok, empty 
161         # list means some errors happened in the transaction and non-empty 
162         # list that there were errors preventing the ts from starting...
163         if errs is not None:
164              raise errors.BootstrapError("Transaction couldn't start: %s" % '\n'.join(errs))
165
166     def run_pkg_script(self, pkg, prog, script, arg):
167         mychroot = lambda: os.chroot(self.rootdir)
168
169         if not script:
170             return
171
172         if prog == "<lua>":
173             prog = "/usr/bin/lua"
174
175         tmpdir = os.path.join(self.rootdir, "tmp")
176         if not os.path.exists(tmpdir):
177             os.makedirs(tmpdir)
178         tmpfd, tmpfp = tempfile.mkstemp(dir=tmpdir, prefix="%s.pre-" % pkg)
179         script = script.replace('\r', '')
180         os.write(tmpfd, script)
181         os.close(tmpfd)
182         os.chmod(tmpfp, 0700)
183
184         try:
185             script_fp = os.path.join('/tmp', os.path.basename(tmpfp))
186             subprocess.call([prog, script_fp, arg], preexec_fn=mychroot)
187         except (OSError, IOError), err:
188             msger.warning(str(err))
189         finally:
190             os.unlink(tmpfp)
191
192 class Bootstrap(object):
193     def __init__(self, rootdir, distro, arch=None):
194         self.rootdir = misc.mkdtemp(dir=rootdir, prefix=distro)
195         self.distro = distro
196         self.arch = arch
197         self.logfile = None
198         self.pkgslist = []
199         self.repomd = None
200
201     def __del__(self):
202         self.cleanup()
203
204     def get_rootdir(self):
205         if os.path.exists(self.rootdir):
206             shutil.rmtree(self.rootdir, ignore_errors=True)
207         os.makedirs(self.rootdir)
208         return self.rootdir
209
210     def dirsetup(self, rootdir=None):
211         _path = lambda pth: os.path.join(rootdir, pth.lstrip('/'))
212
213         if not rootdir:
214             rootdir = self.rootdir
215
216         try:
217             # make /tmp and /etc path
218             tmpdir = _path('/tmp')
219             if not os.path.exists(tmpdir):
220                 os.makedirs(tmpdir)
221             etcdir = _path('/etc')
222             if not os.path.exists(etcdir):
223                 os.makedirs(etcdir)
224
225             # touch distro file
226             tzdist = _path('/etc/%s-release' % self.distro)
227             if not os.path.exists(tzdist):
228                 with open(tzdist, 'w') as wf:
229                     wf.write("bootstrap")
230         except:
231             pass
232
233     def create(self, repomd, pkglist, optlist=()):
234         try:
235             pkgmgr = MiniBackend(self.get_rootdir())
236             pkgmgr.arch = self.arch
237             pkgmgr.repomd = repomd
238             pkgmgr.optionals = list(optlist)
239             map(pkgmgr.selectPackage, pkglist + list(optlist))
240             pkgmgr.runInstall()
241         except (OSError, IOError, errors.CreatorError), err:
242             raise errors.BootstrapError("%s" % err)
243
244     def run(self, cmd, chdir, rootdir=None, bindmounts=None):
245         def mychroot():
246             os.chroot(rootdir)
247             os.chdir(chdir)
248
249         def sync_timesetting(rootdir):
250             try:
251                 # sync time and zone info to bootstrap
252                 if os.path.exists(rootdir + "/etc/localtime"):
253                     os.unlink(rootdir + "/etc/localtime")
254                 shutil.copyfile("/etc/localtime", rootdir + "/etc/localtime")
255             except:
256                 pass
257
258         def sync_passwdfile(rootdir):
259             try:
260                 # sync passwd file to bootstrap, saving the user info
261                 if os.path.exists(rootdir + "/etc/passwd"):
262                     os.unlink(rootdir + "/etc/passwd")
263                 shutil.copyfile("/etc/passwd", rootdir + "/etc/passwd")
264             except:
265                 pass
266
267         def sync_hostfile(rootdir):
268             try:
269                 # sync host info to bootstrap
270                 if os.path.exists(rootdir + "/etc/hosts"):
271                     os.unlink(rootdir + "/etc/hosts")
272                 shutil.copyfile("/etc/hosts", rootdir + "/etc/hosts")
273             except:
274                 pass
275
276         def sync_signfile(rootdir,homedir):
277             if os.path.exists(homedir + "/.sign"):
278                 signfile = rootdir + homedir + "/.sign"
279                 try:
280                     # sync sign info to bootstrap
281                     if os.path.exists(signfile):
282                         if os.path.isdir(signfile):
283                             shutil.rmtree(signfile)
284                         else:
285                             os.unlink(signfile)
286                     try:
287                         shutil.copytree(homedir + "/.sign", signfile)
288                     except OSError as exc:
289                         if exc.errno == errno.ENOTDIR:
290                             shutil.copy(homedir + "/.sign", signfile)
291                         else: raise
292                 except:
293                     pass
294
295         if not rootdir:
296             rootdir = self.rootdir
297
298         if isinstance(cmd, list):
299             shell = False
300         else:
301             shell = True
302
303         env = os.environ
304         env['PATH'] = "%s:%s" % (PATH_BOOTSTRAP, env['PATH'])
305         cropts = configmgr.create
306         if cropts["ks"]:
307             lang = cropts["ks"].handler.lang.lang
308             env['LANG'] = lang
309             env['LC_ALL'] = lang
310
311         retcode = 0
312         gloablmounts = None
313         try:
314             proxy.set_proxy_environ()
315             gloablmounts = setup_chrootenv(rootdir, bindmounts)
316             sync_timesetting(rootdir)
317             sync_passwdfile(rootdir)
318             sync_hostfile(rootdir)
319             env['SUDO_USER'] = 'root'
320             if 'SUDO_USER' in env:
321                 sync_signfile(rootdir, os.path.expanduser('~' + env['SUDO_USER']))
322             else:
323                 sync_signfile(rootdir, os.path.expanduser('~'))
324             retcode = subprocess.call(cmd, preexec_fn=mychroot, env=env, shell=shell)
325         except (OSError, IOError):
326             # add additional information to original exception
327             value, tb = sys.exc_info()[1:]
328             value = '%s: %s' % (value, ' '.join(cmd))
329             raise RuntimeError, value, tb
330         finally:
331             #if self.logfile and os.path.isfile(self.logfile):
332             #    msger.log(file(self.logfile).read())
333             cleanup_chrootenv(rootdir, bindmounts, gloablmounts)
334             proxy.unset_proxy_environ()
335         return retcode
336
337     def cleanup(self):
338         try:
339             # clean mounts
340             cleanup_mounts(self.rootdir)
341             # remove rootdir
342             shutil.rmtree(self.rootdir, ignore_errors=True)
343         except:
344             pass