Compare commits

..

3 Commits

Author SHA1 Message Date
b36bcb194b move argument parsers to handlers themselves 2024-10-05 17:26:06 +03:00
b167df904b build: use tool.flit.external-data to distribute data 2024-10-05 17:17:16 +03:00
cd0ac7a7bd chore: replace passlib with bcrypt
passlib uses deprecated crypt module which is deprecated and scheduled
for removal in 3.13. Unfortunately, this module seems to be
unmaintained, so this commit replaces passlib with bcrypt, unfortunately
breaking current passwords
2024-10-05 16:35:27 +03:00
48 changed files with 742 additions and 747 deletions

View File

@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
# refresh the image # refresh the image
pacman -Syyu --noconfirm pacman -Syyu --noconfirm
# main dependencies # main dependencies
pacman -S --noconfirm devtools git pyalpm python-inflection python-passlib python-pyelftools python-requests python-systemd sudo pacman -S --noconfirm devtools git pyalpm python-bcrypt python-inflection python-pyelftools python-requests python-systemd sudo
# make dependencies # make dependencies
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
# optional dependencies # optional dependencies

View File

@ -35,8 +35,8 @@ RUN pacman -S --noconfirm --asdeps \
devtools \ devtools \
git \ git \
pyalpm \ pyalpm \
python-bcrypt \
python-inflection \ python-inflection \
python-passlib \
python-pyelftools \ python-pyelftools \
python-requests \ python-requests \
&& \ && \

View File

@ -147,7 +147,7 @@ There are multiple subdirectories, some of them are commons for any repository,
* ``pacman/{repository}/{architecture}`` is the repository and architecture specific caches for pacman's databases. * ``pacman/{repository}/{architecture}`` is the repository and architecture specific caches for pacman's databases.
* ``repository/{repository}/{architecture}`` is a repository packages directory. * ``repository/{repository}/{architecture}`` is a repository packages directory.
Normally you should avoid direct interaction with the application tree. For tree migration process refer to the :doc:`migration notes <migration>`. Normally you should avoid direct interaction with the application tree. For tree migration process refer to the :doc:`migration notes <migrations/index>`.
Database Database
-------- --------

View File

@ -34,7 +34,7 @@ Contents
configuration configuration
command-line command-line
faq/index faq/index
migration migrations/index
architecture architecture
advanced-usage advanced-usage
triggers triggers

View File

@ -1,25 +1,5 @@
Manual migrations
=================
Normally the most of migrations are handled automatically after application start, however, some upgrades require manual interventions; this document describes them.
Upgrades to breakpoints
-----------------------
To 2.9.0
^^^^^^^^
This release includes major upgrade for the newest devtools and archlinux repository structure. In order to upgrade package need to:
#. Upgrade to the latest major release of python (3.11) (required by other changes).
#. Upgrade devtools to the latest release.
#. Backup local settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default.
#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration.
#. Remove build chroot as it is incompatible, e.g. ``sudo ahriman service-clean --chroot``.
#. Run ``sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy`` in order to update local databases.
To 2.12.0 To 2.12.0
^^^^^^^^^ ---------
This release includes paths migration. Unlike usual case, no automatic migration is performed because it might break user configuration. The following noticeable changes have been made: This release includes paths migration. Unlike usual case, no automatic migration is performed because it might break user configuration. The following noticeable changes have been made:

View File

@ -0,0 +1,16 @@
To 2.16.0
---------
This release replaces ``passlib`` dependency with ``bcrypt``.
The reason behind this change is that python developers have deprecated and scheduled for removal ``crypt`` module, which is used by ``passlib``. (By the way, they recommend to use ``passlib`` as a replacement.) Unfortunately, it appears that ``passlib`` is unmaintained (see `the issue <https://foss.heptapod.net/python-libs/passlib/-/issues/187>`__), so the only solution is to migrate to anoher library.
Because passwords are stored as hashes, it is near to impossible to shadow change passwords in database, the manual intervention is required if:
#. Authentication is used.
#. Notification provider is ``configuration`` or a user with explicitly set password exists.
Manual steps might look as:
#. Get list of users with their roles ``ahriman user-list``.
#. For each user run update command, i.e. ``ahriman user-add <username> -R <role>``. Type password when it will be requested.

11
docs/migrations/2.9.0.rst Normal file
View File

@ -0,0 +1,11 @@
To 2.9.0
--------
This release includes major upgrade for the newest devtools and archlinux repository structure. In order to upgrade package need to:
#. Upgrade to the latest major release of python (3.11) (required by other changes).
#. Upgrade devtools to the latest release.
#. Backup local settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default.
#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration.
#. Remove build chroot as it is incompatible, e.g. ``sudo ahriman service-clean --chroot``.
#. Run ``sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy`` in order to update local databases.

14
docs/migrations/index.rst Normal file
View File

@ -0,0 +1,14 @@
Manual migrations
=================
Normally the most of migrations are handled automatically after application start, however, some upgrades require manual interventions; this document describes them.
Upgrades to breakpoints
-----------------------
.. toctree::
:maxdepth: 2
2.9.0
2.12.0
2.16.0

View File

