ybox 0.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ybox/__init__.py +2 -0
- ybox/cmd.py +307 -0
- ybox/conf/completions/ybox.fish +93 -0
- ybox/conf/distros/arch/add-gpg-key.sh +29 -0
- ybox/conf/distros/arch/distro.ini +192 -0
- ybox/conf/distros/arch/init-base.sh +10 -0
- ybox/conf/distros/arch/init-user.sh +35 -0
- ybox/conf/distros/arch/init.sh +82 -0
- ybox/conf/distros/arch/list_fmt_long.py +76 -0
- ybox/conf/distros/arch/pkgdeps.py +276 -0
- ybox/conf/distros/deb-generic/check-package.sh +77 -0
- ybox/conf/distros/deb-generic/distro.ini +190 -0
- ybox/conf/distros/deb-generic/fetch-gpg-key-id.sh +30 -0
- ybox/conf/distros/deb-generic/init-base.sh +11 -0
- ybox/conf/distros/deb-generic/init-user.sh +3 -0
- ybox/conf/distros/deb-generic/init.sh +136 -0
- ybox/conf/distros/deb-generic/list_fmt_long.py +114 -0
- ybox/conf/distros/deb-generic/pkgdeps.py +208 -0
- ybox/conf/distros/deb-oldstable/distro.ini +21 -0
- ybox/conf/distros/deb-stable/distro.ini +21 -0
- ybox/conf/distros/supported.list +5 -0
- ybox/conf/distros/ubuntu2204/distro.ini +21 -0
- ybox/conf/distros/ubuntu2404/distro.ini +21 -0
- ybox/conf/profiles/apps.ini +26 -0
- ybox/conf/profiles/basic.ini +310 -0
- ybox/conf/profiles/dev.ini +25 -0
- ybox/conf/profiles/games.ini +39 -0
- ybox/conf/resources/entrypoint-base.sh +170 -0
- ybox/conf/resources/entrypoint-common.sh +23 -0
- ybox/conf/resources/entrypoint-cp.sh +32 -0
- ybox/conf/resources/entrypoint-root.sh +20 -0
- ybox/conf/resources/entrypoint-user.sh +21 -0
- ybox/conf/resources/entrypoint.sh +249 -0
- ybox/conf/resources/prime-run +13 -0
- ybox/conf/resources/run-in-dir +60 -0
- ybox/conf/resources/run-user-bash-cmd +14 -0
- ybox/config.py +255 -0
- ybox/env.py +205 -0
- ybox/filelock.py +77 -0
- ybox/migrate/0.9.0-0.9.7:0.9.8.py +33 -0
- ybox/pkg/__init__.py +0 -0
- ybox/pkg/clean.py +33 -0
- ybox/pkg/info.py +40 -0
- ybox/pkg/inst.py +638 -0
- ybox/pkg/list.py +191 -0
- ybox/pkg/mark.py +68 -0
- ybox/pkg/repair.py +150 -0
- ybox/pkg/repo.py +251 -0
- ybox/pkg/search.py +52 -0
- ybox/pkg/uninst.py +92 -0
- ybox/pkg/update.py +56 -0
- ybox/print.py +121 -0
- ybox/run/__init__.py +0 -0
- ybox/run/cmd.py +54 -0
- ybox/run/control.py +102 -0
- ybox/run/create.py +1116 -0
- ybox/run/destroy.py +64 -0
- ybox/run/graphics.py +367 -0
- ybox/run/logs.py +57 -0
- ybox/run/ls.py +64 -0
- ybox/run/pkg.py +445 -0
- ybox/schema/0.9.1-added.sql +27 -0
- ybox/schema/0.9.6-added.sql +18 -0
- ybox/schema/init.sql +39 -0
- ybox/schema/migrate/0.9.0:0.9.1.sql +42 -0
- ybox/schema/migrate/0.9.1:0.9.2.sql +8 -0
- ybox/schema/migrate/0.9.2:0.9.3.sql +2 -0
- ybox/schema/migrate/0.9.5:0.9.6.sql +2 -0
- ybox/state.py +914 -0
- ybox/util.py +351 -0
- ybox-0.9.8.dist-info/LICENSE +19 -0
- ybox-0.9.8.dist-info/METADATA +533 -0
- ybox-0.9.8.dist-info/RECORD +76 -0
- ybox-0.9.8.dist-info/WHEEL +5 -0
- ybox-0.9.8.dist-info/entry_points.txt +8 -0
- ybox-0.9.8.dist-info/top_level.txt +1 -0
ybox/pkg/list.py
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
"""
|
2
|
+
List packages or package files on an active ybox container.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import re
|
7
|
+
import sys
|
8
|
+
from configparser import SectionProxy
|
9
|
+
from typing import Callable
|
10
|
+
|
11
|
+
from ybox.cmd import PkgMgr, page_command
|
12
|
+
from ybox.config import Consts, StaticConfiguration
|
13
|
+
from ybox.print import fgcolor as fg
|
14
|
+
from ybox.print import print_warn
|
15
|
+
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
16
|
+
from ybox.util import FormatTable
|
17
|
+
|
18
|
+
|
19
|
+
def list_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
20
|
+
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
21
|
+
state: YboxStateManagement) -> int:
|
22
|
+
"""
|
23
|
+
List packages installed in a container including those not managed by `ybox-pkg`, if required.
|
24
|
+
Some package details can also be listed like the version and whether a package has been
|
25
|
+
installed as a dependency or is a top-level package.
|
26
|
+
|
27
|
+
When multiple containers share the same root directory, then listing all packages will include
|
28
|
+
those installed from other containers, if any.
|
29
|
+
|
30
|
+
:param args: arguments having all attributes passed by the user
|
31
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
32
|
+
:param docker_cmd: the podman/docker executable to use
|
33
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
34
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
35
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
36
|
+
:return: integer exit status of list packages command where 0 represents success
|
37
|
+
"""
|
38
|
+
plain_sep: str = args.plain_separator
|
39
|
+
separator = plain_sep or Consts.default_field_separator()
|
40
|
+
if args.os_pkgs:
|
41
|
+
# package list and details will all be fetched using distribution's package manager
|
42
|
+
if args.verbose:
|
43
|
+
list_cmd = pkgmgr[PkgMgr.LIST_ALL_LONG.value] if args.all else pkgmgr[
|
44
|
+
PkgMgr.LIST_LONG.value]
|
45
|
+
else:
|
46
|
+
list_cmd = pkgmgr[PkgMgr.LIST_ALL.value] if args.all else pkgmgr[PkgMgr.LIST.value]
|
47
|
+
list_cmd = list_cmd.format(packages="", separator=separator)
|
48
|
+
if shared_containers := state.get_other_shared_containers(conf.box_name,
|
49
|
+
runtime_conf.shared_root):
|
50
|
+
print_warn("Package listing will include packages from other containers sharing the "
|
51
|
+
f"same root directory: {', '.join(shared_containers)}", file=sys.stderr)
|
52
|
+
else:
|
53
|
+
# package list will be fetched from the state database while the details, if required,
|
54
|
+
# will be fetched using the distribution's package manager
|
55
|
+
dependency_type = ".*" if args.all else ""
|
56
|
+
packages = " ".join(state.get_packages(conf.box_name, dependency_type=dependency_type))
|
57
|
+
if not packages:
|
58
|
+
return 0
|
59
|
+
# TODO: optional dependencies from state database should also be shown since those
|
60
|
+
# can be different from the package manager
|
61
|
+
list_cmd = pkgmgr[PkgMgr.LIST_ALL_LONG.value] if args.verbose else pkgmgr[
|
62
|
+
PkgMgr.LIST_ALL.value]
|
63
|
+
list_cmd = list_cmd.format(packages=packages, separator=separator)
|
64
|
+
|
65
|
+
docker_args = [docker_cmd, "exec"]
|
66
|
+
if sys.stdout.isatty(): # don't act as a terminal if it is being redirected
|
67
|
+
docker_args.append("-it")
|
68
|
+
docker_args.extend([conf.box_name, "/bin/bash", "-c", list_cmd])
|
69
|
+
# empty pager argument is a valid one and indicates no pagination, hence the `is None` check
|
70
|
+
pager: str = (args.pager or "") if args.pager is not None or plain_sep else conf.pager
|
71
|
+
headers = ["Name", "Version", "Dependency Of (req,opt)",
|
72
|
+
"Description"] if args.verbose else ["Name", "Version"]
|
73
|
+
transform = None if plain_sep else _build_table_transform(args, separator, headers)
|
74
|
+
header = plain_sep.join(headers) + "\n" if plain_sep else ""
|
75
|
+
return page_command(docker_args, pager, "listing packages", transform, header)
|
76
|
+
|
77
|
+
|
78
|
+
def _build_table_transform(args: argparse.Namespace, separator: str,
|
79
|
+
headers: list[str]) -> Callable[[str], str]:
|
80
|
+
"""
|
81
|
+
Return a transformation function that takes the output of the underlying package manager
|
82
|
+
(invoked using podman/docker exec) and returns a string of table format layout appropriate
|
83
|
+
for display on a terminal.
|
84
|
+
|
85
|
+
:param args: the parsed arguments passed to the invoking script
|
86
|
+
:param separator: the separator used between the fields
|
87
|
+
:param headers: list of header strings to use for the columns
|
88
|
+
:return: a transformation function that takes input string having the output from package
|
89
|
+
manager and returns a string where the fields are formatted as a table appropriate
|
90
|
+
for display in a terminal
|
91
|
+
"""
|
92
|
+
def as_table(s: str) -> str:
|
93
|
+
"""
|
94
|
+
Format output from the package manager having <name> and <version> fields as a table
|
95
|
+
string for display in a terminal.
|
96
|
+
"""
|
97
|
+
table = (line.split(separator, maxsplit=1) for line in s.splitlines())
|
98
|
+
colors = (fg.lightgray, fg.orange)
|
99
|
+
# outline formats like 'rounded_outline' would be preferable, but unfortunately they
|
100
|
+
# are broken for multiline values, hence using the relatively better looking format
|
101
|
+
# from the non-broken ones
|
102
|
+
return FormatTable(table, headers, colors, "psql", (1.0, 1.0)).show()
|
103
|
+
|
104
|
+
def as_long_table(s: str) -> str:
|
105
|
+
"""
|
106
|
+
Format output from the package manager having <name>, <version>, <dependency of>
|
107
|
+
and <description> fields as a table string for display in a terminal.
|
108
|
+
"""
|
109
|
+
dep_of_idx = 2
|
110
|
+
# add color to "Dependency Of" header to indicate the color scheme used in values
|
111
|
+
headers[dep_of_idx] = f"Dependency Of ({fg.purple}req {fg.cyan}opt{fg.reset})"
|
112
|
+
colors = (fg.lightgray, fg.orange, fg.reset, fg.blue)
|
113
|
+
table_fmt = FormatTable((), headers, colors, "rounded_grid", (2.0, 2.0, 3.0, 3.0))
|
114
|
+
# build the table data which uses the "Dependency Of" column width to decide on how
|
115
|
+
# to truncate the various "req" and "opt" dependency lists
|
116
|
+
table_fmt.table = (_format_long_line(line, separator, table_fmt.max_col_widths[dep_of_idx],
|
117
|
+
args.no_trunc) for line in s.splitlines())
|
118
|
+
return table_fmt.show()
|
119
|
+
|
120
|
+
return as_long_table if args.verbose else as_table
|
121
|
+
|
122
|
+
|
123
|
+
# regex that matches the values in the "Dependency Of" column
|
124
|
+
_DEP_OF_RE = re.compile(r"(req\((?P<req>[^)]+)\))?,?(opt\((?P<opt>.+)\))?")
|
125
|
+
|
126
|
+
|
127
|
+
def _format_long_line(line: str, separator: str, dep_of_width: int,
|
128
|
+
no_trunc: bool) -> tuple[str, str, str, str]:
|
129
|
+
"""
|
130
|
+
Format the `Dependency Of` column to include the required and optional dependencies while the
|
131
|
+
other three fields are returned as is after extraction from the given `line`.
|
132
|
+
|
133
|
+
:param line: a line of output from the package manager which is expected to be of the form:
|
134
|
+
<name>{separator}<version>{separator}<dependency of>{separator}<description>
|
135
|
+
:param separator: the separator used between the fields
|
136
|
+
:param dep_of_width: the final calculated display width of the "Dependency Of" column
|
137
|
+
:param no_trunc: if True then do not truncate the 'Dependency Of' column value
|
138
|
+
:return: tuple of formatted (<name>, <version>, <dependency of>, <description>) fields
|
139
|
+
"""
|
140
|
+
name, version, dep_of, description = line.split(separator, maxsplit=3)
|
141
|
+
# replace literal "\n" with newline
|
142
|
+
description = description.replace(r"\n", "\n")
|
143
|
+
if dep_of and (dep_of_match := _DEP_OF_RE.fullmatch(dep_of)):
|
144
|
+
dep_of_dict = dep_of_match.groupdict()
|
145
|
+
req_by = dep_of_dict.get("req") or ""
|
146
|
+
opt_for = dep_of_dict.get("opt") or ""
|
147
|
+
# description is not trimmed and is shown multi-line, so use its size as an upper limit
|
148
|
+
if not no_trunc and len(dep_of) > (max_width := max(dep_of_width, len(description))):
|
149
|
+
trim_factor = (len(dep_of) - max_width) / float(len(req_by) + len(opt_for))
|
150
|
+
if req_by:
|
151
|
+
trim_size = int(trim_factor * len(req_by) + 0.5) # floor to nearest int
|
152
|
+
req_by = req_by[:max(0, len(req_by) - trim_size - 3)] + "..."
|
153
|
+
if opt_for:
|
154
|
+
trim_size = int(trim_factor * len(opt_for) + 0.5) # floor to nearest int
|
155
|
+
opt_for = opt_for[:max(0, len(opt_for) - trim_size - 3)] + "..."
|
156
|
+
dep_of_parts: list[str] = []
|
157
|
+
if req_by:
|
158
|
+
dep_of_parts.extend((fg.purple, "req(", req_by, ")", fg.reset))
|
159
|
+
if opt_for:
|
160
|
+
if req_by:
|
161
|
+
dep_of_parts.append(" ")
|
162
|
+
dep_of_parts.extend((fg.cyan, "opt(", opt_for, ")", fg.reset))
|
163
|
+
return name, version, "".join(dep_of_parts), description
|
164
|
+
return name, version, dep_of, description
|
165
|
+
|
166
|
+
|
167
|
+
# noinspection PyUnusedLocal
|
168
|
+
def list_files(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
169
|
+
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
170
|
+
state: YboxStateManagement) -> int:
|
171
|
+
# pylint: disable=unused-argument
|
172
|
+
"""
|
173
|
+
List the files of a package installed in a container including those not managed by `ybox-pkg`.
|
174
|
+
|
175
|
+
:param args: arguments having `package` and all other attributes passed by the user
|
176
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
177
|
+
:param docker_cmd: the podman/docker executable to use
|
178
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
179
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
180
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
181
|
+
:return: integer exit status of list package files command where 0 represents success
|
182
|
+
"""
|
183
|
+
package: str = args.package
|
184
|
+
list_cmd = pkgmgr[PkgMgr.LIST_FILES.value]
|
185
|
+
docker_args = [docker_cmd, "exec"]
|
186
|
+
if sys.stdout.isatty(): # don't act as a terminal if it is being redirected
|
187
|
+
docker_args.append("-it")
|
188
|
+
docker_args.extend([conf.box_name, "/bin/bash", "-c", list_cmd.format(package=package)])
|
189
|
+
# empty pager argument is a valid one and indicates no pagination, hence the `is None` check
|
190
|
+
pager: str = args.pager if args.pager is not None else conf.pager
|
191
|
+
return page_command(docker_args, pager, error_msg=f"listing files of '{package}'")
|
ybox/pkg/mark.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
"""
|
2
|
+
Mark a package as a dependency or as explicitly installed.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
from configparser import SectionProxy
|
7
|
+
|
8
|
+
from ybox.cmd import PkgMgr, build_shell_command, run_command
|
9
|
+
from ybox.config import StaticConfiguration
|
10
|
+
from ybox.print import print_error, print_info
|
11
|
+
from ybox.state import (CopyType, DependencyType, RuntimeConfiguration,
|
12
|
+
YboxStateManagement)
|
13
|
+
from ybox.util import check_package
|
14
|
+
|
15
|
+
|
16
|
+
def mark_package(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
17
|
+
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
18
|
+
state: YboxStateManagement) -> int:
|
19
|
+
"""
|
20
|
+
Mark a package as a dependency of another package or an explicitly installed one.
|
21
|
+
|
22
|
+
:param args: arguments having `package` and all other attributes passed by the user
|
23
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
24
|
+
:param docker_cmd: the podman/docker executable to use
|
25
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
26
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
27
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
28
|
+
:return: integer exit status of mark package command where 0 represents success
|
29
|
+
"""
|
30
|
+
mark_explicit: bool = args.explicit
|
31
|
+
mark_dependency_of: str = args.dependency_of or ""
|
32
|
+
if not mark_explicit ^ bool(mark_dependency_of):
|
33
|
+
print_error("ybox-pkg mark: exactly one of -e or -D option must be specified "
|
34
|
+
f"(explicit={mark_explicit}, dependency-of={mark_dependency_of})")
|
35
|
+
return 1
|
36
|
+
# check that the package(s) are installed and replace with actual installed name
|
37
|
+
check_cmd = pkgmgr[PkgMgr.CHECK_INSTALL.value]
|
38
|
+
all_packages = [str(args.package)]
|
39
|
+
if mark_dependency_of:
|
40
|
+
all_packages.insert(0, mark_dependency_of) # keep the non-dependent package at the front
|
41
|
+
for idx, package in enumerate(all_packages):
|
42
|
+
code, inst_pkgs = check_package(docker_cmd, check_cmd, package, conf.box_name)
|
43
|
+
if code != 0:
|
44
|
+
print_error(f"Package '{package}' is not installed in container '{conf.box_name}'")
|
45
|
+
return 1
|
46
|
+
all_packages[idx] = inst_pkgs[0]
|
47
|
+
# make entries in the state database if the packages are not present
|
48
|
+
package = all_packages[0]
|
49
|
+
state.register_package(conf.box_name, package, [], CopyType(0), {},
|
50
|
+
runtime_conf.shared_root, None, "", skip_if_exists=True)
|
51
|
+
if mark_dependency_of:
|
52
|
+
# currently "optional" is the only supported dependency type
|
53
|
+
print_info(f"Marking '{all_packages[1]}' as an optional dependency of '{package}'")
|
54
|
+
state.register_package(conf.box_name, all_packages[1], [], CopyType(0), {},
|
55
|
+
runtime_conf.shared_root, dep_type=DependencyType.OPTIONAL,
|
56
|
+
dep_of=package, skip_if_exists=True)
|
57
|
+
# the package may or may not be a dependency in the underlying packaging, so don't mark
|
58
|
+
# at the package manager level which may cause the package to be orphaned and auto-removed
|
59
|
+
else:
|
60
|
+
print_info(f"Marking '{package}' as explicitly installed")
|
61
|
+
# remove any dependency entries for this package to mark it as explicitly installed
|
62
|
+
state.unregister_dependency(conf.box_name, "%", package)
|
63
|
+
# also mark as explicitly installed using the underlying package manager
|
64
|
+
mark_cmd = pkgmgr[PkgMgr.MARK_EXPLICIT.value]
|
65
|
+
return int(run_command(build_shell_command(
|
66
|
+
docker_cmd, conf.box_name, mark_cmd.format(package=package)), exit_on_error=False,
|
67
|
+
error_msg=f"marking '{package}' as explicitly installed"))
|
68
|
+
return 0
|
ybox/pkg/repair.py
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
"""
|
2
|
+
Try to repair packages and state after a failed package operation or an interrupt/kill.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import subprocess
|
7
|
+
import time
|
8
|
+
from configparser import SectionProxy
|
9
|
+
|
10
|
+
from ybox.cmd import (PkgMgr, build_shell_command, check_active_ybox,
|
11
|
+
run_command)
|
12
|
+
from ybox.config import StaticConfiguration
|
13
|
+
from ybox.print import (fgcolor, print_color, print_error, print_info,
|
14
|
+
print_warn)
|
15
|
+
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
16
|
+
|
17
|
+
|
18
|
+
def repair_package_state(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
19
|
+
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
20
|
+
state: YboxStateManagement) -> int:
|
21
|
+
"""
|
22
|
+
Try to repair packages and state after a failed package operation or an interrupt/kill
|
23
|
+
or dangling package manager processes and/or locks.
|
24
|
+
|
25
|
+
:param args: arguments having all attributes passed by the user
|
26
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
27
|
+
:param docker_cmd: the podman/docker executable to use
|
28
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
29
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
30
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
31
|
+
:return: integer exit status of repair command where 0 represents success
|
32
|
+
"""
|
33
|
+
quiet: bool = args.quiet
|
34
|
+
quiet_flag = pkgmgr[PkgMgr.QUIET_FLAG.value] if quiet else ""
|
35
|
+
# find all the containers sharing the same shared root
|
36
|
+
if runtime_conf.shared_root:
|
37
|
+
containers = [c for c in state.get_containers(shared_root=runtime_conf.shared_root)
|
38
|
+
if check_active_ybox(docker_cmd, c)]
|
39
|
+
else:
|
40
|
+
containers = [conf.box_name]
|
41
|
+
# first check for active package operations across all containers on the same shared root
|
42
|
+
# and kill them
|
43
|
+
if not _kill_processes(pkgmgr, docker_cmd, containers, quiet):
|
44
|
+
return 1
|
45
|
+
# remove any package manager related dangling locks
|
46
|
+
_remove_locks(pkgmgr, docker_cmd, containers, quiet)
|
47
|
+
|
48
|
+
# run package manager to repair any failed package operations
|
49
|
+
repair_cmd = pkgmgr[PkgMgr.REPAIR.value]
|
50
|
+
if args.extensive:
|
51
|
+
resp = "y" if quiet else input("Repair thoroughly by reinstalling packages? (y/N) ")
|
52
|
+
if resp.strip().lower() == "y":
|
53
|
+
repair_cmd = pkgmgr[PkgMgr.REPAIR_ALL.value]
|
54
|
+
if (code := int(run_command(build_shell_command(
|
55
|
+
docker_cmd, conf.box_name, repair_cmd.format(quiet=quiet_flag)), exit_on_error=False,
|
56
|
+
error_msg="repairing packages"))) != 0:
|
57
|
+
return code
|
58
|
+
|
59
|
+
# finally restart containers after user confirmation
|
60
|
+
resp = "y" if quiet else input(f"Restart container(s) {containers}? (y/N) ")
|
61
|
+
if resp.strip().lower() == "y":
|
62
|
+
for container in containers:
|
63
|
+
print_color(f"Restarting ybox container '{container}'", fg=fgcolor.cyan)
|
64
|
+
if run_command([docker_cmd, "container", "stop", container],
|
65
|
+
exit_on_error=False, error_msg="container stop") == 0:
|
66
|
+
time.sleep(2)
|
67
|
+
run_command([docker_cmd, "container", "start", container],
|
68
|
+
exit_on_error=False, error_msg="container start")
|
69
|
+
return 0
|
70
|
+
|
71
|
+
|
72
|
+
def _kill_processes(pkgmgr: SectionProxy, docker_cmd: str, containers: list[str],
|
73
|
+
quiet: bool) -> bool:
|
74
|
+
"""
|
75
|
+
Kill any package manager related processes after user confirmation (if required).
|
76
|
+
|
77
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
78
|
+
:param docker_cmd: the podman/docker executable to use
|
79
|
+
:param containers: list of affected containers having the same shared root
|
80
|
+
:param quiet: if True then skip user confirmation before removing any lock files
|
81
|
+
:return: true for success, else false in case of failure to kill one or more processes
|
82
|
+
"""
|
83
|
+
processes_pattern = pkgmgr[PkgMgr.PROCESSES_PATTERN.value]
|
84
|
+
for container in containers:
|
85
|
+
print_info(
|
86
|
+
f"Checking for active package manager related processes in container '{container}'")
|
87
|
+
ps_result = run_command([docker_cmd, "exec", container, "/usr/bin/pgrep", "-fa",
|
88
|
+
processes_pattern], capture_output=True, exit_on_error=False,
|
89
|
+
error_msg="SKIP")
|
90
|
+
if isinstance(ps_result, int) or not (processes := ps_result.splitlines()):
|
91
|
+
continue
|
92
|
+
# confirm with user before killing those processes
|
93
|
+
print_color("Following active package manager related processes were found in "
|
94
|
+
f"container '{container}':", fgcolor.cyan)
|
95
|
+
for process in processes:
|
96
|
+
pid, _, cmd = process.partition(" ")
|
97
|
+
print(f" [PID={pid}] {cmd}")
|
98
|
+
resp = "y" if quiet else input("Kill the above processes? (y/N/space separated PIDs) ")
|
99
|
+
resp = resp.strip().lower()
|
100
|
+
if (not resp) or resp == "n":
|
101
|
+
continue
|
102
|
+
if resp == "y":
|
103
|
+
pids = [process.partition(" ")[0] for process in processes]
|
104
|
+
else:
|
105
|
+
pids = resp.split()
|
106
|
+
for sig in ("-INT", "-TERM", "-KILL"):
|
107
|
+
print_warn(f"Sending {sig} signal to {' '.join(pids)} in container '{container}'")
|
108
|
+
docker_args = [docker_cmd, "exec", container, "/usr/bin/sudo", "/bin/kill", sig]
|
109
|
+
docker_args.extend(pids)
|
110
|
+
subprocess.run(docker_args, check=False, stderr=subprocess.DEVNULL)
|
111
|
+
time.sleep(2)
|
112
|
+
# filter out only the processes that are still active
|
113
|
+
check_res = run_command([docker_cmd, "exec", "/usr/bin/ps", "-o", "pid=", "-p",
|
114
|
+
",".join(pids)], capture_output=True, exit_on_error=False,
|
115
|
+
error_msg="SKIP")
|
116
|
+
if isinstance(check_res, int) or not (pids := check_res.split()):
|
117
|
+
break
|
118
|
+
if pids:
|
119
|
+
print_error(f"Unable to kill {' '.join(pids)} in container '{container}'. You may "
|
120
|
+
"need to manually check inside with 'ybox-cmd' or restart the container")
|
121
|
+
return False
|
122
|
+
return True
|
123
|
+
|
124
|
+
|
125
|
+
def _remove_locks(pkgmgr: SectionProxy, docker_cmd: str, containers: list[str],
|
126
|
+
quiet: bool) -> None:
|
127
|
+
"""
|
128
|
+
Remove any package manager related lock files after user confirmation (if required).
|
129
|
+
|
130
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
131
|
+
:param docker_cmd: the podman/docker executable to use
|
132
|
+
:param containers: list of affected containers having the same shared root
|
133
|
+
:param quiet: if True then skip user confirmation before removing any lock files
|
134
|
+
"""
|
135
|
+
locks_pattern = pkgmgr[PkgMgr.LOCKS_PATTERN.value]
|
136
|
+
ls_cmd = f"/bin/ls {locks_pattern.replace(',', ' ')} 2>/dev/null"
|
137
|
+
for container in containers:
|
138
|
+
print_info(f"Checking for lock files in container '{container}'")
|
139
|
+
ls_result = subprocess.run(build_shell_command(docker_cmd, container, ls_cmd),
|
140
|
+
check=False, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
141
|
+
# confirm with user before removing locks
|
142
|
+
if not (locks := ls_result.stdout.decode("utf-8").split()):
|
143
|
+
continue
|
144
|
+
print_color(f"Found existing lock file(s) {locks} in container '{container}'",
|
145
|
+
fgcolor.cyan)
|
146
|
+
resp = "y" if quiet else input("Remove the above lock file(s)? (y/N) ")
|
147
|
+
if resp.strip().lower() == "y":
|
148
|
+
docker_args = [docker_cmd, "exec", container, "/usr/bin/sudo", "/bin/rm"]
|
149
|
+
docker_args.extend(locks)
|
150
|
+
run_command(docker_args, exit_on_error=False, error_msg="removing lock files")
|
ybox/pkg/repo.py
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
"""
|
2
|
+
Methods for repository management including adding, removing and listing repositories.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import re
|
7
|
+
import subprocess
|
8
|
+
import sys
|
9
|
+
from configparser import SectionProxy
|
10
|
+
from typing import Iterable, Sequence
|
11
|
+
|
12
|
+
from ybox.cmd import (PkgMgr, RepoCmd, build_shell_command, page_output,
|
13
|
+
run_command)
|
14
|
+
from ybox.config import Consts, StaticConfiguration
|
15
|
+
from ybox.print import fgcolor as fg
|
16
|
+
from ybox.print import print_error, print_info, print_warn
|
17
|
+
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
18
|
+
from ybox.util import FormatTable
|
19
|
+
|
20
|
+
|
21
|
+
def repo_add(args: argparse.Namespace, pkgmgr: SectionProxy, repo: SectionProxy,
|
22
|
+
docker_cmd: str, conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
23
|
+
state: YboxStateManagement) -> int:
|
24
|
+
"""
|
25
|
+
Add a new named repository given server URL(s) and optionally a verification key
|
26
|
+
(usually a GPG/PGP key) to the underlying distribution's package manager.
|
27
|
+
This method will also update the package metadata cache to reflect the change.
|
28
|
+
|
29
|
+
:param args: arguments having `name`, `urls` and all other attributes passed by the user
|
30
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
31
|
+
:param repo: the `[repo]` section from `distro.ini` configuration file of the distribution
|
32
|
+
:param docker_cmd: the podman/docker executable to use
|
33
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
34
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
35
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
36
|
+
:return: integer exit status of repo-add command where 0 represents success
|
37
|
+
"""
|
38
|
+
name: str = args.name
|
39
|
+
urls = ",".join(args.urls)
|
40
|
+
# register_repository expects a valid string hence replace with empty string if `None`
|
41
|
+
key: str = args.key or ""
|
42
|
+
options: str = args.options or ""
|
43
|
+
with_source_repo: bool = args.add_source_repo
|
44
|
+
# register in the state database to check if there is no existing entry (the changes
|
45
|
+
# will be rolled back in case of failure later)
|
46
|
+
shared_root = runtime_conf.shared_root
|
47
|
+
container_or_shared_root = shared_root or conf.box_name
|
48
|
+
if not state.register_repository(name, container_or_shared_root, urls, key, options,
|
49
|
+
with_source_repo, update=False):
|
50
|
+
shared_root_msg = f" (on shared root {shared_root})" if shared_root else ""
|
51
|
+
print_error(f"Repository with name '{name}' already registered for the container "
|
52
|
+
f"'{conf.box_name}'{shared_root_msg}")
|
53
|
+
return 1
|
54
|
+
|
55
|
+
exists_cmd = repo[RepoCmd.EXISTS.value].format(name=name)
|
56
|
+
if (code := int(run_command(build_shell_command(docker_cmd, conf.box_name, exists_cmd),
|
57
|
+
exit_on_error=False, error_msg="SKIP"))) == 0:
|
58
|
+
print_error(f"Repository with name '{name}' already present in the package manager "
|
59
|
+
f"for the container '{conf.box_name}' [distribution: {conf.distribution}]")
|
60
|
+
return 1
|
61
|
+
|
62
|
+
# first fetch and register the key if specified
|
63
|
+
if key:
|
64
|
+
print_info(f"Fetching and registering key '{key}'")
|
65
|
+
key_server = str(args.key_server or repo.get(RepoCmd.DEFAULT_GPG_KEY_SERVER.value,
|
66
|
+
fallback=Consts.default_key_server()))
|
67
|
+
if re.match(r"^\S*?://", key):
|
68
|
+
add_key_cmd = repo[RepoCmd.ADD_KEY.value].format(url=key, name=name)
|
69
|
+
print_info(f"Registering key from URL '{key}'")
|
70
|
+
with subprocess.Popen(build_shell_command(docker_cmd, conf.box_name, add_key_cmd),
|
71
|
+
stdout=subprocess.PIPE) as key_result:
|
72
|
+
# fetch the key ID from the output to register it
|
73
|
+
keyid_tag = "KEYID="
|
74
|
+
assert key_result.stdout is not None
|
75
|
+
while line := key_result.stdout.readline():
|
76
|
+
key_out = line.decode("utf-8").strip()
|
77
|
+
if key_out.startswith(keyid_tag):
|
78
|
+
if (keyid := key_out[len(keyid_tag):]) != key:
|
79
|
+
key = keyid
|
80
|
+
print_info(f"Registered key '{key}'")
|
81
|
+
state.register_repository(name, container_or_shared_root, urls, key,
|
82
|
+
options, with_source_repo, update=True)
|
83
|
+
else:
|
84
|
+
sys.stdout.buffer.write(line)
|
85
|
+
sys.stdout.flush()
|
86
|
+
if (code := key_result.wait(60)) != 0:
|
87
|
+
print_error(f"FAILED to register key '{key}' for repository '{name}' -- "
|
88
|
+
"see the output above for details.")
|
89
|
+
return code
|
90
|
+
else:
|
91
|
+
add_key_cmd = repo[RepoCmd.ADD_KEY_ID.value].format(key=key, server=key_server,
|
92
|
+
name=name)
|
93
|
+
print_info(f"Registering key '{key}'")
|
94
|
+
if (code := int(run_command(build_shell_command(
|
95
|
+
docker_cmd, conf.box_name, add_key_cmd), exit_on_error=False,
|
96
|
+
error_msg="registering repository key"))) != 0:
|
97
|
+
return code
|
98
|
+
|
99
|
+
# in case of failures, unregister key and/or repository at the end
|
100
|
+
add_cmd = repo[RepoCmd.ADD.value].format(name=name, urls=urls, options=options)
|
101
|
+
success = False
|
102
|
+
repo_added = False
|
103
|
+
src_added = False
|
104
|
+
try:
|
105
|
+
# next add the repository
|
106
|
+
print_info(f"Registering repository '{name}'")
|
107
|
+
if (code := int(run_command(build_shell_command(docker_cmd, conf.box_name, add_cmd),
|
108
|
+
exit_on_error=False, error_msg="adding repository"))) != 0:
|
109
|
+
return code
|
110
|
+
repo_added = True
|
111
|
+
# add the source code repository if specified
|
112
|
+
if with_source_repo and (add_src_cmd := repo.get(RepoCmd.ADD_SOURCE.value, fallback="")):
|
113
|
+
print_info(f"Registering source code repository '{name}'")
|
114
|
+
add_src_cmd = add_src_cmd.format(name=name, urls=urls, options=options)
|
115
|
+
if (code := int(run_command(build_shell_command(
|
116
|
+
docker_cmd, conf.box_name, add_src_cmd), exit_on_error=False,
|
117
|
+
error_msg="adding source repository"))) != 0:
|
118
|
+
return code
|
119
|
+
src_added = True
|
120
|
+
|
121
|
+
# finally update the package metadata for the change in repositories
|
122
|
+
code = _refresh_package_metadata(pkgmgr, docker_cmd, conf)
|
123
|
+
success = code == 0
|
124
|
+
return code
|
125
|
+
finally:
|
126
|
+
# remove the added key and/or repository in case of failure
|
127
|
+
if not success:
|
128
|
+
if repo_added:
|
129
|
+
print_info(f"Trying to unregister failed repository '{name}'")
|
130
|
+
remove_cmd = repo[RepoCmd.REMOVE.value].format(name=name, remove_source=src_added)
|
131
|
+
run_command(build_shell_command(docker_cmd, conf.box_name, remove_cmd),
|
132
|
+
exit_on_error=False, error_msg=f"unregistering repository '{name}'")
|
133
|
+
if key:
|
134
|
+
print_info(f"Trying to unregister key for failed repository '{name}'")
|
135
|
+
remove_key_cmd = repo[RepoCmd.REMOVE_KEY.value].format(key=key, name=name)
|
136
|
+
run_command(build_shell_command(docker_cmd, conf.box_name, remove_key_cmd),
|
137
|
+
exit_on_error=False, error_msg=f"unregistering key '{key}'")
|
138
|
+
|
139
|
+
|
140
|
+
def repo_remove(args: argparse.Namespace, pkgmgr: SectionProxy, repo: SectionProxy,
|
141
|
+
docker_cmd: str, conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
142
|
+
state: YboxStateManagement) -> int:
|
143
|
+
"""
|
144
|
+
Remove an existing named repository from the underlying distribution's package manager.
|
145
|
+
This method will also update the package metadata cache to reflect the change.
|
146
|
+
|
147
|
+
:param args: arguments having `name` and all other attributes passed by the user
|
148
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
149
|
+
:param repo: the `[repo]` section from `distro.ini` configuration file of the distribution
|
150
|
+
:param docker_cmd: the podman/docker executable to use
|
151
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
152
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
153
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
154
|
+
:return: integer exit status of repo-remove command where 0 represents success
|
155
|
+
"""
|
156
|
+
# unregister from the state database to check if there is no existing entry (the changes
|
157
|
+
# will be rolled back in case of failure later)
|
158
|
+
name: str = args.name
|
159
|
+
shared_root = runtime_conf.shared_root
|
160
|
+
container_or_shared_root = shared_root or conf.box_name
|
161
|
+
if not (result := state.unregister_repository(name, container_or_shared_root)):
|
162
|
+
shared_root_msg = f" (on shared root {shared_root})" if shared_root else ""
|
163
|
+
print_error(f"No such repository with name '{name}' registered for the container "
|
164
|
+
f"'{conf.box_name}'{shared_root_msg}")
|
165
|
+
return 1
|
166
|
+
key, with_source_repo = result
|
167
|
+
# first unregister the repository key, if any
|
168
|
+
if key:
|
169
|
+
print_info(f"Unregistering repository key '{key}'")
|
170
|
+
remove_key_cmd = repo[RepoCmd.REMOVE_KEY.value].format(key=key, name=name)
|
171
|
+
if (code := int(run_command(build_shell_command(
|
172
|
+
docker_cmd, conf.box_name, remove_key_cmd), exit_on_error=False,
|
173
|
+
error_msg=f"unregistering key '{key}'"))) != 0 and not args.force:
|
174
|
+
return code
|
175
|
+
# next remove the repository
|
176
|
+
print_info(f"Unregistering repository '{name}'")
|
177
|
+
remove_cmd = repo[RepoCmd.REMOVE.value].format(name=name, remove_source=with_source_repo)
|
178
|
+
if (code := int(run_command(build_shell_command(
|
179
|
+
docker_cmd, conf.box_name, remove_cmd), exit_on_error=False,
|
180
|
+
error_msg=f"unregistering repository '{name}'"))) != 0 and not args.force:
|
181
|
+
return code
|
182
|
+
# finally update package metadata
|
183
|
+
code = _refresh_package_metadata(pkgmgr, docker_cmd, conf)
|
184
|
+
return 0 if args.force else code
|
185
|
+
|
186
|
+
|
187
|
+
def _refresh_package_metadata(pkgmgr: SectionProxy, docker_cmd: str,
|
188
|
+
conf: StaticConfiguration) -> int:
|
189
|
+
"""
|
190
|
+
Refresh package metadata cache to reflect the change in the registered repositories.
|
191
|
+
|
192
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
193
|
+
:param docker_cmd: the podman/docker executable to use
|
194
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
195
|
+
:return: integer exit status of package refresh command where 0 represents success
|
196
|
+
"""
|
197
|
+
print_info("Refreshing package metadata")
|
198
|
+
update_meta_cmd = pkgmgr[PkgMgr.UPDATE_META.value]
|
199
|
+
return int(run_command(build_shell_command(docker_cmd, conf.box_name, update_meta_cmd),
|
200
|
+
exit_on_error=False, error_msg="updating package metadata"))
|
201
|
+
|
202
|
+
|
203
|
+
# noinspection PyUnusedLocal
|
204
|
+
def repo_list(args: argparse.Namespace, pkgmgr: SectionProxy, repo: SectionProxy,
|
205
|
+
docker_cmd: str, conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
206
|
+
state: YboxStateManagement) -> int:
|
207
|
+
# pylint: disable=unused-argument
|
208
|
+
"""
|
209
|
+
List the repositories registered using :func:`repo-add`.
|
210
|
+
|
211
|
+
:param args: arguments having all attributes passed by the user
|
212
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
213
|
+
:param repo: the `[repo]` section from `distro.ini` configuration file of the distribution
|
214
|
+
:param docker_cmd: the podman/docker executable to use
|
215
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
216
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
217
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
218
|
+
:return: integer exit status of repo-list command where 0 represents success
|
219
|
+
"""
|
220
|
+
separator: str = args.plain_separator or ""
|
221
|
+
|
222
|
+
def plain_output(tbl: Iterable[Iterable[str]], hdr: Sequence[str]) -> str:
|
223
|
+
return "\n".join((separator.join(hdr), *(separator.join(line) for line in tbl)))
|
224
|
+
|
225
|
+
repos = state.get_repositories(runtime_conf.shared_root or conf.box_name)
|
226
|
+
if not repos:
|
227
|
+
print_warn(f"No external repositories have been registered in container '{conf.box_name}'")
|
228
|
+
return 1
|
229
|
+
fg_name = fg.lightgray
|
230
|
+
fg_urls = fg.orange
|
231
|
+
if args.verbose:
|
232
|
+
table = ((name, urls, key, options, "true" if with_source_repo else "false")
|
233
|
+
for name, urls, key, options, with_source_repo in repos)
|
234
|
+
headers = ("Name", "Servers", "Key", "Options", "Source")
|
235
|
+
if separator:
|
236
|
+
out = plain_output(table, headers)
|
237
|
+
else:
|
238
|
+
# using ratio of 4:16:11:6:3 (out of 40) for the widths of the five columns
|
239
|
+
out = FormatTable(table, headers, (fg_name, fg_urls, fg.cyan, fg.green, fg.blue),
|
240
|
+
"rounded_grid", (4.0, 16.0, 11.0, 6.0, 3.0)).show()
|
241
|
+
else:
|
242
|
+
table = ((name, urls) for name, urls, _, _, _ in repos)
|
243
|
+
headers = ("Name", "Servers")
|
244
|
+
if separator:
|
245
|
+
out = plain_output(table, headers)
|
246
|
+
else:
|
247
|
+
out = FormatTable(table, headers, (fg_name, fg_urls),
|
248
|
+
"rounded_grid", (2.0, 8.0)).show()
|
249
|
+
# empty pager argument is a valid one and indicates no pagination, hence the `is None` check
|
250
|
+
pager: str = (args.pager or "") if args.pager is not None or separator else conf.pager
|
251
|
+
return page_output((out.encode("utf-8"),), pager)
|