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.
Files changed (76) hide show
  1. ybox/__init__.py +2 -0
  2. ybox/cmd.py +307 -0
  3. ybox/conf/completions/ybox.fish +93 -0
  4. ybox/conf/distros/arch/add-gpg-key.sh +29 -0
  5. ybox/conf/distros/arch/distro.ini +192 -0
  6. ybox/conf/distros/arch/init-base.sh +10 -0
  7. ybox/conf/distros/arch/init-user.sh +35 -0
  8. ybox/conf/distros/arch/init.sh +82 -0
  9. ybox/conf/distros/arch/list_fmt_long.py +76 -0
  10. ybox/conf/distros/arch/pkgdeps.py +276 -0
  11. ybox/conf/distros/deb-generic/check-package.sh +77 -0
  12. ybox/conf/distros/deb-generic/distro.ini +190 -0
  13. ybox/conf/distros/deb-generic/fetch-gpg-key-id.sh +30 -0
  14. ybox/conf/distros/deb-generic/init-base.sh +11 -0
  15. ybox/conf/distros/deb-generic/init-user.sh +3 -0
  16. ybox/conf/distros/deb-generic/init.sh +136 -0
  17. ybox/conf/distros/deb-generic/list_fmt_long.py +114 -0
  18. ybox/conf/distros/deb-generic/pkgdeps.py +208 -0
  19. ybox/conf/distros/deb-oldstable/distro.ini +21 -0
  20. ybox/conf/distros/deb-stable/distro.ini +21 -0
  21. ybox/conf/distros/supported.list +5 -0
  22. ybox/conf/distros/ubuntu2204/distro.ini +21 -0
  23. ybox/conf/distros/ubuntu2404/distro.ini +21 -0
  24. ybox/conf/profiles/apps.ini +26 -0
  25. ybox/conf/profiles/basic.ini +310 -0
  26. ybox/conf/profiles/dev.ini +25 -0
  27. ybox/conf/profiles/games.ini +39 -0
  28. ybox/conf/resources/entrypoint-base.sh +170 -0
  29. ybox/conf/resources/entrypoint-common.sh +23 -0
  30. ybox/conf/resources/entrypoint-cp.sh +32 -0
  31. ybox/conf/resources/entrypoint-root.sh +20 -0
  32. ybox/conf/resources/entrypoint-user.sh +21 -0
  33. ybox/conf/resources/entrypoint.sh +249 -0
  34. ybox/conf/resources/prime-run +13 -0
  35. ybox/conf/resources/run-in-dir +60 -0
  36. ybox/conf/resources/run-user-bash-cmd +14 -0
  37. ybox/config.py +255 -0
  38. ybox/env.py +205 -0
  39. ybox/filelock.py +77 -0
  40. ybox/migrate/0.9.0-0.9.7:0.9.8.py +33 -0
  41. ybox/pkg/__init__.py +0 -0
  42. ybox/pkg/clean.py +33 -0
  43. ybox/pkg/info.py +40 -0
  44. ybox/pkg/inst.py +638 -0
  45. ybox/pkg/list.py +191 -0
  46. ybox/pkg/mark.py +68 -0
  47. ybox/pkg/repair.py +150 -0
  48. ybox/pkg/repo.py +251 -0
  49. ybox/pkg/search.py +52 -0
  50. ybox/pkg/uninst.py +92 -0
  51. ybox/pkg/update.py +56 -0
  52. ybox/print.py +121 -0
  53. ybox/run/__init__.py +0 -0
  54. ybox/run/cmd.py +54 -0
  55. ybox/run/control.py +102 -0
  56. ybox/run/create.py +1116 -0
  57. ybox/run/destroy.py +64 -0
  58. ybox/run/graphics.py +367 -0
  59. ybox/run/logs.py +57 -0
  60. ybox/run/ls.py +64 -0
  61. ybox/run/pkg.py +445 -0
  62. ybox/schema/0.9.1-added.sql +27 -0
  63. ybox/schema/0.9.6-added.sql +18 -0
  64. ybox/schema/init.sql +39 -0
  65. ybox/schema/migrate/0.9.0:0.9.1.sql +42 -0
  66. ybox/schema/migrate/0.9.1:0.9.2.sql +8 -0
  67. ybox/schema/migrate/0.9.2:0.9.3.sql +2 -0
  68. ybox/schema/migrate/0.9.5:0.9.6.sql +2 -0
  69. ybox/state.py +914 -0
  70. ybox/util.py +351 -0
  71. ybox-0.9.8.dist-info/LICENSE +19 -0
  72. ybox-0.9.8.dist-info/METADATA +533 -0
  73. ybox-0.9.8.dist-info/RECORD +76 -0
  74. ybox-0.9.8.dist-info/WHEEL +5 -0
  75. ybox-0.9.8.dist-info/entry_points.txt +8 -0
  76. 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)