@ -7,7 +7,7 @@ pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') license=('GPL3')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests') depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-bcrypt' 'python-inflection' 'python-pyelftools' 'python-requests')
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel') makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
optdepends=('python-aioauth-client: web server with OAuth2 authorization' optdepends=('python-aioauth-client: web server with OAuth2 authorization'
'python-aiohttp: web server' 'python-aiohttp: web server'
@ -42,9 +42,6 @@ package() {
python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl" python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl"
# thanks too PEP517, which we all wanted, you need to install data files manually nowadays
pushd package && find . \( -type f -or -type l \) -exec install -Dm644 "{}" "$pkgdir/usr/{}" \; && popd
# keep usr/share configs as reference and copy them to /etc # keep usr/share configs as reference and copy them to /etc
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini" install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini" install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"
@ -52,7 +49,3 @@ package() {
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf" install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf" install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
} }
sha512sums=('19841842641520b573cdde6cb80a7cfcd69756d323fdfeebc2eee2d264a1325ead4ab2f8383bb369f7896bfc1de59d7358f133f4afeb90a9b9f0695f482a58d0'
'53d37efec812afebf86281716259f9ea78a307b83897166c72777251c3eebcb587ecee375d907514781fb2a5c808cbb24ef9f3f244f12740155d0603bf213131'
'62b2eccc352d33853ef243c9cddd63663014aa97b87242f1b5bc5099a7dbd69ff3821f24ffc58e1b7f2387bd4e9e9712cc4c67f661b1724ad99cdf09b3717794')

View File

@ -21,7 +21,7 @@ It was found that there was an upgrade from old devtools package to the new one,
* remove build chroot, e.g.: ahriman service-clean --chroot; * remove build chroot, e.g.: ahriman service-clean --chroot;
* update local databases: ahriman update --no-aur --no-local --no-manual -yy. * update local databases: ahriman update --no-aur --no-local --no-manual -yy.
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html. For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migrations/2.9.0.html.
EOF EOF
} }
@ -37,6 +37,16 @@ Whereas old local tree is still supported it is highly recommended to migrate to
* enable web and timer services again by using x86_64-aur suffix, * enable web and timer services again by using x86_64-aur suffix,
where x86_64 is the repository architecture and aur is the repository name. where x86_64 is the repository architecture and aur is the repository name.
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html. For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migrations/2.12.0.html.
EOF
}
_2_16_0_1_changes() {
cat << EOF
In order to prepare to python 3.13 the project now uses bcrypt instead of passlib for generating and validating
passwords, because the passlib seems to be unmaintained and will be broken since then. If you are using password
authentication, you'd need to generate passwords again.
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migrations/2.16.0.html.
EOF EOF
} }

View File

@ -17,8 +17,8 @@ authors = [
] ]
dependencies = [ dependencies = [
"bcrypt",
"inflection", "inflection",
"passlib",
"pyelftools", "pyelftools",
"requests", "requests",
] ]
@ -100,3 +100,6 @@ include = [
exclude = [ exclude = [
"package/archlinux", "package/archlinux",
] ]
[tool.flit.external-data]
directory = "package"

View File

@ -89,7 +89,8 @@ Start web service (requires additional configuration):
handlers_root = Path(__file__).parent / "handlers" handlers_root = Path(__file__).parent / "handlers"
for handler in implementations(handlers_root, "ahriman.application.handlers", Handler): for handler in implementations(handlers_root, "ahriman.application.handlers", Handler):
for subparser in handler.arguments(subparsers): for subparser_parser in handler.arguments:
subparser = subparser_parser(subparsers)
subparser.formatter_class = _HelpFormatter subparser.formatter_class = _HelpFormatter
subparser.set_defaults(handler=handler, parser=_parser) subparser.set_defaults(handler=handler, parser=_parser)

View File

@ -34,50 +34,6 @@ class Add(Handler):
add packages handler add packages handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
description="add existing or new package to the build queue",
epilog="This subcommand should be used for new package addition. "
"It also supports flag --now in case if you would like to build "
"the package immediately. You can add new package from one of "
"supported sources:\n\n"
"1. If it is already built package you can specify the path to the archive.\n"
"2. You can also add built packages from the directory (e.g. during the "
"migration from another repository source).\n"
"3. It is also possible to add package from local PKGBUILD, but in this case "
"it will be ignored during the next automatic updates.\n"
"4. Ahriman supports downloading archives from remote (e.g. HTTP) sources.\n"
"5. Finally you can add package from AUR.")
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
parser.add_argument("--changes", help="calculate changes from the latest known commit if available",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
action="append")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -112,3 +68,49 @@ class Add(Handler):
application.print_updates(packages, log_fn=application.logger.info) application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers, bump_pkgrel=args.increment) result = application.update(packages, packagers, bump_pkgrel=args.increment)
Add.check_status(args.exit_code, not result.is_empty) Add.check_status(args.exit_code, not result.is_empty)
@staticmethod
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package addition subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
description="add existing or new package to the build queue",
epilog="This subcommand should be used for new package addition. "
"It also supports flag --now in case if you would like to build "
"the package immediately. You can add new package from one of "
"supported sources:\n\n"
"1. If it is already built package you can specify the path to the archive.\n"
"2. You can also add built packages from the directory (e.g. during the "
"migration from another repository source).\n"
"3. It is also possible to add package from local PKGBUILD, but in this case "
"it will be ignored during the next automatic updates.\n"
"4. Ahriman supports downloading archives from remote (e.g. HTTP) sources.\n"
"5. Finally you can add package from AUR.")
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
parser.add_argument("--changes", help="calculate changes from the latest known commit if available",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
action="append")
return parser
arguments = [_set_package_add_parser]

View File

