]> git.ipfire.org Git - people/stevee/pakfire.git/blob - src/pakfire/actions.py
Fix execution of scriptlets in a dirty environment
[people/stevee/pakfire.git] / src / pakfire / actions.py
1 #!/usr/bin/python
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 ###############################################################################
21
22 import os
23 import sys
24
25 import packages
26 import shell
27 import util
28
29 import logging
30 log = logging.getLogger("pakfire")
31
32 from constants import *
33 from i18n import _
34
35 class Action(object):
36 def __init__(self, pakfire, pkg_solv, pkg_bin=None):
37 self.pakfire = pakfire
38
39 self.pkg_solv = pkg_solv
40 if pkg_bin:
41 self.pkg_bin = pkg_bin
42
43 self.init()
44
45 def __cmp__(self, other):
46 return cmp(self.pkg, other.pkg)
47
48 def __repr__(self):
49 return "<%s %s>" % (self.__class__.__name__, self.pkg.friendly_name)
50
51 def init(self):
52 # A function to run additional initialization.
53 pass
54
55 def check(self, filelist):
56 # This is just a dummy test that does nothing at all.
57 return filelist
58
59 def verify(self):
60 assert self.pkg, "No package! %s" % self.pkg
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
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
77 @property
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
83
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()
93
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)
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
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
128 def execute(self, command, **kwargs):
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"):
137 if chroot_path:
138 _cwd = os.path.join(chroot_path, i)
139 else:
140 _cwd = i
141
142 if os.path.exists(_cwd):
143 cwd = _cwd
144 break
145
146 args = {
147 "cwd" : cwd,
148 "env" : MINIMAL_ENVIRONMENT,
149 "logger" : self.get_logger(),
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.
159 args["chroot_path"] = chroot_path
160
161 # Execute command.
162 shellenv = shell.ShellExecuteEnvironment(command, **args)
163 shellenv.execute()
164
165
166 class ActionScript(Action):
167 type = "script"
168 script_action = None
169
170 def init(self):
171 self._scriptlet = None
172
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
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
187
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
205 @property
206 def interpreter(self):
207 """
208 Get the interpreter of this scriptlet.
209 """
210 return util.scriptlet_interpreter(self.scriptlet)
211
212 @property
213 def args(self):
214 return []
215
216 def run(self):
217 # Exit immediately, if the scriptlet is empty.
218 if not self.scriptlet:
219 return
220
221 # Actually run the scriplet.
222 log.debug("Running scriptlet %s" % self)
223
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):
238 log.debug(_("Executing scriptlet..."))
239
240 # Check if the interpreter does exist and is executable.
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)
246
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)
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:])
255 assert script_file.startswith(self.pakfire.path)
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
280 # Generate the script command.
281 command = [script_file_chroot] + self.args
282
283 try:
284 self.execute(command)
285
286 except ShellEnvironmentError, e:
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
293 except Exception, e:
294 raise ActionError, _("The scriptlet returned with an unhandled error:\n%s" % e)
295
296 finally:
297 # Remove the script file.
298 try:
299 os.unlink(script_file)
300 except OSError:
301 log.debug("Could not remove scriptlet file: %s" % script_file)
302
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
345
346 class ActionScriptPreIn(ActionScript):
347 script_action = "prein"
348
349
350 class ActionScriptPostIn(ActionScript):
351 script_action = "postin"
352
353
354 class ActionScriptPreUn(ActionScript):
355 script_action = "preun"
356
357
358 class ActionScriptPostUn(ActionScript):
359 script_action = "postun"
360
361
362 class ActionScriptPreUp(ActionScript):
363 script_action = "preup"
364
365
366 class ActionScriptPostUp(ActionScript):
367 script_action = "postup"
368
369
370 class ActionScriptPreTrans(ActionScript):
371 pass
372
373
374 class ActionScriptPreTransIn(ActionScriptPreTrans):
375 script_action = "pretransin"
376
377
378 class ActionScriptPreTransUn(ActionScriptPreTrans):
379 script_action = "pretransun"
380
381
382 class ActionScriptPreTransUp(ActionScriptPreTrans):
383 script_action = "pretransup"
384
385
386 class ActionScriptPostTrans(ActionScript):
387 pass
388
389
390 class ActionScriptPostTransIn(ActionScriptPostTrans):
391 script_action = "posttransin"
392
393
394 class ActionScriptPostTransUn(ActionScriptPostTrans):
395 script_action = "posttransun"
396
397
398 class ActionScriptPostTransUp(ActionScriptPostTrans):
399 script_action = "posttransup"
400
401
402 class ActionInstall(Action):
403 type = "install"
404
405 def check(self, check):
406 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
407
408 # Check if this package can be installed.
409 check.install(self.pkg)
410
411 def run(self):
412 # Add package to the database.
413 self.local.add_package(self.pkg)
414
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)
425
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):
443 self.execute(LDCONFIG)
444
445 else:
446 log.debug("ldconfig is not present or not executable.")
447
448
449 class ActionUpdate(ActionInstall):
450 type = "upgrade"
451
452 def check(self, check):
453 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
454
455 # Check if this package can be updated.
456 check.update(self.pkg)
457
458
459 class ActionRemove(Action):
460 type = "erase"
461
462 def check(self, check):
463 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
464
465 # Check if this package can be removed.
466 check.remove(self.pkg)
467
468 def run(self):
469 if isinstance(self, ActionCleanup):
470 msg = _("Cleanup")
471 else:
472 msg = _("Removing")
473
474 self.pkg.cleanup(msg, prefix=self.pakfire.path)
475
476 # Remove package from the database.
477 self.local.rem_package(self.pkg)
478
479
480 class ActionCleanup(ActionRemove):
481 type = "ignore"
482
483 def check(self, check):
484 log.debug(_("Running transaction test for %s") % self.pkg.friendly_name)
485
486 # Check if this package can be removed.
487 check.cleanup(self.pkg)
488
489
490 class ActionReinstall(ActionInstall):
491 type = "reinstall"
492
493
494 class ActionDowngrade(ActionInstall):
495 type = "downgrade"