]> git.ipfire.org Git - pakfire.git/blame - src/pakfire/cli.py
client: Move CLI to an own file
[pakfire.git] / src / pakfire / cli.py
CommitLineData
964aa579 1#!/usr/bin/python3
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###############################################################################
47a4cb89
MT
21
22import argparse
68c0e769 23import datetime
17563cd0 24import logging
936f6b37 25import os
b913e798 26import shutil
17563cd0 27import signal
47a4cb89
MT
28import sys
29
28d8677b 30from . import _pakfire
964aa579 31from . import client
964aa579 32from . import daemon
964aa579 33
964aa579
MT
34from .constants import *
35from .i18n import _
47a4cb89 36
47a4cb89 37class Cli(object):
e75d1e3c
MT
38 default_path = "/"
39
67b139fb
MT
40 def parse_cli(self):
41 parser = argparse.ArgumentParser(
42 description = _("Pakfire command line interface"),
43 )
44 subparsers = parser.add_subparsers()
45
46 # Add common arguments
47 self._add_common_arguments(parser)
48
5f4c668a
MT
49 parser.add_argument("--arch", "-a", nargs="?",
50 help=_("Run pakfire for the given architecture"))
67b139fb
MT
51 parser.add_argument("--root", metavar="PATH", default="/",
52 help=_("The path where pakfire should operate in"))
53
54 # check
55 check = subparsers.add_parser("check", help=_("Check the system for any errors"))
56 check.set_defaults(func=self.handle_check)
57
67b139fb
MT
58 # clean
59 clean = subparsers.add_parser("clean", help=_("Cleanup all temporary files"))
60 clean.set_defaults(func=self.handle_clean)
61
8d86a58f
MT
62 # execute
63 execute = subparsers.add_parser("execute",
64 help=_("Executes a command in the pakfire environment (useful for development)"))
1577db9b 65 execute.add_argument("--bind", action="append", default=[], dest="binds",
8d86a58f
MT
66 help=_("Bind-mounts the given directory"))
67 execute.add_argument("command", nargs=argparse.REMAINDER)
68 execute.set_defaults(func=self.handle_execute)
69
67b139fb
MT
70 # info
71 info = subparsers.add_parser("info",
72 help=_("Print some information about the given package(s)"))
31ed921f
MT
73 info.add_argument("--long", action="store_true",
74 help=_("Show more information"))
4952c631 75 info.add_argument("package", help=_("Give at least the name of one package"))
67b139fb
MT
76 info.set_defaults(func=self.handle_info)
77
78 # install
79 install = subparsers.add_parser("install",
80 help=_("Install one or more packages to the system"))
81 install.add_argument("package", nargs="+",
82 help=_("Give name of at least one package to install"))
c572abee 83 install.add_argument("--without-recommended", action="store_true",
67b139fb 84 help=_("Don't install recommended packages"))
a005d132
MT
85 install.add_argument("--allow-uninstall", action="store_true",
86 help=_("Allow uninstalling packages"))
87 install.add_argument("--allow-downgrade", action="store_true",
88 help=_("Allow downgrading packages"))
67b139fb
MT
89 install.set_defaults(func=self.handle_install)
90
91 # provides
92 provides = subparsers.add_parser("provides",
93 help=_("Get a list of packages that provide a given file or feature"))
54334355 94 provides.add_argument("pattern", help=_("File or feature to search for"))
67b139fb
MT
95 provides.set_defaults(func=self.handle_provides)
96
8201cef2
MT
97 # requires
98 requires = subparsers.add_parser("requires",
99 help=_("Get a list of packages that require this dependency"))
e1bcca48 100 requires.add_argument("pattern", help=_("File or feature to search for"))
8201cef2
MT
101 requires.set_defaults(func=self.handle_requires)
102
67b139fb
MT
103 # reinstall
104 reinstall = subparsers.add_parser("reinstall",
105 help=_("Reinstall one or more packages"))
106 reinstall.add_argument("package", nargs="+",
107 help=_("Give name of at least one package to reinstall"))
108 reinstall.set_defaults(func=self.handle_reinstall)
109
110 # remove
111 remove = subparsers.add_parser("remove",
112 help=_("Remove one or more packages from the system"))
113 remove.add_argument("package", nargs="+",
114 help=_("Give name of at least one package to remove"))
d798b956
MT
115 remove.add_argument("--keep-dependencies", action="store_true",
116 help=_("Keep dependencies installed"))
67b139fb
MT
117 remove.set_defaults(func=self.handle_remove)
118
119 # repolist
120 repolist = subparsers.add_parser("repolist",
121 help=_("List all currently enabled repositories"))
122 repolist.set_defaults(func=self.handle_repolist)
123
67b139fb
MT
124 # search
125 search = subparsers.add_parser("search", help=_("Search for a given pattern"))
126 search.add_argument("pattern", help=_("A pattern to search for"))
127 search.set_defaults(func=self.handle_search)
128
7b6f9f7b
MT
129 # sync
130 sync = subparsers.add_parser("sync",
131 help=_("Sync all installed with the latest one in the distribution"))
550c7c1f
MT
132 sync.add_argument("--keep-orphaned", action="store_true",
133 help=_("Keep orphaned packages"))
7b6f9f7b
MT
134 sync.set_defaults(func=self.handle_sync)
135
67b139fb
MT
136 # update
137 update = subparsers.add_parser("update",
138 help=_("Update the whole system or one specific package"))
139 update.add_argument("package", nargs="*",
140 help=_("Give a name of a package to update or leave emtpy for all"))
c9b18fbd
MT
141 update.add_argument("--exclude", "-x", nargs="+", default=[],
142 help=_("Exclude package from update"))
a005d132
MT
143 update.add_argument("--allow-uninstall", action="store_true",
144 help=_("Allow uninstalling packages"))
145 update.add_argument("--allow-downgrade", action="store_true",
146 help=_("Allow downgrading packages"))
67b139fb 147 update.set_defaults(func=self.handle_update)
685cb819 148
67b139fb 149 return parser.parse_args()
685cb819 150
95a7e4d9
MT
151 def _add_common_arguments(self, parser, offline_switch=True):
152 parser.add_argument("--version", action="version",
50381f5c
MT
153 version="%(prog)s " + PAKFIRE_VERSION)
154
95a7e4d9 155 parser.add_argument("-v", "--verbose", action="store_true",
47a4cb89
MT
156 help=_("Enable verbose output."))
157
95a7e4d9 158 parser.add_argument("-c", "--config", nargs="?",
47a4cb89
MT
159 help=_("Path to a configuration file to load."))
160
95a7e4d9 161 parser.add_argument("--disable-repo", nargs="*", metavar="REPO",
685cb819 162 help=_("Disable a repository temporarily."), default=[])
f781b1ab 163
95a7e4d9 164 parser.add_argument("--enable-repo", nargs="*", metavar="REPO",
685cb819 165 help=_("Enable a repository temporarily."), default=[])
f9a012a8 166
c62d93f1 167 if offline_switch:
95a7e4d9 168 parser.add_argument("--offline", action="store_true",
c62d93f1 169 help=_("Run pakfire in offline mode."))
6a509182 170
67b139fb 171 def pakfire(self, ns):
28d8677b 172 p = _pakfire.Pakfire(
5f4c668a 173 arch=ns.arch,
8a36f1f2 174 conf=ns.config,
e75d1e3c
MT
175 path=ns.root if "root" in ns else self.default_path,
176 offline=ns.offline,
177 )
05fb1da0 178
67b139fb 179 # Disable repositories.
8ef6c388
MT
180 for repo_name in ns.disable_repo:
181 repo = p.get_repo(repo_name)
182 repo.enabled = False
67b139fb
MT
183
184 # Enable repositories.
8ef6c388
MT
185 for repo_name in ns.enable_repo:
186 repo = p.get_repo(repo_name)
187 repo.enabled = True
67b139fb
MT
188
189 return p
995ed2dd 190
8d86a58f
MT
191 def handle_execute(self, ns):
192 pakfire = self.pakfire(ns)
193
194 # Bind-mount everything
195 for bind in ns.binds:
196 pakfire.bind(bind)
197
59b9d1c2
MT
198 # Log everything to the console
199 def logging_callback(level, line):
200 if level >= logging.ERROR:
201 sys.stderr.write("%s\n" % line)
202 else:
203 sys.stdout.write("%s\n" % line)
204
205 return pakfire.execute(ns.command, logging_callback=logging_callback)
8d86a58f 206
47a4cb89 207 def run(self):
95a7e4d9 208 args = self.parse_cli()
74b61c5c 209 assert args.func, "Argument function not defined"
47a4cb89 210
17563cd0
MT
211 try:
212 return args.func(args)
213
214 except KeyboardInterrupt:
17563cd0
MT
215 return 128 + signal.SIGINT
216
27160734 217 except DependencyError as e:
9b64412f
MT
218 print(_("One or more dependencies could not been resolved"))
219 print("") # empty line
27160734
MT
220
221 # This exception provides a list of all problems
222 problems, = e.args
223
224 # List all problems
225 for problem in problems:
9b64412f 226 print(" * %s" % problem)
27160734 227
9b64412f 228 print(" %s" % _("Possible solutions are:"))
27160734 229 for solution in problem.solutions:
9b64412f 230 print(" * %s" % solution)
27160734
MT
231
232 # Add another empty line
9b64412f 233 print("")
27160734
MT
234
235 return 4
236
17563cd0
MT
237 # Catch all errors and show a user-friendly error message.
238 except Error as e:
9b64412f
MT
239 print(_("An error has occured when running Pakfire"))
240 print(_("%s: %s") % (e.__class__.__name__, e.message))
17563cd0
MT
241
242 return e.exit_code
47a4cb89 243
145f61fe 244 def handle_info(self, ns):
74fe39d3
MT
245 p = self.pakfire(ns)
246
4952c631 247 for pkg in p.search(ns.package, name_only=True):
74fe39d3
MT
248 s = pkg.dump(long=ns.long)
249 print(s)
47a4cb89 250
279509bc 251 def handle_search(self, ns):
74fe39d3 252 p = self.pakfire(ns)
2b2d5b1b 253
74fe39d3
MT
254 for pkg in p.search(ns.pattern):
255 # Skip any -debuginfo packages
256 if pkg.name.endswith("-debuginfo"):
257 continue
258
259 print("%-24s: %s" % (pkg.name, pkg.summary))
47a4cb89 260
b7e72fe2
MT
261 def handle_update(self, ns):
262 p = self.pakfire(ns)
263 p.update(
264 ns.package,
c9b18fbd 265 excludes=ns.exclude,
a005d132
MT
266 allow_uninstall=ns.allow_uninstall,
267 allow_downgrade=ns.allow_downgrade,
b7e72fe2 268 )
d314d72b 269
7b6f9f7b 270 def handle_sync(self, ns):
550c7c1f 271 self.pakfire(ns).sync(keep_orphaned=ns.keep_orphaned)
279509bc 272
279509bc 273 def handle_install(self, ns):
b7e72fe2 274 p = self.pakfire(ns)
a005d132
MT
275 p.install(
276 ns.package,
277 without_recommended=ns.without_recommended,
278 allow_uninstall=ns.allow_uninstall,
279 allow_downgrade=ns.allow_downgrade,
280 )
279509bc
MT
281
282 def handle_reinstall(self, ns):
283 with self.pakfire(ns) as p:
b546c786
MT
284 transaction = p.reinstall(ns.package)
285
286 # Execute the transaction
287 self._execute_transaction(transaction)
279509bc
MT
288
289 def handle_remove(self, ns):
b7e72fe2 290 p = self.pakfire(ns)
d798b956 291 p.erase(ns.package, keep_dependencies=ns.keep_dependencies)
279509bc
MT
292
293 def handle_provides(self, ns, long=False):
74fe39d3
MT
294 for pkg in self.pakfire(ns).whatprovides(ns.pattern):
295 s = pkg.dump(long=long)
296 print(s)
279509bc 297
8201cef2
MT
298 def handle_requires(self, ns):
299 for pkg in self.pakfire(ns).whatrequires(ns.pattern):
300 s = pkg.dump(long=True)
301 print(s)
302
279509bc 303 def handle_repolist(self, ns):
74fe39d3
MT
304 p = self.pakfire(ns)
305
306 FORMAT = " %-20s %8s %12s %12s "
307 title = FORMAT % (_("Repository"), _("Enabled"), _("Priority"), _("Packages"))
308 print(title)
309 print("=" * len(title)) # spacing line
279509bc 310
74fe39d3
MT
311 for repo in p.repos:
312 print(FORMAT % (repo.name, repo.enabled, repo.priority, len(repo)))
279509bc
MT
313
314 def handle_clean(self, ns):
9b64412f 315 print(_("Cleaning up everything..."))
31267a64 316
cb6d631c
MT
317 p = self.pakfire(ns)
318 p.clean()
c901e1f8 319
279509bc 320 def handle_check(self, ns):
d37d8d56 321 self.pakfire(ns).check()
995ed2dd 322
47a4cb89 323
68c0e769 324class CliKey(Cli):
0a89bb8a
MT
325 def parse_cli(self):
326 parser = argparse.ArgumentParser(
327 description = _("Pakfire key command line interface"),
68c0e769 328 )
0a89bb8a 329 subparsers = parser.add_subparsers()
68c0e769 330
0a89bb8a
MT
331 # Add common arguments
332 self._add_common_arguments(parser)
68c0e769 333
b9b6a13e
MT
334 # delete
335 delete = subparsers.add_parser("delete", help=_("Delete a key from the local keyring"))
336 delete.add_argument("fingerprint", nargs="+", help=_("The fingerprint of the key to delete"))
337 delete.set_defaults(func=self.handle_delete)
338
7b526dd2
MT
339 # export
340 export = subparsers.add_parser("export", help=_("Export a key to a file"))
341 export.add_argument("fingerprint", nargs=1,
342 help=_("The fingerprint of the key to export"))
343 export.add_argument("--filename", nargs="*", help=_("Write the key to this file"))
344 export.add_argument("--secret", action="store_true",
345 help=_("Export the secret key"))
346 export.set_defaults(func=self.handle_export)
347
d36b1022 348 # import
7b526dd2
MT
349 _import = subparsers.add_parser("import", help=_("Import a key from file"))
350 _import.add_argument("filename", nargs="+", help=_("Filename of that key to import"))
351 _import.set_defaults(func=self.handle_import)
352
0a89bb8a
MT
353 # generate
354 generate = subparsers.add_parser("generate", help=_("Import a key from file"))
355 generate.add_argument("--realname", nargs=1,
356 help=_("The real name of the owner of this key"))
357 generate.add_argument("--email", nargs=1,
358 help=_("The email address of the owner of this key"))
359 generate.set_defaults(func=self.handle_generate)
360
361 # list
362 list = subparsers.add_parser("list", help=_("List all imported keys"))
363 list.set_defaults(func=self.handle_list)
364
0a89bb8a 365 return parser.parse_args()
68c0e769 366
68c0e769
MT
367 def parse_command_sign(self):
368 # Implement the "sign" command.
369 sub_sign = self.sub_commands.add_parser("sign",
370 help=_("Sign one or more packages."))
371 sub_sign.add_argument("--key", "-k", nargs=1,
372 help=_("Key that is used sign the package(s)."))
373 sub_sign.add_argument("package", nargs="+",
374 help=_("Package(s) to sign."))
375 sub_sign.add_argument("action", action="store_const", const="sign")
376
377 def parse_command_verify(self):
378 # Implement the "verify" command.
379 sub_verify = self.sub_commands.add_parser("verify",
380 help=_("Verify one or more packages."))
381 #sub_verify.add_argument("--key", "-k", nargs=1,
382 # help=_("Key that is used verify the package(s)."))
383 sub_verify.add_argument("package", nargs="+",
384 help=_("Package(s) to verify."))
385 sub_verify.add_argument("action", action="store_const", const="verify")
386
0a89bb8a
MT
387 def handle_generate(self, ns):
388 p = self.pakfire(ns)
68c0e769 389
964aa579
MT
390 print(_("Generating the key may take a moment..."))
391 print()
68c0e769 392
ad62bb07 393 key = p.generate_key("%s <%s>" % (ns.realname.pop(), ns.email.pop()))
0a89bb8a
MT
394
395 def handle_list(self, ns):
396 p = self.pakfire(ns)
397
ad62bb07 398 for key in p.keys:
0a89bb8a
MT
399 print(key)
400
401 def handle_export(self, ns):
402 p = self.pakfire(ns)
403
404 for fingerprint in ns.fingerprint:
ad62bb07 405 key = p.get_key(fingerprint)
0a89bb8a
MT
406 if not key:
407 print(_("Could not find key with fingerprint %s") % fingerprint)
408 continue
409
410 data = key.export(ns.secret)
411
412 for file in ns.filename:
413 with open(file, "w") as f:
414 f.write(data)
415 else:
416 print(data)
68c0e769 417
7b526dd2
MT
418 def handle_import(self, ns):
419 p = self.pakfire(ns)
68c0e769 420
7b526dd2
MT
421 for filename in ns.filename:
422 with open(filename) as f:
423 data = f.read()
424
91f40bc5
MT
425 keys = p.import_key(data)
426 for key in keys:
427 print(key)
68c0e769 428
b9b6a13e
MT
429 def handle_delete(self, ns):
430 p = self.pakfire(ns)
eaf999ef 431
b9b6a13e
MT
432 for fingerprint in ns.fingerprint:
433 key = p.get_key(fingerprint)
434 if key:
435 key.delete()
eaf999ef 436
68c0e769
MT
437 def handle_sign(self):
438 # Get the files from the command line options
439 files = []
440
441 for file in self.args.package:
442 # Check, if we got a regular file
443 if os.path.exists(file):
444 file = os.path.abspath(file)
445 files.append(file)
446
447 else:
964aa579 448 raise FileNotFoundError(file)
68c0e769
MT
449
450 key = self.args.key[0]
451
36b328f2 452 # Create pakfire instance.
3a1ddabb 453 p = self.create_pakfire()
36b328f2 454
68c0e769
MT
455 for file in files:
456 # Open the package.
36b328f2 457 pkg = packages.open(p, None, file)
68c0e769 458
964aa579 459 print(_("Signing %s...") % pkg.friendly_name)
68c0e769
MT
460 pkg.sign(key)
461
462 def handle_verify(self):
b4f722d1
MT
463 # TODO needs to use new key code from libpakfire
464
68c0e769
MT
465 # Get the files from the command line options
466 files = []
467
468 for file in self.args.package:
469 # Check, if we got a regular file
470 if os.path.exists(file) and not os.path.isdir(file):
471 file = os.path.abspath(file)
472 files.append(file)
473
36b328f2 474 # Create pakfire instance.
3a1ddabb 475 p = self.create_pakfire()
36b328f2 476
68c0e769
MT
477 for file in files:
478 # Open the package.
36b328f2 479 pkg = packages.open(p, None, file)
68c0e769 480
964aa579 481 print(_("Verifying %s...") % pkg.friendly_name)
68c0e769
MT
482 sigs = pkg.verify()
483
484 for sig in sigs:
3a1ddabb 485 key = p.keyring.get_key(sig.fpr)
68c0e769
MT
486 if key:
487 subkey = key.subkeys[0]
488
964aa579 489 print(" %s %s" % (subkey.fpr[-16:], key.uids[0].uid))
68c0e769 490 if sig.validity:
964aa579 491 print(" %s" % _("This signature is valid."))
68c0e769
MT
492
493 else:
964aa579
MT
494 print(" %s <%s>" % (sig.fpr, _("Unknown key")))
495 print(" %s" % _("Could not check if this signature is valid."))
68c0e769
MT
496
497 created = datetime.datetime.fromtimestamp(sig.timestamp)
964aa579 498 print(" %s" % _("Created: %s") % created)
68c0e769
MT
499
500 if sig.exp_timestamp:
501 expires = datetime.datetime.fromtimestamp(sig.exp_timestamp)
964aa579 502 print(" %s" % _("Expires: %s") % expires)
68c0e769 503
964aa579 504 print() # Empty line