@ -36,23 +36,6 @@ class Backup(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup repository settings and database")
parser.add_argument("path", help="path of the output archive", type=Path)
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -70,6 +53,23 @@ class Backup(Handler):
for backup_path in backup_paths: for backup_path in backup_paths:
archive.add(backup_path) archive.add(backup_path)
@staticmethod
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository backup subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup repository settings and database")
parser.add_argument("path", help="path of the output archive", type=Path)
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return parser
@staticmethod @staticmethod
def get_paths(configuration: Configuration) -> set[Path]: def get_paths(configuration: Configuration) -> set[Path]:
""" """
@ -100,3 +100,5 @@ class Backup(Handler):
paths.add(gnupg_home) paths.add(gnupg_home)
return paths return paths
arguments = [_set_repo_backup_parser]

View File

@ -35,19 +35,6 @@ class Change(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
return [cls._set_package_changes_parser(root), cls._set_package_changes_remove_parser(root)]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -108,3 +95,5 @@ class Change(Handler):
parser.add_argument("package", help="package base") parser.add_argument("package", help="package base")
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True) parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
return parser return parser
arguments = [_set_package_changes_parser, _set_package_changes_remove_parser]

View File

@ -31,15 +31,32 @@ class Clean(Handler):
""" """
@classmethod @classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]: def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
""" """
add parser(s) for the subcommand callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
application.on_start()
application.clean(cache=args.cache, chroot=args.chroot, manual=args.manual, packages=args.packages,
pacman=args.pacman)
@staticmethod
def _set_service_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository clean subcommand
Args: Args:
root(SubParserAction): subparsers for the commands root(SubParserAction): subparsers for the commands
Returns: Returns:
list[argparse.ArgumentParser]: created argument parser argparse.ArgumentParser: created argument parser
""" """
parser = root.add_parser("service-clean", aliases=["clean", "repo-clean"], help="clean local caches", parser = root.add_parser("service-clean", aliases=["clean", "repo-clean"], help="clean local caches",
description="remove local caches", description="remove local caches",
@ -56,21 +73,6 @@ class Clean(Handler):
parser.add_argument("--pacman", help="clear directory with pacman local database cache", parser.add_argument("--pacman", help="clear directory with pacman local database cache",
action=argparse.BooleanOptionalAction, default=False) action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(lock=None, quiet=True, unsafe=True) parser.set_defaults(lock=None, quiet=True, unsafe=True)
return [parser] return parser
@classmethod arguments = [_set_service_clean_parser]
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
application.on_start()
application.clean(cache=args.cache, chroot=args.chroot, manual=args.manual, packages=args.packages,
pacman=args.pacman)

View File

@ -35,26 +35,6 @@ class Copy(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting action ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("package-copy", aliases=["copy"], help="copy package from another repository",
description="copy package and its metadata from another repository")
parser.add_argument("source", help="source repository name")
parser.add_argument("package", help="package base", nargs="+")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--remove", help="remove package from the source repository after", action="store_true")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -87,6 +67,26 @@ class Copy(Handler):
if args.remove: if args.remove:
source_application.remove(args.package) source_application.remove(args.package)
@staticmethod
def _set_package_copy_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package copy subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-copy", aliases=["copy"], help="copy package from another repository",
description="copy package and its metadata from another repository")
parser.add_argument("source", help="source repository name")
parser.add_argument("package", help="package base", nargs="+")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--remove", help="remove package from the source repository after", action="store_true")
return parser
@staticmethod @staticmethod
def copy_package(package: Package, application: Application, source_application: Application) -> None: def copy_package(package: Package, application: Application, source_application: Application) -> None:
""" """
@ -113,3 +113,5 @@ class Copy(Handler):
package.base, source_application.reporter.package_dependencies_get(package.base) package.base, source_application.reporter.package_dependencies_get(package.base)
) )
application.reporter.package_update(package, BuildStatusEnum.Pending) application.reporter.package_update(package, BuildStatusEnum.Pending)
arguments = [_set_package_copy_parser]

View File

@ -36,15 +36,40 @@ class Daemon(Handler):
""" """
@classmethod @classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]: def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
""" """
add parser(s) for the subcommand callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
if args.partitions:
iterator = UpdatesIterator(application, args.interval)
else:
iterator = FixedUpdatesIterator(application, args.interval)
for packages in iterator:
if packages is None:
continue # nothing to check case
args.package = packages
Update.run(args, repository_id, configuration, report=report)
@staticmethod
def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for daemon subcommand
Args: Args:
root(SubParserAction): subparsers for the commands root(SubParserAction): subparsers for the commands
Returns: Returns:
list[argparse.ArgumentParser]: created argument parser argparse.ArgumentParser: created argument parser
""" """
parser = root.add_parser("repo-daemon", aliases=["daemon"], help="run application as daemon", parser = root.add_parser("repo-daemon", aliases=["daemon"], help="run application as daemon",
description="start process which periodically will run update process") description="start process which periodically will run update process")
@ -76,29 +101,6 @@ class Daemon(Handler):
"-yy to force refresh even if up to date", "-yy to force refresh even if up to date",
action="count", default=False) action="count", default=False)
parser.set_defaults(exit_code=False, lock=Path("ahriman-daemon.pid"), package=[]) parser.set_defaults(exit_code=False, lock=Path("ahriman-daemon.pid"), package=[])
return [parser] return parser
@classmethod arguments = [_set_repo_daemon_parser]
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
if args.partitions:
iterator = UpdatesIterator(application, args.interval)
else:
iterator = FixedUpdatesIterator(application, args.interval)
for packages in iterator:
if packages is None:
continue # nothing to check case
args.package = packages
Update.run(args, repository_id, configuration, report=report)

View File

@ -32,28 +32,6 @@ class Dump(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration",
description="dump configuration for the specified architecture")
parser.add_argument("section", help="filter settings by section", nargs="?")
parser.add_argument("key", help="filter settings by key", nargs="?")
parser.add_argument("--info", help="show additional information, e.g. configuration files",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--secure", help="hide passwords and secrets from output",
action=argparse.BooleanOptionalAction, default=True)
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -81,3 +59,27 @@ class Dump(Handler):
case section, key: # key only case section, key: # key only
value = configuration.get(section, key, fallback="") value = configuration.get(section, key, fallback="")
StringPrinter(value)(verbose=False) StringPrinter(value)(verbose=False)
@staticmethod
def _set_service_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration",
description="dump configuration for the specified architecture")
parser.add_argument("section", help="filter settings by section", nargs="?")
parser.add_argument("key", help="filter settings by key", nargs="?")
parser.add_argument("--info", help="show additional information, e.g. configuration files",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--secure", help="hide passwords and secrets from output",
action=argparse.BooleanOptionalAction, default=True)
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_service_config_parser]

