diff --git a/doc/bootc.rst b/doc/bootc.rst index 7e386d8e..aefda66e 100644 --- a/doc/bootc.rst +++ b/doc/bootc.rst @@ -8,7 +8,7 @@ Manipulate image mode based systems using RPMs available from dnf repositories. Synopsis -------- -``dnf bootc [status] `` +``dnf bootc [status|install] `` ----------------- Arguments (bootc) @@ -17,6 +17,9 @@ Arguments (bootc) ``bootc status `` The status of the image mode system. +``bootc install [options] ...`` + Install one or more packages. + ------- Options ------- @@ -33,7 +36,28 @@ All general DNF options are accepted, see `Options` in :manpage:`dnf(8)` for det Filter JSONPath expression (valid on the status subcommand). ``--pending-exit-77`` - If pending deploymewnt available, exit 77 (valid on the status subcommand). + If pending deployment available, exit 77 (valid on the status subcommand). + +``--uninstall PKG...`` + Remove overlayed additional package (valid on the install subcommand). + +``-A``, ``--apply-live`` + Apply changes to both pending deployment and running filesystem tree (valid on the install subcommand). + +``--force-replacefiles`` + Allow package to replace files from other packages (valid on the install subcommand). + +``-r``, ``--reboot`` + Initiate a reboot after operation is complete (valid on the install subcommand). + +``--allow-inactive`` + Allow inactive package requests (valid on the install subcommand). + +``--idempotent`` + Do nothing if package already (un)installed (valid on the install subcommand). + +``--unchanged-exit-77`` + If no overlays were changed, exit 77 (valid on the install subcommand). ``--peer`` - Force a peer-to-peer connection instead of using the system message bus (valid on the status subcommand). + Force a peer-to-peer connection instead of using the system message bus (valid on the status and install subcommands). diff --git a/plugins/bootc.py b/plugins/bootc.py index 055e6a6a..b9fc2670 100644 --- a/plugins/bootc.py +++ b/plugins/bootc.py @@ -19,19 +19,15 @@ class BootcCommand(dnf.cli.Command): aliases = ["bootc"] summary = _("Modify software on a bootc-based system") + usage = _("[PACKAGE ...]") _BOOTC_ALIASES = {"update": "upgrade", "erase": "remove"} - _BOOTC_SUBCOMMANDS = ["status", "install"] + _BOOTC_SUBCMDS = ["status"] + _BOOTC_SUBCMDS_PKGSPECS = ["install"] + _BOOTC_SUBCMDS_ALL = _BOOTC_SUBCMDS + _BOOTC_SUBCMDS_PKGSPECS _EXT_CMD = "rpm-ostree" - def _canonical(self): - if self.opts.subcmd is None: - self.opts.subcmd = "status" - - if self.opts.subcmd in self._BOOTC_ALIASES.keys(): - self.opts.subcmd = self._BOOTC_ALIASES[self.opts.subcmd] - def __init__(self, cli): super().__init__(cli) @@ -40,12 +36,9 @@ def set_argparser(parser): # subcommands for the plugin parser.add_argument( "subcmd", - nargs="?", - metavar="BOOTC", - help=_("available subcommands: {} (default), {}").format( - BootcCommand._BOOTC_SUBCOMMANDS[0], - ", ".join(BootcCommand._BOOTC_SUBCOMMANDS[1:]), - ), + nargs=1, + choices=BootcCommand._BOOTC_SUBCMDS_ALL, + help=_("Available subcommands"), ) # these options are for 'status' @@ -68,27 +61,70 @@ def set_argparser(parser): action="store_true", help=_("If pending deployment available, exit 77 (status)"), ) + + # these options are for 'install' + parser.add_argument( + "--uninstall", + nargs="+", + metavar="PKG", + action="store", + help=_("Remove overlayed additional package (install)"), + ) + parser.add_argument( + "-A", + "--apply-live", + action="store_true", + help=_( + "Apply changes to both pending deployment and running filesystem tree (install)" + ), + ) + parser.add_argument( + "--force-replacefiles", + action="store_true", + help=_("Allow package to replace files from other packages (install)"), + ) + parser.add_argument( + "-r", + "--reboot", + action="store_true", + help=_("Initiate a reboot after operation is complete (install)"), + ) + parser.add_argument( + "--allow-inactive", + action="store_true", + help=_("Allow inactive package requests (install)"), + ) + parser.add_argument( + "--idempotent", + action="store_true", + help=_("Do nothing if package already (un)installed (install)"), + ) + parser.add_argument( + "--unchanged-exit-77", + action="store_true", + help=_("If no overlays were changed, exit 77 (install)"), + ) + + # valid under multiple subcommands parser.add_argument( "--peer", action="store_true", help=_( - "Force a peer-to-peer connection instead of using the system message bus (status)" + "Force a peer-to-peer connection instead of using the system message bus (status, install)" ), ) + parser.add_argument( + "pkgspec", nargs="*", help=_("One or more package specifications") + ) + def configure(self): super().configure() - self._canonical() - cmd = self.opts.subcmd - - # ensure we have a valid subcommand - if cmd not in self._BOOTC_SUBCOMMANDS: - logger.critical( - _("Invalid bootc sub-command, use: %s."), - ", ".join(self._BOOTC_SUBCOMMANDS), - ) - raise dnf.cli.CliError + if self.opts.subcmd[0] in self._BOOTC_ALIASES.keys(): + cmd = self._BOOTC_ALIASES[self.opts.subcmd[0]] + else: + cmd = self.opts.subcmd[0] self.extargs = [self._EXT_CMD, cmd] @@ -110,6 +146,66 @@ def configure(self): self.extargs.append("--peer") elif self.opts.installroot: self.extargs.append("--sysroot=%s" % self.opts.installroot) + elif cmd == "install": + if self.opts.quiet: + self.extargs.append("-q") + elif self.opts.installroot: + self.extargs.append("--sysroot=%s" % self.opts.installroot) + elif self.opts.peer: + self.extargs.append("--peer") + elif self.opts.assumeyes: + self.extargs.append("-y") + elif self.opts.assumeno: + self.extargs.append("-n") + elif self.opts.cacheonly: + self.extargs.append("-C") + elif self.opts.downloadonly: + self.extargs.append("--download-only") + elif self.opts.releasever: + self.extargs.append + self.extargs.append("--releasever=%s" % self.opts.releasever) + elif len(self.opts.repos_ed) > 0: + enabled = set() + disabled = set() + + for name, state in self.opts.repos_ed: + if state == "enable": + enabled.add(name) + elif state == "disable": + disabled.add(name) + + if len(list(enabled)) > 0: + for repo in list(enabled): + self.extargs.append("--enablerepo=%s" % repo) + + if len(list(disabled)) > 0: + for repo in list(disabled): + self.extargs.append("--disablerepo=%s" % repo) + elif self.opts.uninstall: + for pname in self.opts.uninstall: + self.extargs.append("--uninstall=%s" % pname) + elif self.opts.apply_live: + self.extargs.append("-A") + elif self.opts.force_replacefiles: + self.extargs.append("--force-replacefiles") + elif self.opts.reboot: + self.extargs.append("-r") + elif self.opts.allow_inactive: + self.extargs.append("--allow-inactive") + elif self.opts.idempotent: + self.extargs.append("--idempotent") + elif self.opts.unchanged_exit_77: + self.extargs.append("--unchanged-exit-77") + + if cmd in self._BOOTC_SUBCMDS_PKGSPECS: + if self.opts.pkgspec is not None and len(self.opts.pkgspec) > 0: + self.extargs += self.opts.pkgspec + else: + # ensure we have a valid subcommand + logger.critical( + _("Missing package specification on bootc sub-command '%s'." % cmd) + ) + raise dnf.cli.CliError def run(self): # combine stdout and stderr; capture text output