]> git.ipfire.org Git - people/stevee/pakfire.git/blob - src/pakfire/transaction.py
Use autotools.
[people/stevee/pakfire.git] / src / pakfire / transaction.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 progressbar
24 import sys
25 import time
26
27 import i18n
28 import packages
29 import satsolver
30 import system
31 import util
32
33 import logging
34 log = logging.getLogger("pakfire")
35
36 from constants import *
37 from i18n import _
38 from pakfire._pakfire import Transaction, sync
39 _Transaction = Transaction
40
41 PKG_DUMP_FORMAT = " %-21s %-8s %-21s %-18s %6s "
42
43 # Import all actions directly.
44 from actions import *
45
46 class TransactionCheck(object):
47 def __init__(self, pakfire, transaction):
48 self.pakfire = pakfire
49 self.transaction = transaction
50
51 # A place to store errors.
52 self.errors = []
53
54 # Get a list of all installed files from the database.
55 self.filelist = self.load_filelist()
56
57 # Get information about the mounted filesystems.
58 self.mountpoints = system.Mountpoints(self.pakfire.path)
59
60 @property
61 def error_files(self):
62 ret = []
63
64 for name, count in self.filelist.items():
65 if count > 1:
66 ret.append(name)
67
68 return sorted(ret)
69
70 def provides_file(self, name):
71 return [] # XXX TODO
72
73 @property
74 def successful(self):
75 if self.error_files:
76 return False
77
78 # Check if all mountpoints have enough space left.
79 for mp in self.mountpoints:
80 if mp.space_left < 0:
81 return False
82
83 return True
84
85 def print_errors(self, logger=None):
86 if logger is None:
87 logger = logging.getLogger("pakfire")
88
89 for file in self.error_files:
90 pkgs = self.provides_file(file)
91
92 if len(pkgs) == 2:
93 logger.critical(
94 _("file %(name)s from %(pkg1)s conflicts with file from package %(pkg2)s") % \
95 { "name" : file, "pkg1" : pkgs[0], "pkg2" : pkgs[1] }
96 )
97
98 elif len(pkgs) >= 3:
99 logger.critical(
100 _("file %(name)s from %(pkg)s conflicts with files from %(pkgs)s") % \
101 { "name" : file, "pkg" : pkgs[0], "pkgs" : i18n.list(pkgs[1:])}
102 )
103
104 else:
105 logger.critical(
106 _("file %(name)s causes the transaction test to fail for an unknown reason") % \
107 { "name" : file }
108 )
109
110 for mp in self.mountpoints:
111 if mp.space_left >= 0:
112 continue
113
114 logger.critical(_("There is not enough space left on %(name)s. Need at least %(size)s to perform transaction.") \
115 % { "name" : mp.path, "size" : util.format_size(mp.space_needed) })
116
117 def load_filelist(self):
118 filelist = {}
119
120 for file in self.pakfire.repos.local.filelist:
121 filelist[file] = 1
122
123 return filelist
124
125 def install(self, pkg):
126 for file in pkg.filelist:
127 if file.is_dir():
128 continue
129
130 try:
131 self.filelist[file.name] += 1
132 except KeyError:
133 self.filelist[file.name] = 1
134
135 # Add all filesize data to mountpoints.
136 self.mountpoints.add_pkg(pkg)
137
138 def remove(self, pkg):
139 for file in pkg.filelist:
140 if file.is_dir():
141 continue
142
143 try:
144 self.filelist[file.name] -= 1
145 except KeyError:
146 pass
147
148 # Remove all filesize data from mountpoints.
149 self.mountpoints.rem_pkg(pkg)
150
151 def update(self, pkg):
152 self.install(pkg)
153
154 def cleanup(self, pkg):
155 self.remove(pkg)
156
157
158 class Transaction(object):
159 action_classes = {
160 ActionInstall.type : [
161 ActionScriptPreTransIn,
162 ActionScriptPreIn,
163 ActionInstall,
164 ActionScriptPostIn,
165 ActionScriptPostTransIn,
166 ],
167 ActionReinstall.type : [
168 ActionScriptPreTransIn,
169 ActionScriptPreIn,
170 ActionReinstall,
171 ActionScriptPostIn,
172 ActionScriptPostTransIn,
173 ],
174 ActionRemove.type : [
175 ActionScriptPreTransUn,
176 ActionScriptPreUn,
177 ActionRemove,
178 ActionScriptPostUn,
179 ActionScriptPostTransUn,
180 ],
181 ActionUpdate.type : [
182 ActionScriptPreTransUp,
183 ActionScriptPreUp,
184 ActionUpdate,
185 ActionScriptPostUp,
186 ActionScriptPostTransUp,
187 ],
188 ActionCleanup.type : [
189 ActionCleanup,
190 ],
191 ActionDowngrade.type : [
192 ActionScriptPreTransUp,
193 ActionScriptPreUp,
194 ActionDowngrade,
195 ActionScriptPostUp,
196 ActionScriptPostTransUp,
197 ],
198 }
199
200 def __init__(self, pakfire):
201 self.pakfire = pakfire
202 self.actions = []
203
204 self.installsizechange = 0
205
206 self.__need_sort = False
207
208 def __nonzero__(self):
209 if self.actions:
210 return True
211
212 return False
213
214 @classmethod
215 def from_solver(cls, pakfire, solver):
216 # Create a new instance of our own transaction class.
217 transaction = cls(pakfire)
218
219 # Get transaction data from the solver.
220 _transaction = _Transaction(solver.solver)
221
222 # Save installsizechange.
223 transaction.installsizechange = _transaction.get_installsizechange()
224
225 # Get all steps that need to be done from the solver.
226 steps = _transaction.steps()
227
228 actions = []
229 actions_post = []
230
231 for step in steps:
232 action_name = step.get_type()
233 pkg = packages.SolvPackage(pakfire, step.get_solvable())
234
235 transaction.add(action_name, pkg)
236
237 # Sort all previously added actions.
238 transaction.sort()
239
240 return transaction
241
242 @property
243 def local(self):
244 # Shortcut to local repository.
245 return self.pakfire.repos.local
246
247 def add(self, action_name, pkg):
248 assert isinstance(pkg, packages.SolvPackage), pkg
249
250 try:
251 classes = self.action_classes[action_name]
252 except KeyError:
253 raise Exception, "Unknown action requires: %s" % action_name
254
255 for cls in classes:
256 action = cls(self.pakfire, pkg)
257 assert isinstance(action, Action), action
258
259 self.actions.append(action)
260
261 self.__need_sort = True
262
263 def sort(self):
264 """
265 Sort all actions.
266 """
267 actions = []
268 actions_pre = []
269 actions_post = []
270
271 for action in self.actions:
272 if isinstance(action, ActionScriptPreTrans):
273 actions_pre.append(action)
274 elif isinstance(action, ActionScriptPostTrans):
275 actions_post.append(action)
276 else:
277 actions.append(action)
278
279 self.actions = actions_pre + actions + actions_post
280 self.__need_sort = False
281
282 @property
283 def installs(self):
284 return [a.pkg for a in self.actions if isinstance(a, ActionInstall)]
285
286 @property
287 def reinstalls(self):
288 return [a.pkg for a in self.actions if isinstance(a, ActionReinstall)]
289
290 @property
291 def removes(self):
292 return [a.pkg for a in self.actions if isinstance(a, ActionRemove)]
293
294 @property
295 def updates(self):
296 return [a.pkg for a in self.actions if isinstance(a, ActionUpdate)]
297
298 @property
299 def downgrades(self):
300 return [a.pkg for a in self.actions if isinstance(a, ActionDowngrade)]
301
302 @property
303 def downloads(self):
304 return sorted([a.pkg_solv for a in self.actions if a.needs_download])
305
306 def download(self, logger=None):
307 if logger is None:
308 logger = logging.getLogger("pakfire")
309
310 # Get all download actions as a list.
311 downloads = [d for d in self.downloads]
312
313 # If there are no downloads, we can just stop here.
314 if not downloads:
315 return
316
317 # Calculate downloadsize.
318 download_size = sum([d.size for d in downloads])
319
320 # Get free space of the download location.
321 path = os.path.realpath(REPO_CACHE_DIR)
322 while not os.path.ismount(path):
323 path = os.path.dirname(path)
324 path_stat = os.statvfs(path)
325
326 if download_size >= path_stat.f_bavail * path_stat.f_bsize:
327 raise DownloadError, _("Not enough space to download %s of packages.") \
328 % util.format_size(download_size)
329
330 logger.info(_("Downloading packages:"))
331 time_start = time.time()
332
333 i = 0
334 for pkg in downloads:
335 i += 1
336
337 # Download the package file.
338 bin_pkg = pkg.download(text="(%d/%d): " % (i, len(downloads)), logger=logger)
339
340 # Search in every action if we need to replace the package.
341 for action in self.actions:
342 if not action.pkg_solv.uuid == bin_pkg.uuid:
343 continue
344
345 # Replace the package.
346 action.pkg = bin_pkg
347
348 # Write an empty line to the console when there have been any downloads.
349 width, height = util.terminal_size()
350
351 # Print a nice line.
352 logger.info("-" * width)
353
354 # Format and calculate download information.
355 time_stop = time.time()
356 download_time = time_stop - time_start
357 download_speed = download_size / download_time
358 download_speed = util.format_speed(download_speed)
359 download_size = util.format_size(download_size)
360 download_time = util.format_time(download_time)
361
362 line = "%s | %5sB %s " % \
363 (download_speed, download_size, download_time)
364 line = " " * (width - len(line)) + line
365 logger.info(line)
366 logger.info("")
367
368 def dump_pkg(self, pkg):
369 ret = []
370
371 name = pkg.name
372 if len(name) > 21:
373 ret.append(" %s" % name)
374 name = ""
375
376 ret.append(PKG_DUMP_FORMAT % (name, pkg.arch, pkg.friendly_version,
377 pkg.repo.name, util.format_size(pkg.size)))
378
379 return ret
380
381 def dump_pkgs(self, caption, pkgs):
382 if not pkgs:
383 return []
384
385 s = [caption,]
386 for pkg in sorted(pkgs):
387 s += self.dump_pkg(pkg)
388 s.append("")
389 return s
390
391 def dump(self, logger=None):
392 if logger is None:
393 logger = logging.getLogger("pakfire")
394
395 if not self.actions:
396 logger.info(_("Nothing to do"))
397 return
398
399 width = 80
400 line = "=" * width
401
402 s = [""]
403 s.append(line)
404 s.append(PKG_DUMP_FORMAT % (_("Package"), _("Arch"), _("Version"),
405 _("Repository"), _("Size")))
406 s.append(line)
407
408 actions = (
409 (_("Installing:"), self.installs),
410 (_("Reinstalling:"), self.reinstalls),
411 (_("Updating:"), self.updates),
412 (_("Downgrading:"), self.downgrades),
413 (_("Removing:"), self.removes),
414 )
415
416 for caption, pkgs in actions:
417 s += self.dump_pkgs(caption, pkgs)
418
419 s.append(_("Transaction Summary"))
420 s.append(line)
421
422 for caption, pkgs in actions:
423 if not len(pkgs):
424 continue
425 s.append("%-20s %-4d %s" % (caption, len(pkgs),
426 _("package", "packages", len(pkgs))))
427
428 # Calculate the size of all files that need to be downloaded this this
429 # transaction.
430 download_size = sum([d.size for d in self.downloads])
431 if download_size:
432 s.append(_("Total download size: %s") % util.format_size(download_size))
433
434 # Show the size that is consumed by the new packages.
435 if self.installsizechange > 0:
436 s.append(_("Installed size: %s") % util.format_size(self.installsizechange))
437 elif self.installsizechange < 0:
438 freed_size = abs(self.installsizechange)
439 s.append(_("Freed size: %s") % util.format_size(freed_size))
440 s.append("")
441
442 for line in s:
443 logger.info(line)
444
445 def cli_yesno(self):
446 # Empty transactions are always denied.
447 if not self.actions:
448 return False
449
450 return util.ask_user(_("Is this okay?"))
451
452 def check(self, logger=None):
453 if logger is None:
454 logger = logging.getLogger("pakfire")
455
456 logger.info(_("Running Transaction Test"))
457
458 # Initialize the check object.
459 check = TransactionCheck(self.pakfire, self)
460
461 for action in self.actions:
462 try:
463 action.check(check)
464 except ActionError, e:
465 raise
466
467 if check.successful:
468 logger.info(_("Transaction Test Succeeded"))
469 return
470
471 # In case of an unsuccessful transaction test, we print the error
472 # and raise TransactionCheckError.
473 check.print_errors(logger=logger)
474
475 raise TransactionCheckError, _("Transaction test was not successful")
476
477 def verify_signatures(self, mode=None, logger=None):
478 """
479 Check the downloaded files for valid signatures.
480 """
481 if not logger:
482 logger = log.getLogger("pakfire")
483
484 if mode is None:
485 mode = self.pakfire.config.get("signatures", "mode", "strict")
486
487 # If this disabled, we do nothing.
488 if mode == "disabled":
489 return
490
491 # Search for actions we need to process.
492 actions = []
493 for action in self.actions:
494 # Skip scripts.
495 if isinstance(action, ActionScript):
496 continue
497
498 actions.append(action)
499
500 # Make a nice progressbar.
501 p = util.make_progress(_("Verifying signatures..."), len(actions))
502
503 # Collect all errors.
504 errors = []
505
506 try:
507 # Do the verification for every action.
508 i = 0
509 for action in actions:
510 # Update the progressbar.
511 if p:
512 i += 1
513 p.update(i)
514
515 try:
516 action.verify()
517
518 except SignatureError, e:
519 errors.append("%s" % e)
520 finally:
521 if p: p.finish()
522
523 # If no errors were found everything is fine.
524 if not errors:
525 logger.info("")
526 return
527
528 # Raise a SignatureError in strict mode.
529 if mode == "strict":
530 raise SignatureError, "\n".join(errors)
531
532 elif mode == "permissive":
533 logger.warning(_("Found %s signature error(s)!") % len(errors))
534 for error in errors:
535 logger.warning(" %s" % error)
536 logger.warning("")
537
538 logger.warning(_("Going on because we are running in permissive mode."))
539 logger.warning(_("This is dangerous!"))
540 logger.warning("")
541
542 def run(self, logger=None, signatures_mode=None):
543 assert self.actions, "Cannot run an empty transaction."
544 assert not self.__need_sort, "Did you forget to sort the transaction?"
545
546 if logger is None:
547 logger = logging.getLogger("pakfire")
548
549 # Download all packages.
550 # (don't add logger here because I do not want to see downloads
551 # in the build logs on the build service)
552 self.download()
553
554 # Verify signatures.
555 self.verify_signatures(mode=signatures_mode, logger=logger)
556
557 # Run the transaction test
558 self.check(logger=logger)
559
560 logger.info(_("Running transaction"))
561 # Run all actions in order and catch all kinds of ActionError.
562 for action in self.actions:
563 try:
564 action.run()
565
566 except ActionError, e:
567 logger.error("Action finished with an error: %s - %s" % (action, e))
568 #except Exception, e:
569 # logger.error(_("An unforeseen error occoured: %s") % e)
570
571 logger.info("")
572
573 # Commit repository metadata.
574 self.local.commit()
575
576 # Call sync to make sure all buffers are written to disk.
577 sync()