View File

@ -46,6 +46,8 @@ class Handler:
Attributes: Attributes:
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
arguments(list[Callable[[SubParserAction], argparse.ArgumentParser]]): (class attribute) argument parser
methods, which will be called to create command line parsers
Examples: Examples:
Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
@ -57,22 +59,7 @@ class Handler:
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = True ALLOW_MULTI_ARCHITECTURE_RUN = True
arguments: list[Callable[[SubParserAction], argparse.ArgumentParser]]
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
@classmethod @classmethod
def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool: def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool:

View File

@ -31,23 +31,6 @@ class Help(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit")
parser.add_argument("subcommand", help="show help message for specific command", nargs="?")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -65,3 +48,22 @@ class Help(Handler):
parser.parse_args(["--help"]) parser.parse_args(["--help"])
else: else:
parser.parse_args([args.subcommand, "--help"]) parser.parse_args([args.subcommand, "--help"])
@staticmethod
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing help subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit")
parser.add_argument("subcommand", help="show help message for specific command", nargs="?")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
arguments = [_set_help_parser]

View File

@ -32,28 +32,6 @@ class KeyImport(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-key-import", aliases=["key-import"], help="import PGP key",
description="import PGP key from public sources to the repository user",
epilog="By default ahriman runs build process with package sources validation "
"(in case if signature and keys are available in PKGBUILD). This process will "
"fail in case if key is not known for build user. This subcommand can be used "
"in order to import the PGP key to user keychain.")
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(architecture="", lock=None, report=False, repository="")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -68,3 +46,27 @@ class KeyImport(Handler):
""" """
application = Application(repository_id, configuration, report=report) application = Application(repository_id, configuration, report=report)
application.repository.sign.key_import(args.key_server, args.key) application.repository.sign.key_import(args.key_server, args.key)
@staticmethod
def _set_service_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for key import subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-key-import", aliases=["key-import"], help="import PGP key",
description="import PGP key from public sources to the repository user",
epilog="By default ahriman runs build process with package sources validation "
"(in case if signature and keys are available in PKGBUILD). This process will "
"fail in case if key is not known for build user. This subcommand can be used "
"in order to import the PGP key to user keychain.")
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(architecture="", lock=None, report=False, repository="")
return parser
arguments = [_set_service_key_import_parser]

View File

@ -40,24 +40,6 @@ class Patch(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
return [
cls._set_patch_add_parser(root),
cls._set_patch_list_parser(root),
cls._set_patch_remove_parser(root),
cls._set_patch_set_add_parser(root),
]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -267,3 +249,10 @@ class Patch(Handler):
application.reporter.package_patches_remove(package_base, variable) application.reporter.package_patches_remove(package_base, variable)
else: else:
application.reporter.package_patches_remove(package_base, None) # just pass as is application.reporter.package_patches_remove(package_base, None) # just pass as is
arguments = [
_set_patch_add_parser,
_set_patch_list_parser,
_set_patch_remove_parser,
_set_patch_set_add_parser,
]

View File

@ -34,38 +34,6 @@ class Rebuild(Handler):
make world handler make world handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
description="force rebuild whole repository")
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified packages",
action="append")
parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
action="store_true")
parser.add_argument("--from-database",
help="read packages from database instead of filesystem. This feature in particular is "
"required in case if you would like to restore repository from another repository "
"instance. Note, however, that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.",
action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -92,6 +60,38 @@ class Rebuild(Handler):
result = application.update(packages, Packagers(args.username), bump_pkgrel=args.increment) result = application.update(packages, Packagers(args.username), bump_pkgrel=args.increment)
Rebuild.check_status(args.exit_code, not result.is_empty) Rebuild.check_status(args.exit_code, not result.is_empty)
@staticmethod
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository rebuild subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
description="force rebuild whole repository")
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified packages",
action="append")
parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
action="store_true")
parser.add_argument("--from-database",
help="read packages from database instead of filesystem. This feature in particular is "
"required in case if you would like to restore repository from another repository "
"instance. Note, however, that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.",
action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
return parser
@staticmethod @staticmethod
def extract_packages(application: Application, status: BuildStatusEnum | None, *, def extract_packages(application: Application, status: BuildStatusEnum | None, *,
from_database: bool) -> list[Package]: from_database: bool) -> list[Package]:
@ -114,3 +114,5 @@ class Rebuild(Handler):
] ]
return application.repository.packages() return application.repository.packages()
arguments = [_set_repo_rebuild_parser]

View File

@ -30,22 +30,6 @@ class Remove(Handler):
remove packages handler remove packages handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
description="remove package from the repository")
parser.add_argument("package", help="package name or base", nargs="+")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -61,3 +45,21 @@ class Remove(Handler):
application = Application(repository_id, configuration, report=report) application = Application(repository_id, configuration, report=report)
application.on_start() application.on_start()
application.remove(args.package) application.remove(args.package)
@staticmethod
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package removal subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
description="remove package from the repository")
parser.add_argument("package", help="package name or base", nargs="+")
return parser
arguments = [_set_package_remove_parser]

View File

@ -31,22 +31,6 @@ class RemoveUnknown(Handler):
remove unknown packages handler remove unknown packages handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-remove-unknown", aliases=["remove-unknown"], help="remove unknown packages",
description="remove packages which are missing in AUR and do not have local PKGBUILDs")
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -69,3 +53,21 @@ class RemoveUnknown(Handler):
return return
application.remove(unknown_packages) application.remove(unknown_packages)
@staticmethod
def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove unknown packages subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-remove-unknown", aliases=["remove-unknown"], help="remove unknown packages",
description="remove packages which are missing in AUR and do not have local PKGBUILDs")
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
return parser
arguments = [_set_repo_remove_unknown_parser]

