]> git.ipfire.org Git - pakfire.git/blame - src/pakfire/actions.py
Fix execution of scriptlets in a dirty environment
[pakfire.git] / src / pakfire / actions.py
CommitLineData
1e80d5d7 1#!/usr/bin/python
b792d887
MT
2###############################################################################
3# #
4# Pakfire - The IPFire package management system #
5# Copyright (C) 2011 Pakfire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
1e80d5d7 21
94438733 22import os
536bec81 23import sys
1e80d5d7
MT
24
25import packages
64ffcd37 26import shell
94438733 27import util
1e80d5d7 28
8b6bc023
MT
29import logging
30log = logging.getLogger("pakfire")
31
1e80d5d7
MT
32from constants import *
33from i18n import _
34
35class Action(object):
d37a555d 36 def __init__(self, pakfire, pkg_solv, pkg_bin=None):
1e80d5d7 37 self.pakfire = pakfire
1e80d5d7 38
d37a555d
MT
39 self.pkg_solv = pkg_solv
40 if pkg_bin:
41 self.pkg_bin = pkg_bin
1e80d5d7 42
94438733
MT
43 self.init()
44
aa9f2645
MT
45 def __cmp__(self, other):
46 return cmp(self.pkg, other.pkg)
47
1e80d5d7
MT
48 def __repr__(self):
49 return "<%s %s>" % (self.__class__.__name__, self.pkg.friendly_name)
50
94438733
MT
51 def init(self):
52 # A function to run additional initialization.
53 pass
54
862bea4d
MT
55 def check(self, filelist):
56 # This is just a dummy test that does nothing at all.
57 return filelist
58
81bb7698 59 def verify(self):
d37a555d 60 assert self.pkg, "No package! %s" % self.pkg
0f8d6745
MT
61 assert self.pkg.repo, "Package has no repository? %s" % self.pkg
62
63 # Local packages need no verification.
64 if self.pkg.repo.local:
65 return
66
81bb7698
MT
67 # Check if there are any signatures at all.
68 if not self.pkg.signatures:
69 raise SignatureError, _("%s has got no signatures") % self.pkg.friendly_name
70
71 # Run the verification process and save the result.
72 sigs = self.pkg.verify()
73
74 if not sigs:
75 raise SignatureError, _("%s has got no valid signatures") % self.pkg.friendly_name
76
1e80d5d7 77 @property
d37a555d
MT
78 def pkg(self):
79 """
80 Return the best version of the package we can use.
81 """
82 return self.pkg_bin or self.pkg_solv
1e80d5d7 83
d37a555d
MT
84 def get_binary_package(self):
85 """
86 Tries to find the binary version of the package in the local cache.
87 """
88 return self.pkg_solv.get_from_cache()
89
90 def _get_pkg_bin(self):
91 if not hasattr(self, "_pkg_bin"):
92 self._pkg_bin = self.get_binary_package()
1e80d5d7 93
d37a555d
MT
94 return self._pkg_bin
95
96 def _set_pkg_bin(self, pkg):
97 if pkg and not self.pkg_solv.uuid == pkg.uuid:
98 raise RuntimeError, "Not the same package: %s != %s" % (self.pkg_solv, pkg)
99
100 self._pkg_bin = pkg
101
102 pkg_bin = property(_get_pkg_bin, _set_pkg_bin)
1e80d5d7
MT
103
104 def run(self):
105 raise NotImplementedError
106
107 @property
108 def local(self):
109 """
110 Reference to local repository.
111 """
112 return self.pakfire.repos.local
113
c5b58985
MT
114 def get_logger_name(self):
115 return "pakfire.action.%s" % self.pkg.friendly_name
116
117 def get_logger(self):
118 logger_name = self.get_logger_name()
119
120 logger = logging.getLogger(logger_name)
121 logger.setLevel(logging.INFO)
122
123 # Propagate everything to upstream logger.
124 logger.propagate = True
125
126 return logger
127
64ffcd37 128 def execute(self, command, **kwargs):
d335c40e
MT
129 # If we are running in /, we do not need to chroot there.
130 chroot_path = None
131 if not self.pakfire.path == "/":
132 chroot_path = self.pakfire.path
133
134 # Find suitable cwd.
135 cwd = "/"
136 for i in ("tmp", "root"):
254f5e56
MT
137 if chroot_path:
138 _cwd = os.path.join(chroot_path, i)
139 else:
140 _cwd = i
141
d335c40e
MT
142 if os.path.exists(_cwd):
143 cwd = _cwd
144 break
145
146 args = {
147 "cwd" : cwd,
1dd38b7b 148 "env" : MINIMAL_ENVIRONMENT,
c5b58985 149 "logger" : self.get_logger(),
d335c40e
MT
150 "personality" : self.pakfire.distro.personality,
151 "shell" : False,
152 "timeout" : SCRIPTLET_TIMEOUT,
153 }
154
155 # Overwrite by args that were passed.
156 args.update(kwargs)
157
158 # You can never overwrite chrootPath.
64ffcd37 159 args["chroot_path"] = chroot_path
d335c40e 160
64ffcd37
MT
161 # Execute command.
162 shellenv = shell.ShellExecuteEnvironment(command, **args)
163 shellenv.execute()
d335c40e 164
1e80d5d7
MT
165
166class ActionScript(Action):
d767668b 167 type = "script"
c07a3ca7 168 script_action = None
d767668b 169
94438733 170 def init(self):
bd629777
MT
171 self._scriptlet = None
172
c5b58985
MT
173 def get_logger_name(self):
174 logger_name = Action.get_logger_name(self)
175
176 return "%s.%s" % (logger_name, self.script_action or "unknown")
177
bd629777
MT
178 @property
179 def scriptlet(self):
180 """
181 Load the scriplet.
182 """
183 if self._scriptlet is None:
184 self._scriptlet = self.pkg.get_scriptlet(self.script_action)
185
186 return self._scriptlet
94438733 187
536bec81
MT
188 def get_lang(self):
189 if not self.scriptlet:
190 return
191
192 interp = None
193
194 for line in self.scriptlet.splitlines():
195 if line.startswith("#!/"):
196 interp = "exec"
197 break
198
199 elif line.startswith("#<lang: "):
200 interp = line[8:].replace(">", "")
201 break
202
203 return interp
204
94438733
MT
205 @property
206 def interpreter(self):
207 """
208 Get the interpreter of this scriptlet.
209 """
c07a3ca7 210 return util.scriptlet_interpreter(self.scriptlet)
94438733
MT
211
212 @property
213 def args(self):
c07a3ca7 214 return []
94438733 215
1e80d5d7 216 def run(self):
94438733
MT
217 # Exit immediately, if the scriptlet is empty.
218 if not self.scriptlet:
219 return
220
221 # Actually run the scriplet.
8b6bc023 222 log.debug("Running scriptlet %s" % self)
94438733 223
536bec81
MT
224 # Check of what kind the scriptlet is and run the
225 # corresponding handler.
226 lang = self.get_lang()
227
228 if lang == "exec":
229 self.run_exec()
230
231 elif lang == "python":
232 self.run_python()
233
234 else:
235 raise ActionError, _("Could not handle scriptlet of unknown type. Skipping.")
236
237 def run_exec(self):
c5b58985 238 log.debug(_("Executing scriptlet..."))
536bec81 239
94438733 240 # Check if the interpreter does exist and is executable.
c07a3ca7
MT
241 if self.interpreter:
242 interpreter = "%s/%s" % (self.pakfire.path, self.interpreter)
243 if not os.path.exists(interpreter):
244 raise ActionError, _("Cannot run scriptlet because no interpreter is available: %s" \
245 % self.interpreter)
94438733 246
c07a3ca7
MT
247 if not os.access(interpreter, os.X_OK):
248 raise ActionError, _("Cannot run scriptlet because the interpreter is not executable: %s" \
249 % self.interpreter)
94438733
MT
250
251 # Create a name for the temporary script file.
252 script_file_chroot = os.path.join("/", LOCAL_TMP_PATH,
253 "scriptlet_%s" % util.random_string(10))
254 script_file = os.path.join(self.pakfire.path, script_file_chroot[1:])
254f5e56 255 assert script_file.startswith(self.pakfire.path)
94438733
MT
256
257 # Create script directory, if it does not exist.
258 script_dir = os.path.dirname(script_file)
259 if not os.path.exists(script_dir):
260 os.makedirs(script_dir)
261
262 # Write the scriptlet to a file that we can execute it.
263 try:
264 f = open(script_file, "wb")
265 f.write(self.scriptlet)
266 f.close()
267
268 # The file is only accessable by root.
269 os.chmod(script_file, 700)
270 except:
271 # Remove the file if an error occurs.
272 try:
273 os.unlink(script_file)
274 except OSError:
275 pass
276
277 # XXX catch errors and return a beautiful message to the user
278 raise
279
c07a3ca7
MT
280 # Generate the script command.
281 command = [script_file_chroot] + self.args
94438733 282
94438733 283 try:
64ffcd37 284 self.execute(command)
94438733 285
64ffcd37 286 except ShellEnvironmentError, e:
94438733
MT
287 raise ActionError, _("The scriptlet returned an error:\n%s" % e)
288
289 except commandTimeoutExpired:
290 raise ActionError, _("The scriptlet ran more than %s seconds and was killed." \
291 % SCRIPTLET_TIMEOUT)
292
1361a82b
MT
293 except Exception, e:
294 raise ActionError, _("The scriptlet returned with an unhandled error:\n%s" % e)
295
94438733
MT
296 finally:
297 # Remove the script file.
298 try:
299 os.unlink(script_file)
300 except OSError:
8b6bc023 301 log.debug("Could not remove scriptlet file: %s" % script_file)
1e80d5d7 302
536bec81
MT
303 def run_python(self):
304 # This functions creates a fork with then chroots into the
305 # pakfire root if necessary and then compiles the given scriptlet
306 # code and runs it.
307
308 log.debug(_("Executing python scriptlet..."))
309
310 # Create fork.
311 pid = os.fork()
312
313 if not pid:
314 # child code
315
316 # The child chroots into the pakfire path.
317 if not self.pakfire.path == "/":
318 os.chroot(self.pakfire.path)
319
320 # Create a clean global environment, where only
321 # builtin functions are available and the os and sys modules.
322 _globals = {
323 "os" : os,
324 "sys" : sys,
325 }
326
327 # Compile the scriptlet and execute it.
328 try:
329 obj = compile(self.scriptlet, "<string>", "exec")
330 eval(obj, _globals, {})
331
332 except Exception, e:
333 print _("Exception occured: %s") % e
334 os._exit(1)
335
336 # End the child process without cleaning up.
337 os._exit(0)
338
339 else:
340 # parent code
341
342 # Wait until the child process has finished.
343 os.waitpid(pid, 0)
344
1e80d5d7
MT
345
346class ActionScriptPreIn(ActionScript):
c07a3ca7 347 script_action = "prein"
1e80d5d7
MT
348
349
350class ActionScriptPostIn(ActionScript):
c07a3ca7 351 script_action = "postin"
1e80d5d7
MT
352
353
354class ActionScriptPreUn(ActionScript):
c07a3ca7 355 script_action = "preun"
1e80d5d7
MT
356
357
358class ActionScriptPostUn(ActionScript):
c07a3ca7 359 script_action = "postun"
1e80d5d7
MT
360
361
d767668b 362class ActionScriptPreUp(ActionScript):
c07a3ca7 363 script_action = "preup"
d767668b
MT
364
365
366class ActionScriptPostUp(ActionScript):
c07a3ca7 367 script_action = "postup"
d767668b
MT
368
369
f943de15
MT
370class ActionScriptPreTrans(ActionScript):
371 pass
372
373
374class ActionScriptPreTransIn(ActionScriptPreTrans):
375 script_action = "pretransin"
376
377
378class ActionScriptPreTransUn(ActionScriptPreTrans):
379 script_action = "pretransun"
380
381
382class ActionScriptPreTransUp(ActionScriptPreTrans):
383 script_action = "pretransup"
384
385
d767668b
MT
386class ActionScriptPostTrans(ActionScript):
387 pass
388
389
390class ActionScriptPostTransIn(ActionScriptPostTrans):
c07a3ca7 391 script_action = "posttransin"
d767668b
MT
392
393
394class ActionScriptPostTransUn(ActionScriptPostTrans):
c07a3ca7 395 script_action = "posttransun"
d767668b
MT
396
397
398class ActionScriptPostTransUp(ActionScriptPostTrans):
c07a3ca7 399 script_action = "posttransup"
d767668b
MT
400
401
1e80d5d7
MT
402class ActionInstall(Action):
403 type = "install"
404
862bea4d 405 def check(self, check):
8b6bc023 406 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
862bea4d
MT
407
408 # Check if this package can be installed.
409 check.install(self.pkg)
410
1e80d5d7 411 def run(self):
e871a081
MT
412 # Add package to the database.
413 self.local.add_package(self.pkg)
414
d37a555d
MT
415 if isinstance(self, ActionReinstall):
416 msg = _("Reinstalling")
417 elif isinstance(self, ActionUpdate):
418 msg = _("Updating")
419 elif isinstance(self, ActionDowngrade):
420 msg = _("Downgrading")
421 else:
422 msg = _("Installing")
423
424 self.pkg.extract(msg, prefix=self.pakfire.path)
1e80d5d7 425
d335c40e
MT
426 # Check if shared objects were extracted. If this is the case, we need
427 # to run ldconfig.
428 ldconfig_needed = False
429 for file in self.pkg.filelist:
430 if ".so." in file.name:
431 ldconfig_needed = True
432 break
433
434 if "etc/ld.so.conf" in file.name:
435 ldconfig_needed = True
436 break
437
438 if ldconfig_needed:
439 # Check if ldconfig is present.
440 ldconfig = os.path.join(self.pakfire.path, LDCONFIG[1:])
441
442 if os.path.exists(ldconfig) and os.access(ldconfig, os.X_OK):
64ffcd37 443 self.execute(LDCONFIG)
d335c40e
MT
444
445 else:
8b6bc023 446 log.debug("ldconfig is not present or not executable.")
d335c40e 447
1e80d5d7 448
d37a555d 449class ActionUpdate(ActionInstall):
1e80d5d7
MT
450 type = "upgrade"
451
862bea4d 452 def check(self, check):
8b6bc023 453 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
862bea4d
MT
454
455 # Check if this package can be updated.
456 check.update(self.pkg)
457
1e80d5d7 458
6ee3d6b9 459class ActionRemove(Action):
1e80d5d7
MT
460 type = "erase"
461
862bea4d 462 def check(self, check):
8b6bc023 463 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
862bea4d
MT
464
465 # Check if this package can be removed.
466 check.remove(self.pkg)
467
6ee3d6b9 468 def run(self):
d37a555d
MT
469 if isinstance(self, ActionCleanup):
470 msg = _("Cleanup")
471 else:
472 msg = _("Removing")
473
474 self.pkg.cleanup(msg, prefix=self.pakfire.path)
1e80d5d7 475
e871a081 476 # Remove package from the database.
d37a555d 477 self.local.rem_package(self.pkg)
e871a081
MT
478
479
d37a555d 480class ActionCleanup(ActionRemove):
e871a081
MT
481 type = "ignore"
482
862bea4d 483 def check(self, check):
8b6bc023 484 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
862bea4d
MT
485
486 # Check if this package can be removed.
487 check.cleanup(self.pkg)
488
1e80d5d7 489
d37a555d 490class ActionReinstall(ActionInstall):
1e80d5d7
MT
491 type = "reinstall"
492
1e80d5d7 493
d37a555d 494class ActionDowngrade(ActionInstall):
1e80d5d7 495 type = "downgrade"