View File

@ -32,24 +32,6 @@ class Repositories(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-repositories", help="show repositories",
description="list all available repositories")
parser.add_argument("--id-only", help="show machine readable identifier instead",
action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -70,3 +52,23 @@ class Repositories(Handler):
) )
for repository in cls.repositories_extract(dummy_args): for repository in cls.repositories_extract(dummy_args):
RepositoryPrinter(repository)(verbose=not args.id_only) RepositoryPrinter(repository)(verbose=not args.id_only)
@staticmethod
def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repositories listing
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-repositories", help="show repositories",
description="list all available repositories")
parser.add_argument("--id-only", help="show machine readable identifier instead",
action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return parser
arguments = [_set_service_repositories]

View File

@ -34,24 +34,6 @@ class Restore(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-restore", help="restore repository data",
description="restore settings and database")
parser.add_argument("path", help="path of the input archive", type=Path)
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -66,3 +48,23 @@ class Restore(Handler):
""" """
with tarfile.open(args.path) as archive: with tarfile.open(args.path) as archive:
archive.extractall(path=args.output) # nosec archive.extractall(path=args.output) # nosec
@staticmethod
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository restore subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-restore", help="restore repository data",
description="restore settings and database")
parser.add_argument("path", help="path of the input archive", type=Path)
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return parser
arguments = [_set_repo_restore_parser]

View File

@ -32,25 +32,6 @@ class Run(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-run", aliases=["run"], help="run multiple commands",
description="run multiple commands on success run of the previous command",
epilog="Commands must be quoted by using usual bash rules. Processes will be spawned "
"under the same user as this command.")
parser.add_argument("command", help="command to be run (quoted) without ``ahriman``", nargs="+")
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -68,6 +49,25 @@ class Run(Handler):
status = Run.run_command(shlex.split(command), parser) status = Run.run_command(shlex.split(command), parser)
Run.check_status(True, status) Run.check_status(True, status)
@staticmethod
def _set_service_run(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for multicommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-run", aliases=["run"], help="run multiple commands",
description="run multiple commands on success run of the previous command",
epilog="Commands must be quoted by using usual bash rules. Processes will be spawned "
"under the same user as this command.")
parser.add_argument("command", help="command to be run (quoted) without ``ahriman``", nargs="+")
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
return parser
@staticmethod @staticmethod
def run_command(command: list[str], parser: argparse.ArgumentParser) -> bool: def run_command(command: list[str], parser: argparse.ArgumentParser) -> bool:
""" """
@ -83,3 +83,5 @@ class Run(Handler):
args = parser.parse_args(command) args = parser.parse_args(command)
handler: Handler = args.handler handler: Handler = args.handler
return handler.execute(args) == 0 return handler.execute(args) == 0
arguments = [_set_service_run]

View File

@ -46,33 +46,6 @@ class Search(Handler):
if field.default_factory is not list if field.default_factory is not list
} }
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
description="search for package in AUR using API")
parser.add_argument("search",
help="search terms, can be specified multiple times, the result will match all terms",
nargs="+")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("--sort-by",
help="sort field by this field. In case if two packages have the same value of "
"the specified field, they will be always sorted by name",
default="name", choices=sorted(Search.SORT_FIELDS))
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -95,6 +68,33 @@ class Search(Handler):
for package in Search.sort(packages_list, args.sort_by): for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package)(verbose=args.info) AurPrinter(package)(verbose=args.info)
@staticmethod
def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for AUR search subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
description="search for package in AUR using API")
parser.add_argument("search",
help="search terms, can be specified multiple times, the result will match all terms",
nargs="+")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("--sort-by",
help="sort field by this field. In case if two packages have the same value of "
"the specified field, they will be always sorted by name",
default="name", choices=sorted(Search.SORT_FIELDS))
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
@staticmethod @staticmethod
def sort(packages: Iterable[AURPackage], sort_by: str) -> list[AURPackage]: def sort(packages: Iterable[AURPackage], sort_by: str) -> list[AURPackage]:
""" """
@ -117,3 +117,5 @@ class Search(Handler):
comparator: Callable[[AURPackage], tuple[str, str]] =\ comparator: Callable[[AURPackage], tuple[str, str]] =\
lambda package: (getattr(package, sort_by), package.name) lambda package: (getattr(package, sort_by), package.name)
return sorted(packages, key=comparator) return sorted(packages, key=comparator)
arguments = [_set_aur_search_parser]

View File

@ -34,24 +34,6 @@ class ServiceUpdates(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("help-updates", help="check for service updates",
description="request AUR for current version and compare with current service version")
parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available",
action="store_true")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -76,3 +58,23 @@ class ServiceUpdates(Handler):
UpdatePrinter(remote, local_version)(verbose=True, separator=" -> ") UpdatePrinter(remote, local_version)(verbose=True, separator=" -> ")
ServiceUpdates.check_status(args.exit_code, same_version) ServiceUpdates.check_status(args.exit_code, same_version)
@staticmethod
def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for service update check subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-updates", help="check for service updates",
description="request AUR for current version and compare with current service version")
parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available",
action="store_true")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
arguments = [_set_help_updates_parser]

View File

@ -50,44 +50,6 @@ class Setup(Handler):
MIRRORLIST_PATH = Path("/") / "etc" / "pacman.d" / "mirrorlist" MIRRORLIST_PATH = Path("/") / "etc" / "pacman.d" / "mirrorlist"
SUDOERS_DIR_PATH = Path("/") / "etc" / "sudoers.d" SUDOERS_DIR_PATH = Path("/") / "etc" / "sudoers.d"
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-setup", aliases=["init", "repo-init", "repo-setup", "setup"],
help="initial service configuration",
description="create initial service configuration, requires root",
epilog="Create minimal configuration for the service according to provided options.")
parser.add_argument("--build-as-user", help="force makepkg user to the specific one")
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
type=Path,
default=Path("/") / "usr" / "share" / "devtools" / "pacman.conf.d" / "extra.conf")
parser.add_argument("--generate-salt", help="generate salt for user passwords",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("--makeflags-jobs",
help="append MAKEFLAGS variable with parallelism set to number of cores",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--mirror", help="use the specified explicitly mirror instead of including mirrorlist")
parser.add_argument("--multilib", help="add or do not multilib repository",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--packager", help="packager name and email", required=True)
parser.add_argument("--server", help="server to be used for devtools. If none set, local files will be used")
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=enum_values(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications",
type=Path)
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -120,6 +82,44 @@ class Setup(Handler):
# lazy database sync # lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement application.repository.pacman.handle # pylint: disable=pointless-statement
@staticmethod
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for setup subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-setup", aliases=["init", "repo-init", "repo-setup", "setup"],
help="initial service configuration",
description="create initial service configuration, requires root",
epilog="Create minimal configuration for the service according to provided options.")
parser.add_argument("--build-as-user", help="force makepkg user to the specific one")
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
type=Path,
default=Path("/") / "usr" / "share" / "devtools" / "pacman.conf.d" / "extra.conf")
parser.add_argument("--generate-salt", help="generate salt for user passwords",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("--makeflags-jobs",
help="append MAKEFLAGS variable with parallelism set to number of cores",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--mirror", help="use the specified explicitly mirror instead of including mirrorlist")
parser.add_argument("--multilib", help="add or do not multilib repository",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--packager", help="packager name and email", required=True)
parser.add_argument("--server", help="server to be used for devtools. If none set, local files will be used")
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=enum_values(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications",
type=Path)
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod @staticmethod
def build_command(root: Path, repository_id: RepositoryId) -> Path: def build_command(root: Path, repository_id: RepositoryId) -> Path:
""" """
@ -280,3 +280,5 @@ class Setup(Handler):
command.unlink(missing_ok=True) command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH) command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
paths.chown(command) # we would like to keep owner inside ahriman's home paths.chown(command) # we would like to keep owner inside ahriman's home
arguments = [_set_service_setup_parser]

View File

@ -36,24 +36,6 @@ class Shell(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-shell", aliases=["shell"], help="invoke python shell",
description="drop into python shell")
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
parser.set_defaults(lock=None, report=False)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -81,3 +63,23 @@ class Shell(Handler):
code.interact(local=local_variables) code.interact(local=local_variables)
else: else:
code.InteractiveConsole(locals=local_variables).runcode(args.code) code.InteractiveConsole(locals=local_variables).runcode(args.code)
@staticmethod
def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for shell subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-shell", aliases=["shell"], help="invoke python shell",
description="drop into python shell")
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
parser.set_defaults(lock=None, report=False)
return parser
arguments = [_set_service_shell_parser]

View File

@ -30,23 +30,6 @@ class Sign(Handler):
(re-)sign handler (re-)sign handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
description="(re-)sign packages and repository database according to current settings",
epilog="Sign repository and/or packages as configured.")
parser.add_argument("package", help="sign only specified packages", nargs="*")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -60,3 +43,22 @@ class Sign(Handler):
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
""" """
Application(repository_id, configuration, report=report).sign(args.package) Application(repository_id, configuration, report=report).sign(args.package)
@staticmethod
def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for sign subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
description="(re-)sign packages and repository database according to current settings",
epilog="Sign repository and/or packages as configured.")
parser.add_argument("package", help="sign only specified packages", nargs="*")
return parser
arguments = [_set_repo_sign_parser]

View File

@ -40,30 +40,6 @@ class Statistics(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-statistics", help="repository statistics",
description="fetch repository statistics")
parser.add_argument("package", help="fetch only events for the specified package", nargs="?")
parser.add_argument("--chart", help="create updates chart and save it to the specified path", type=Path)
parser.add_argument("-e", "--event", help="event type filter",
type=EventType, choices=enum_values(EventType), default=EventType.PackageUpdated)
parser.add_argument("--from-date", help="only fetch events which are newer than the date")
parser.add_argument("--limit", help="limit response by specified amount of events", type=int, default=-1)
parser.add_argument("--offset", help="skip specified amount of events", type=int, default=0)
parser.add_argument("--to-date", help="only fetch events which are older than the date")
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -92,6 +68,30 @@ class Statistics(Handler):
case _: case _:
Statistics.stats_for_package(args.event, events, args.chart) Statistics.stats_for_package(args.event, events, args.chart)
@staticmethod
def _set_repo_statistics_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository statistics subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-statistics", help="repository statistics",
description="fetch repository statistics")
parser.add_argument("package", help="fetch only events for the specified package", nargs="?")
parser.add_argument("--chart", help="create updates chart and save it to the specified path", type=Path)
parser.add_argument("-e", "--event", help="event type filter",
type=EventType, choices=enum_values(EventType), default=EventType.PackageUpdated)
parser.add_argument("--from-date", help="only fetch events which are newer than the date")
parser.add_argument("--limit", help="limit response by specified amount of events", type=int, default=-1)
parser.add_argument("--offset", help="skip specified amount of events", type=int, default=0)
parser.add_argument("--to-date", help="only fetch events which are older than the date")
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod @staticmethod
def event_stats(event_type: str, events: list[Event]) -> None: def event_stats(event_type: str, events: list[Event]) -> None:
""" """
@ -192,3 +192,5 @@ class Statistics(Handler):
# chart if enabled # chart if enabled
if chart_path is not None: if chart_path is not None:
Statistics.plot_packages(event_type, by_object_id, chart_path) Statistics.plot_packages(event_type, by_object_id, chart_path)
arguments = [_set_repo_statistics_parser]

View File

@ -38,32 +38,6 @@ class Status(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
description="request status of the package",
epilog="This command requests package status from the web interface "
"if it is available.")
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -95,3 +69,31 @@ class Status(Handler):
lambda item: args.status is None or item[1].status == args.status lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator): for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
PackagePrinter(package, package_status)(verbose=args.info) PackagePrinter(package, package_status)(verbose=args.info)
@staticmethod
def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
description="request status of the package",
epilog="This command requests package status from the web interface "
"if it is available.")
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_package_status_parser]

View File

@ -35,23 +35,6 @@ class StatusUpdate(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
return [
cls._set_package_status_remove_parser(root),
cls._set_package_status_update_parser(root),
cls._set_repo_status_update_parser(root),
]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -136,3 +119,9 @@ class StatusUpdate(Handler):
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success) type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(action=Action.Update, lock=None, package=[], quiet=True, report=False, unsafe=True) parser.set_defaults(action=Action.Update, lock=None, package=[], quiet=True, report=False, unsafe=True)
return parser return parser
arguments = [
_set_package_status_remove_parser,
_set_package_status_update_parser,
_set_repo_status_update_parser,
]

View File

@ -34,24 +34,6 @@ class Structure(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("repo-tree", help="dump repository tree",
description="dump repository tree based on packages dependencies")
parser.add_argument("-p", "--partitions", help="also divide packages by independent partitions",
type=int, default=1)
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -76,3 +58,23 @@ class Structure(Handler):
# empty line # empty line
StringPrinter("")(verbose=False) StringPrinter("")(verbose=False)
@staticmethod
def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository tree subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-tree", help="dump repository tree",
description="dump repository tree based on packages dependencies")
parser.add_argument("-p", "--partitions", help="also divide packages by independent partitions",
type=int, default=1)
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_repo_tree_parser]

View File

@ -30,22 +30,6 @@ class TreeMigrate(Handler):
tree migration handler tree migration handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-tree-migrate", help="migrate repository tree",
description="migrate repository tree between versions")
parser.set_defaults(lock=None, quiet=True, report=False)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -66,6 +50,22 @@ class TreeMigrate(Handler):
# perform migration # perform migration
TreeMigrate.tree_move(current_tree, target_tree) TreeMigrate.tree_move(current_tree, target_tree)
@staticmethod
def _set_service_tree_migrate_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for tree migration subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-tree-migrate", help="migrate repository tree",
description="migrate repository tree between versions")
parser.set_defaults(lock=None, quiet=True, report=False)
return parser
@staticmethod @staticmethod
def tree_move(from_tree: RepositoryPaths, to_tree: RepositoryPaths) -> None: def tree_move(from_tree: RepositoryPaths, to_tree: RepositoryPaths) -> None:
""" """
@ -82,3 +82,5 @@ class TreeMigrate(Handler):
RepositoryPaths.repository, RepositoryPaths.repository,
): ):
attribute.fget(from_tree).rename(attribute.fget(to_tree)) # type: ignore[attr-defined] attribute.fget(from_tree).rename(attribute.fget(to_tree)) # type: ignore[attr-defined]
arguments = [_set_service_tree_migrate_parser]

View File

@ -31,25 +31,6 @@ class Triggers(Handler):
triggers handlers triggers handlers
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
return [
cls._set_repo_create_keyring_parser(root),
cls._set_repo_create_mirrorlist_parser(root),
cls._set_repo_report_parser(root),
cls._set_repo_sync_parser(root),
cls._set_repo_triggers_parser(root),
]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -155,3 +136,11 @@ class Triggers(Handler):
parser.add_argument("trigger", help="instead of running all triggers as set by configuration, just process " parser.add_argument("trigger", help="instead of running all triggers as set by configuration, just process "
"specified ones in order of mention", nargs="*") "specified ones in order of mention", nargs="*")
return parser return parser
arguments = [
_set_repo_create_keyring_parser,
_set_repo_create_mirrorlist_parser,
_set_repo_report_parser,
_set_repo_sync_parser,
_set_repo_triggers_parser,
]

View File

@ -32,25 +32,6 @@ class UnsafeCommands(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args")
parser.add_argument("subcommand",
help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -71,6 +52,25 @@ class UnsafeCommands(Handler):
for command in unsafe_commands: for command in unsafe_commands:
StringPrinter(command)(verbose=True) StringPrinter(command)(verbose=True)
@staticmethod
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing unsafe commands
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args")
parser.add_argument("subcommand",
help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
@staticmethod @staticmethod
def check_unsafe(command: list[str], unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None: def check_unsafe(command: list[str], unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None:
""" """
@ -100,3 +100,5 @@ class UnsafeCommands(Handler):
subparser = next((action for action in parser._actions if isinstance(action, argparse._SubParsersAction)), None) subparser = next((action for action in parser._actions if isinstance(action, argparse._SubParsersAction)), None)
actions = subparser.choices if subparser is not None else {} actions = subparser.choices if subparser is not None else {}
return sorted(action_name for action_name, action in actions.items() if action.get_default("unsafe")) return sorted(action_name for action_name, action in actions.items() if action.get_default("unsafe"))
arguments = [_set_help_commands_unsafe_parser]

View File

@ -34,19 +34,6 @@ class Update(Handler):
package update handler package update handler
""" """
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
return [cls._set_repo_check_parser(root), cls._set_repo_update_parser(root)]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -165,3 +152,8 @@ class Update(Handler):
def inner(line: str) -> None: def inner(line: str) -> None:
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
return inner return inner
arguments = [
_set_repo_check_parser,
_set_repo_update_parser,
]

View File

@ -39,23 +39,6 @@ class Users(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
return [
cls._set_user_add_parser(root),
cls._set_user_list_parser(root),
cls._set_user_remove_parser(root),
]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -178,3 +161,9 @@ class Users(Handler):
return User(username=args.username, password=password, access=args.role, return User(username=args.username, password=password, access=args.role,
packager_id=args.packager, key=args.key) packager_id=args.packager, key=args.key)
arguments = [
_set_user_add_parser,
_set_user_list_parser,
_set_user_remove_parser,
]

View File

@ -38,25 +38,6 @@ class Validate(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("service-config-validate", aliases=["config-validate", "repo-config-validate"],
help="validate system configuration",
description="validate configuration and print found errors")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if configuration is invalid",
action="store_true")
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -82,6 +63,25 @@ class Validate(Handler):
# as we reach this part it means that we always have errors # as we reach this part it means that we always have errors
Validate.check_status(args.exit_code, False) Validate.check_status(args.exit_code, False)
@staticmethod
def _set_service_config_validate_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config validation subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-config-validate", aliases=["config-validate", "repo-config-validate"],
help="validate system configuration",
description="validate configuration and print found errors")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if configuration is invalid",
action="store_true")
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod @staticmethod
def schema(repository_id: RepositoryId, configuration: Configuration) -> ConfigurationSchema: def schema(repository_id: RepositoryId, configuration: Configuration) -> ConfigurationSchema:
""" """
@ -155,3 +155,5 @@ class Validate(Handler):
Validate.schema_merge(value, schema[key]) Validate.schema_merge(value, schema[key])
return schema return schema
arguments = [_set_service_config_validate_parser]

View File

@ -42,22 +42,6 @@ class Versions(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+") PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+")
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("help-version", aliases=["version"], help="application version",
description="print application and its dependencies versions")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -75,6 +59,22 @@ class Versions(Handler):
packages = Versions.package_dependencies("ahriman") packages = Versions.package_dependencies("ahriman")
VersionPrinter("Installed packages", dict(packages))(verbose=False, separator=" ") VersionPrinter("Installed packages", dict(packages))(verbose=False, separator=" ")
@staticmethod
def _set_help_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for version subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-version", aliases=["version"], help="application version",
description="print application and its dependencies versions")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
@staticmethod @staticmethod
def package_dependencies(root: str) -> Generator[tuple[str, str], None, None]: def package_dependencies(root: str) -> Generator[tuple[str, str], None, None]:
""" """
@ -112,3 +112,5 @@ class Versions(Handler):
yield distribution.name, distribution.version yield distribution.name, distribution.version
except metadata.PackageNotFoundError: except metadata.PackageNotFoundError:
continue continue
arguments = [_set_help_version_parser]

View File

@ -36,21 +36,6 @@ class Web(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def arguments(cls, root: SubParserAction) -> list[argparse.ArgumentParser]:
"""
add parser(s) for the subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
list[argparse.ArgumentParser]: created argument parser
"""
parser = root.add_parser("web", help="web server", description="start web server")
parser.set_defaults(architecture="", lock=Path("ahriman-web.pid"), report=False, repository="")
return [parser]
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None: report: bool) -> None:
@ -87,6 +72,21 @@ class Web(Handler):
spawner.stop() spawner.stop()
spawner.join() spawner.join()
@staticmethod
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("web", help="web server", description="start web server")
parser.set_defaults(architecture="", lock=Path("ahriman-web.pid"), report=False, repository="")
return parser
@staticmethod @staticmethod
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Generator[str, None, None]: def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Generator[str, None, None]:
""" """
@ -116,3 +116,5 @@ class Web(Handler):
# arguments from configuration # arguments from configuration
if (wait_timeout := configuration.getint("web", "wait_timeout", fallback=None)) is not None: if (wait_timeout := configuration.getint("web", "wait_timeout", fallback=None)) is not None:
yield from ["--wait-timeout", str(wait_timeout)] yield from ["--wait-timeout", str(wait_timeout)]
arguments = [_set_web_parser]

View File

@ -17,8 +17,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import bcrypt
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from passlib.hash import sha512_crypt
from secrets import token_urlsafe as generate_password from secrets import token_urlsafe as generate_password
from typing import Self from typing import Self
@ -67,8 +68,6 @@ class User:
packager_id: str | None = None packager_id: str | None = None
key: str | None = None key: str | None = None
_HASHER = sha512_crypt
def __post_init__(self) -> None: def __post_init__(self) -> None:
""" """
remove empty fields remove empty fields
@ -101,10 +100,9 @@ class User:
bool: ``True`` in case if password matches, ``False`` otherwise bool: ``True`` in case if password matches, ``False`` otherwise
""" """
try: try:
verified: bool = self._HASHER.verify(password + salt, self.password) return bcrypt.checkpw((password + salt).encode("utf8"), self.password.encode("utf8"))
except ValueError: except ValueError:
verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy return False # the absence of evidence is not the evidence of absence (c) Gin Rummy
return verified
def hash_password(self, salt: str) -> Self: def hash_password(self, salt: str) -> Self:
""" """
@ -120,8 +118,8 @@ class User:
# in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider # in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider
# when we do not store any password here # when we do not store any password here
return self return self
password_hash: str = self._HASHER.hash(self.password + salt) password_hash = bcrypt.hashpw((self.password + salt).encode("utf8"), bcrypt.gensalt())
return replace(self, password=password_hash) return replace(self, password=password_hash.decode("utf8"))
def verify_access(self, required: UserAccess) -> bool: def verify_access(self, required: UserAccess) -> bool:
""" """

View File

@ -3,7 +3,6 @@ import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -12,14 +11,6 @@ from ahriman.models.log_handler import LogHandler
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
def test_arguments() -> None:
"""
must raise NotImplemented for missing arguments method
"""
with pytest.raises(NotImplementedError):
Handler.arguments(MagicMock())
def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must call inside lock must call inside lock