ybox 0.9.8.1__py3-none-any.whl → 0.9.11__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 +1 -1
- ybox/cmd.py +17 -1
- ybox/conf/completions/ybox.fish +2 -0
- ybox/conf/distros/arch/init-user.sh +2 -2
- ybox/conf/distros/arch/init.sh +1 -0
- ybox/conf/distros/arch/pkgdeps.py +2 -0
- ybox/conf/distros/deb-generic/pkgdeps.py +2 -1
- ybox/conf/profiles/apps.ini +10 -5
- ybox/conf/profiles/basic.ini +48 -23
- ybox/conf/profiles/dev.ini +4 -6
- ybox/conf/resources/entrypoint-cp.sh +1 -1
- ybox/conf/resources/entrypoint-root.sh +4 -3
- ybox/conf/resources/entrypoint-user.sh +5 -3
- ybox/conf/resources/entrypoint.sh +24 -22
- ybox/conf/resources/prime-run +0 -2
- ybox/conf/resources/run-in-dir +30 -16
- ybox/conf/resources/run-user-bash-cmd +17 -1
- ybox/conf/resources/ybox-systemd.template +24 -0
- ybox/config.py +9 -1
- ybox/env.py +18 -7
- ybox/migrate/{0.9.0-0.9.7:0.9.8.py → 0.9.0-0.9.10:0.9.11.py} +6 -5
- ybox/pkg/clean.py +1 -7
- ybox/pkg/info.py +1 -7
- ybox/pkg/inst.py +40 -22
- ybox/pkg/list.py +1 -6
- ybox/pkg/mark.py +1 -1
- ybox/pkg/repair.py +4 -0
- ybox/pkg/search.py +1 -7
- ybox/run/cmd.py +2 -1
- ybox/run/control.py +107 -25
- ybox/run/create.py +254 -63
- ybox/run/destroy.py +89 -4
- ybox/run/graphics.py +37 -17
- ybox/run/logs.py +2 -1
- ybox/run/ls.py +2 -1
- ybox/run/pkg.py +49 -7
- ybox/state.py +22 -3
- ybox/util.py +5 -5
- {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/METADATA +68 -34
- ybox-0.9.11.dist-info/RECORD +77 -0
- {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/WHEEL +1 -1
- ybox-0.9.8.1.dist-info/RECORD +0 -76
- {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/entry_points.txt +0 -0
- {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info/licenses}/LICENSE +0 -0
- {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/top_level.txt +0 -0
ybox/env.py
CHANGED
@@ -60,34 +60,41 @@ class Environ:
|
|
60
60
|
:param home_dir: if a non-default user home directory has to be set
|
61
61
|
"""
|
62
62
|
self._home_dir = home_dir or os.path.expanduser("~")
|
63
|
+
self._home_dir = self._home_dir.rstrip("/")
|
63
64
|
self._docker_cmd = docker_cmd or get_docker_command()
|
64
65
|
cmd_version = subprocess.check_output([self._docker_cmd, "--version"])
|
65
66
|
self._uses_podman = "podman" in cmd_version.decode("utf-8").lower()
|
66
67
|
# local user home might be in a different location than /home but target user in the
|
67
68
|
# container will always be in /home with podman else /root for the root user with docker
|
68
69
|
# as ensured by entrypoint-base.sh script
|
69
|
-
|
70
|
+
current_user = getpass.getuser()
|
71
|
+
current_uid = pwd.getpwnam(current_user).pw_uid
|
70
72
|
if self._uses_podman:
|
71
|
-
self._target_user =
|
72
|
-
target_uid =
|
73
|
+
self._target_user = current_user
|
74
|
+
target_uid = current_uid
|
73
75
|
self._target_home = f"/home/{self._target_user}"
|
74
76
|
else:
|
75
77
|
self._target_user = "root"
|
78
|
+
target_uid = 0
|
76
79
|
self._target_home = "/root"
|
77
80
|
# confirm that docker is being used in rootless mode (not required for podman because
|
78
81
|
# it runs as rootless when run by a non-root user in any case without explicit sudo
|
79
82
|
# which the ybox tools don't use)
|
80
83
|
if (docker_ctx := subprocess.check_output(
|
81
84
|
[self._docker_cmd, "context", "show"]).decode("utf-8")).strip() != "rootless":
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
+
# check for DOCKER_HOST environment variable
|
86
|
+
expected_docker_host = f"unix:///run/user/{current_uid}/docker.sock"
|
87
|
+
if not docker_ctx or os.environ.get("DOCKER_HOST", "") != expected_docker_host:
|
88
|
+
raise NotSupportedError("docker should use the rootless mode (see "
|
89
|
+
"https://docs.docker.com/engine/security/rootless/) "
|
90
|
+
f"but the current context is '{docker_ctx}' and "
|
91
|
+
f"$DOCKER_HOST is not set to '{expected_docker_host}'")
|
85
92
|
os.environ["TARGET_HOME"] = self._target_home
|
86
93
|
self._user_base = user_base = site.getuserbase()
|
87
94
|
target_user_base = f"{self._target_home}/.local"
|
88
95
|
self._data_dir = f"{user_base}/share/ybox"
|
89
96
|
self._target_data_dir = f"{target_user_base}/share/ybox"
|
90
|
-
self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "")
|
97
|
+
self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "").rstrip("/")
|
91
98
|
# the container user's one can be different because it is the root user for docker
|
92
99
|
self._target_xdg_rt_dir = f"/run/user/{target_uid}"
|
93
100
|
self._now = datetime.now()
|
@@ -148,6 +155,10 @@ class Environ:
|
|
148
155
|
"""if podman is the container manager being used"""
|
149
156
|
return self._uses_podman
|
150
157
|
|
158
|
+
def systemd_user_conf_dir(self) -> str:
|
159
|
+
"""standard configuration directory location of user specific systemd services"""
|
160
|
+
return f"{self._home_dir}/.config/systemd/user"
|
161
|
+
|
151
162
|
@property
|
152
163
|
def target_user(self) -> str:
|
153
164
|
"""username of the container user (which is the same as the current user for podman
|
@@ -20,11 +20,12 @@ copy_ybox_scripts_to_container(static_conf, distro_conf)
|
|
20
20
|
|
21
21
|
# rename PKGMGR_CLEANUP to PKGMGR_CLEAN in pkgmgr.conf
|
22
22
|
scripts_dir = static_conf.scripts_dir
|
23
|
-
pkgmgr_conf = f"{scripts_dir}/pkgmgr.conf"
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
pkgmgr_conf = Path(f"{scripts_dir}/pkgmgr.conf")
|
24
|
+
if pkgmgr_conf.exists():
|
25
|
+
with pkgmgr_conf.open("r", encoding="utf-8") as pkgmgr_file:
|
26
|
+
pkgmgr_data = pkgmgr_file.read()
|
27
|
+
with pkgmgr_conf.open("w", encoding="utf-8") as pkgmgr_file:
|
28
|
+
pkgmgr_file.write(pkgmgr_data.replace("PKGMGR_CLEANUP", "PKGMGR_CLEAN"))
|
28
29
|
# run entrypoint-root.sh again to refresh scripts and configuration
|
29
30
|
subprocess.run([static_conf.env.docker_cmd, "exec", "-it", static_conf.box_name, "/usr/bin/sudo",
|
30
31
|
"/bin/bash", f"{static_conf.target_scripts_dir}/entrypoint-root.sh"])
|
ybox/pkg/clean.py
CHANGED
@@ -8,14 +8,10 @@ from configparser import SectionProxy
|
|
8
8
|
from ybox.cmd import PkgMgr, build_shell_command, run_command
|
9
9
|
from ybox.config import StaticConfiguration
|
10
10
|
from ybox.print import print_info
|
11
|
-
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
12
11
|
|
13
12
|
|
14
|
-
# noinspection PyUnusedLocal
|
15
13
|
def clean_cache(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
16
|
-
conf: StaticConfiguration
|
17
|
-
state: YboxStateManagement) -> int:
|
18
|
-
# pylint: disable=unused-argument
|
14
|
+
conf: StaticConfiguration) -> int:
|
19
15
|
"""
|
20
16
|
Clean package cache and related intermediate files.
|
21
17
|
|
@@ -23,8 +19,6 @@ def clean_cache(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
|
23
19
|
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
24
20
|
:param docker_cmd: the podman/docker executable to use
|
25
21
|
: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
22
|
:return: integer exit status of clean command where 0 represents success
|
29
23
|
"""
|
30
24
|
print_info(f"Cleaning package cache in container '{conf.box_name}'")
|
ybox/pkg/info.py
CHANGED
@@ -8,14 +8,10 @@ from configparser import SectionProxy
|
|
8
8
|
|
9
9
|
from ybox.cmd import PkgMgr, page_command
|
10
10
|
from ybox.config import StaticConfiguration
|
11
|
-
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
12
11
|
|
13
12
|
|
14
|
-
# noinspection PyUnusedLocal
|
15
13
|
def info_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
16
|
-
conf: StaticConfiguration
|
17
|
-
state: YboxStateManagement) -> int:
|
18
|
-
# pylint: disable=unused-argument
|
14
|
+
conf: StaticConfiguration) -> int:
|
19
15
|
"""
|
20
16
|
Show detailed information of an installed or repository package(s).
|
21
17
|
|
@@ -23,8 +19,6 @@ def info_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: st
|
|
23
19
|
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
24
20
|
:param docker_cmd: the podman/docker executable to use
|
25
21
|
: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
22
|
:return: integer exit status of info command where 0 represents success
|
29
23
|
"""
|
30
24
|
quiet_flag = pkgmgr[PkgMgr.QUIET_DETAILS_FLAG.value] if args.quiet else ""
|
ybox/pkg/inst.py
CHANGED
@@ -24,10 +24,18 @@ from ybox.state import (CopyType, DependencyType, RuntimeConfiguration,
|
|
24
24
|
from ybox.util import check_package, ini_file_reader, select_item_from_menu
|
25
25
|
|
26
26
|
# match both "Exec=" and "TryExec=" lines (don't capture trailing newline)
|
27
|
-
|
27
|
+
_EXEC_PATTERN = r"\s*((Try)?Exec\s*=\s*)(\S+)\s*(.*?)\s*"
|
28
|
+
# pattern to match icons with absolute paths and change them to just names
|
29
|
+
_ICON_PATH_PATTERN = r"\s*Icon\s*=\s*(/usr/share/(icons|pixmaps)/\S+)\s*"
|
30
|
+
# regex to match either of the two above
|
31
|
+
_EXEC_ICON_RE = re.compile(f"^{_EXEC_PATTERN}|{_ICON_PATH_PATTERN}$")
|
28
32
|
# match !p and !a to replace executable program (third group above) and arguments respectively
|
29
33
|
_FLAGS_RE = re.compile("![ap]")
|
30
|
-
|
34
|
+
# environment variables passed through from host environment to podman/docker executable
|
35
|
+
_PASSTHROUGH_ENVVARS = ("XAUTHORITY", "DISPLAY", "WAYLAND_DISPLAY", "FREETYPE_PROPERTIES",
|
36
|
+
"SSH_AUTH_SOCK", "GPG_AGENT_INFO",
|
37
|
+
"__NV_PRIME_RENDER_OFFLOAD", "__GLX_VENDOR_LIBRARY_NAME",
|
38
|
+
"__VK_LAYER_NV_optimus", "VK_ICD_FILES", "VK_ICD_FILENAMES")
|
31
39
|
|
32
40
|
|
33
41
|
def install_package(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
@@ -153,7 +161,10 @@ def _install_package(package: str, args: argparse.Namespace, install_cmd: str, l
|
|
153
161
|
if not skip_desktop_files:
|
154
162
|
copy_type |= CopyType.DESKTOP
|
155
163
|
if not skip_executables:
|
156
|
-
|
164
|
+
resp = input("Create wrapper(s) for application executable(s) of package "
|
165
|
+
f"'{package}'? (Y/n) ") if quiet == 0 else "Y"
|
166
|
+
if resp.strip().lower() != "n":
|
167
|
+
copy_type |= CopyType.EXECUTABLE
|
157
168
|
# TODO: wrappers for newly installed required dependencies should also be created;
|
158
169
|
# handle DependencyType.SUGGESTION if supported by underlying package manager
|
159
170
|
app_flags: dict[str, str] = {}
|
@@ -223,13 +234,14 @@ def get_optional_deps(package: str, docker_cmd: str, container_name: str,
|
|
223
234
|
# 2) redirect PKG: lines somewhere else like a common file: this can be done but will
|
224
235
|
# likely be more messy than the code below (e.g. handle concurrent executions),
|
225
236
|
# but still can be considered in future
|
237
|
+
# (reduced bufsize to show download progress better)
|
226
238
|
with subprocess.Popen(build_shell_command(
|
227
239
|
docker_cmd, container_name, f"{opt_deps_cmd} {package}"),
|
228
|
-
stdout=subprocess.PIPE) as deps_result:
|
240
|
+
bufsize=256, stdout=subprocess.PIPE) as deps_result:
|
229
241
|
line = bytearray()
|
230
242
|
# possible end of lines
|
231
|
-
eol1 = b"\r"
|
232
|
-
eol2 = b"\n"
|
243
|
+
eol1 = ord(b"\r")
|
244
|
+
eol2 = ord(b"\n")
|
233
245
|
buffered = 0
|
234
246
|
assert deps_result.stdout is not None
|
235
247
|
# readline does not work for in-place updates like from aria2
|
@@ -245,7 +257,7 @@ def get_optional_deps(package: str, docker_cmd: str, container_name: str,
|
|
245
257
|
break
|
246
258
|
else:
|
247
259
|
line.append(char[0])
|
248
|
-
if buffered >=
|
260
|
+
if buffered >= 8: # flush frequently to show download progress, for example
|
249
261
|
sys.stdout.flush()
|
250
262
|
buffered = 0
|
251
263
|
sys.stdout.flush()
|
@@ -419,7 +431,7 @@ def docker_cp_action(docker_cmd: str, box_name: str, src: str,
|
|
419
431
|
with tempfile.TemporaryDirectory() as temp_dir:
|
420
432
|
# use shell pipe and tar instead of python Popen and tarfile which will require much more
|
421
433
|
# code unncessarily and may not be able to use `run_command`
|
422
|
-
shell_cmd = (f"'{docker_cmd}' exec '{box_name}' tar -C '{src_dir}' -
|
434
|
+
shell_cmd = (f"'{docker_cmd}' exec '{box_name}' tar -C '{src_dir}' -chpf - '{src_file}' | "
|
423
435
|
f"tar -C '{temp_dir}' -xpf -")
|
424
436
|
if (code := int(run_command(["/bin/sh", "-c", shell_cmd], exit_on_error=False,
|
425
437
|
error_msg=f"copying of file from '{box_name}:{src}'"))) == 0:
|
@@ -430,9 +442,9 @@ def docker_cp_action(docker_cmd: str, box_name: str, src: str,
|
|
430
442
|
def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticConfiguration,
|
431
443
|
app_flags: dict[str, str], wrapper_files: list[str]) -> None:
|
432
444
|
"""
|
433
|
-
For a desktop file, add "podman/docker exec ..." to its Exec
|
434
|
-
|
435
|
-
|
445
|
+
For a desktop file, add "podman/docker exec ..." to its `Exec` lines. Also read the additional
|
446
|
+
flags for the command passed in `app_flags` and add them to an appropriate position in the
|
447
|
+
`Exec` lines. Also removes the `TryExec` lines that do not work in some desktop environments.
|
436
448
|
|
437
449
|
:param filename: name of the desktop file being wrapped
|
438
450
|
:param file: full path of the desktop file being wrapped
|
@@ -445,7 +457,13 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
|
|
445
457
|
# container name is added to desktop file to make it unique
|
446
458
|
wrapper_name = f"ybox.{conf.box_name}.{filename}"
|
447
459
|
|
448
|
-
def
|
460
|
+
def replace_exec_icon(match: re.Match[str]) -> str:
|
461
|
+
"""replace Exec, TryExec and Icon lines appropriately for the host system"""
|
462
|
+
if not (exec_word := match.group(1)): # check for the case of `Icon=/usr/...`
|
463
|
+
return f"Icon={os.path.basename(match.group(5))}\n"
|
464
|
+
# remove TryExec lines that don't work in some desktop environments (like KDE plasma 5)
|
465
|
+
if match.group(2):
|
466
|
+
return ""
|
449
467
|
program = match.group(3)
|
450
468
|
args = match.group(4)
|
451
469
|
# check for additional flags to be added
|
@@ -457,9 +475,9 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
|
|
457
475
|
else:
|
458
476
|
full_cmd = program
|
459
477
|
# pseudo-tty cannot be allocated with rootless docker outside of a terminal app
|
460
|
-
|
461
|
-
|
462
|
-
f'"" {full_cmd}\n')
|
478
|
+
env_vars = " -e=".join(_PASSTHROUGH_ENVVARS)
|
479
|
+
return (f'{exec_word}{docker_cmd} exec -e={env_vars} {conf.box_name} '
|
480
|
+
f'/usr/local/bin/run-in-dir "" {full_cmd}\n')
|
463
481
|
|
464
482
|
# the destination will be $HOME/.local/share/applications
|
465
483
|
os.makedirs(conf.env.user_applications_dir, mode=Consts.default_directory_mode(),
|
@@ -470,7 +488,7 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
|
|
470
488
|
def write_desktop_file(src: str) -> None:
|
471
489
|
with open(wrapper_file, "w", encoding="utf-8") as wrapper_fd:
|
472
490
|
with open(src, "r", encoding="utf-8") as src_fd:
|
473
|
-
wrapper_fd.writelines(
|
491
|
+
wrapper_fd.writelines(_EXEC_ICON_RE.sub(replace_exec_icon, line) for line in src_fd)
|
474
492
|
if docker_cp_action(docker_cmd, conf.box_name, file, write_desktop_file) == 0:
|
475
493
|
wrapper_files.append(wrapper_file)
|
476
494
|
|
@@ -539,8 +557,8 @@ def _copy_app_icons(selected_icons: dict[str, tuple[float, str]], docker_cmd: st
|
|
539
557
|
# copy from temporary file over the existing one, if any, to overwrite rather than move
|
540
558
|
# (which will preserve all of its hard links, for example)
|
541
559
|
exists = os.path.exists(target_icon_path)
|
542
|
-
if docker_cp_action(docker_cmd, conf.box_name, icon_path,
|
543
|
-
|
560
|
+
if docker_cp_action(docker_cmd, conf.box_name, icon_path, lambda src, dest=target_icon_path:
|
561
|
+
None if shutil.copy2(src, dest) else None) == 0: # "if" for pyright
|
544
562
|
# skip registration of icon file it already existed and was overwritten so that
|
545
563
|
# it is not removed on package uninstall
|
546
564
|
if not exists:
|
@@ -568,9 +586,9 @@ def _can_wrap_executable(filename: str, file: str, conf: StaticConfiguration, qu
|
|
568
586
|
print_warn(f"Skipping local wrapper for {file}")
|
569
587
|
return False
|
570
588
|
# also check if creating user executable will override system executable
|
571
|
-
for bin_dir in
|
589
|
+
for bin_dir in Consts.sys_bin_dirs():
|
572
590
|
sys_exec = f"{bin_dir}/{filename}"
|
573
|
-
if os.
|
591
|
+
if os.access(sys_exec, os.X_OK):
|
574
592
|
resp = input(f"Target file {wrapper_exec} will override system installed "
|
575
593
|
f"{sys_exec}. Continue? (y/N) ") if quiet < 2 else "N"
|
576
594
|
if resp.strip().lower() != "y":
|
@@ -604,9 +622,9 @@ def _wrap_executable(filename: str, file: str, docker_cmd: str, conf: StaticConf
|
|
604
622
|
lambda f_match: _replace_flags(f_match, flags, f'"{file}"', '"$@"'), flags)
|
605
623
|
else:
|
606
624
|
full_cmd = f'/usr/local/bin/run-in-dir "`pwd`" "{file}" "$@"'
|
625
|
+
env_vars = " -e=".join(_PASSTHROUGH_ENVVARS)
|
607
626
|
exec_content = ("#!/bin/sh\n",
|
608
|
-
f"exec {docker_cmd} exec -it -e=
|
609
|
-
f"-e=FREETYPE_PROPERTIES {conf.box_name} ", full_cmd)
|
627
|
+
f"exec {docker_cmd} exec -it -e={env_vars} {conf.box_name} ", full_cmd)
|
610
628
|
with open(wrapper_exec, "w", encoding="utf-8") as wrapper_fd:
|
611
629
|
wrapper_fd.writelines(exec_content)
|
612
630
|
os.chmod(wrapper_exec, mode=0o755, follow_symlinks=True)
|
ybox/pkg/list.py
CHANGED
@@ -164,11 +164,8 @@ def _format_long_line(line: str, separator: str, dep_of_width: int,
|
|
164
164
|
return name, version, dep_of, description
|
165
165
|
|
166
166
|
|
167
|
-
# noinspection PyUnusedLocal
|
168
167
|
def list_files(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
169
|
-
conf: StaticConfiguration
|
170
|
-
state: YboxStateManagement) -> int:
|
171
|
-
# pylint: disable=unused-argument
|
168
|
+
conf: StaticConfiguration) -> int:
|
172
169
|
"""
|
173
170
|
List the files of a package installed in a container including those not managed by `ybox-pkg`.
|
174
171
|
|
@@ -176,8 +173,6 @@ def list_files(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
|
176
173
|
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
177
174
|
:param docker_cmd: the podman/docker executable to use
|
178
175
|
: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
176
|
:return: integer exit status of list package files command where 0 represents success
|
182
177
|
"""
|
183
178
|
package: str = args.package
|
ybox/pkg/mark.py
CHANGED
@@ -30,7 +30,7 @@ def mark_package(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str
|
|
30
30
|
mark_explicit: bool = args.explicit
|
31
31
|
mark_dependency_of: str = args.dependency_of or ""
|
32
32
|
if not mark_explicit ^ bool(mark_dependency_of):
|
33
|
-
print_error("ybox-pkg mark: exactly one of -e or -
|
33
|
+
print_error("ybox-pkg mark: exactly one of -e or -d option must be specified "
|
34
34
|
f"(explicit={mark_explicit}, dependency-of={mark_dependency_of})")
|
35
35
|
return 1
|
36
36
|
# check that the package(s) are installed and replace with actual installed name
|
ybox/pkg/repair.py
CHANGED
@@ -14,6 +14,10 @@ from ybox.print import (fgcolor, print_color, print_error, print_info,
|
|
14
14
|
print_warn)
|
15
15
|
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
16
16
|
|
17
|
+
# TODO: SW: repair should work even if no containers are active (just remove the locks)
|
18
|
+
# Also, just checking lock file existing lock file existence is not enough (e.g. for dpkg/apt)
|
19
|
+
# and may need to check if they are locked by any process after kill so define this in distro.ini
|
20
|
+
|
17
21
|
|
18
22
|
def repair_package_state(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
19
23
|
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
ybox/pkg/search.py
CHANGED
@@ -8,14 +8,10 @@ from configparser import SectionProxy
|
|
8
8
|
|
9
9
|
from ybox.cmd import PkgMgr, page_command
|
10
10
|
from ybox.config import StaticConfiguration
|
11
|
-
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
12
11
|
|
13
12
|
|
14
|
-
# noinspection PyUnusedLocal
|
15
13
|
def search_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
16
|
-
conf: StaticConfiguration
|
17
|
-
state: YboxStateManagement) -> int:
|
18
|
-
# pylint: disable=unused-argument
|
14
|
+
conf: StaticConfiguration) -> int:
|
19
15
|
"""
|
20
16
|
Uninstall package specified by `args.package` on a ybox container with given podman/docker
|
21
17
|
command. Additional flags honored are `args.quiet` to bypass user confirmation during
|
@@ -27,8 +23,6 @@ def search_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd:
|
|
27
23
|
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
28
24
|
:param docker_cmd: the podman/docker executable to use
|
29
25
|
:param conf: the :class:`StaticConfiguration` for the container
|
30
|
-
:param runtime_conf: the `RuntimeConfiguration` of the container
|
31
|
-
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
32
26
|
:return: integer exit status of search command where 0 represents success
|
33
27
|
"""
|
34
28
|
quiet_flag = pkgmgr[PkgMgr.QUIET_DETAILS_FLAG.value] if args.quiet else ""
|
ybox/run/cmd.py
CHANGED
@@ -5,7 +5,7 @@ Code for the `ybox-cmd` script that is used to execute programs in an active ybo
|
|
5
5
|
import argparse
|
6
6
|
import sys
|
7
7
|
|
8
|
-
from ybox.cmd import run_command
|
8
|
+
from ybox.cmd import parser_version_check, run_command
|
9
9
|
from ybox.env import get_docker_command
|
10
10
|
|
11
11
|
|
@@ -51,4 +51,5 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
51
51
|
parser.add_argument("container_name", type=str, help="name of the active ybox")
|
52
52
|
parser.add_argument("command", nargs="*", default="/bin/bash",
|
53
53
|
help="run the given command (default is /bin/bash)")
|
54
|
+
parser_version_check(parser, argv)
|
54
55
|
return parser.parse_args(argv)
|
ybox/run/control.py
CHANGED
@@ -6,51 +6,70 @@ import argparse
|
|
6
6
|
import sys
|
7
7
|
import time
|
8
8
|
|
9
|
-
from ybox.cmd import check_active_ybox, get_ybox_state,
|
9
|
+
from ybox.cmd import (check_active_ybox, get_ybox_state, parser_version_check,
|
10
|
+
run_command)
|
10
11
|
from ybox.config import StaticConfiguration
|
11
12
|
from ybox.env import Environ, get_docker_command
|
12
13
|
from ybox.print import fgcolor, print_color, print_error
|
13
14
|
from ybox.util import wait_for_ybox_container
|
14
15
|
|
16
|
+
# TODO: SW: add backup/restore of the running image with the option to backup $HOME of the
|
17
|
+
# container user, and shared root warning if it is being used
|
18
|
+
|
15
19
|
|
16
20
|
def main() -> None:
|
17
21
|
"""main function for `ybox-control` script"""
|
18
22
|
main_argv(sys.argv[1:])
|
19
23
|
|
20
24
|
|
21
|
-
def start_container(docker_cmd: str,
|
25
|
+
def start_container(docker_cmd: str, args: argparse.Namespace):
|
22
26
|
"""
|
23
27
|
Start an existing ybox container.
|
24
28
|
|
25
29
|
:param docker_cmd: the podman/docker executable to use
|
26
|
-
:param
|
30
|
+
:param args: arguments having all attributes passed by the user
|
27
31
|
"""
|
32
|
+
container_name = args.container
|
28
33
|
if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
|
29
34
|
if status[0] == "running":
|
30
|
-
print_color(f"
|
35
|
+
print_color(f"ybox container '{container_name}' already active", fg=fgcolor.cyan)
|
31
36
|
else:
|
32
37
|
print_color(f"Starting ybox container '{container_name}'", fg=fgcolor.cyan)
|
33
38
|
run_command([docker_cmd, "container", "start", container_name],
|
34
39
|
error_msg="container start")
|
35
40
|
conf = StaticConfiguration(Environ(docker_cmd), status[1], container_name)
|
36
|
-
wait_for_ybox_container(docker_cmd, conf)
|
41
|
+
wait_for_ybox_container(docker_cmd, conf, args.timeout)
|
37
42
|
else:
|
38
43
|
print_error(f"No ybox container '{container_name}' found")
|
39
44
|
sys.exit(1)
|
40
45
|
|
41
46
|
|
42
|
-
def stop_container(docker_cmd: str,
|
47
|
+
def stop_container(docker_cmd: str, args: argparse.Namespace):
|
43
48
|
"""
|
44
|
-
Stop
|
49
|
+
Stop an active ybox container.
|
50
|
+
|
51
|
+
:param docker_cmd: the podman/docker executable to use
|
52
|
+
:param args: arguments having all attributes passed by the user
|
53
|
+
"""
|
54
|
+
_stop_container(docker_cmd, args.container, args.timeout,
|
55
|
+
fail_on_error=not args.ignore_stopped)
|
56
|
+
|
57
|
+
|
58
|
+
def _stop_container(docker_cmd: str, container_name: str, timeout: int,
|
59
|
+
fail_on_error: bool):
|
60
|
+
"""
|
61
|
+
Stop an active ybox container.
|
45
62
|
|
46
63
|
:param docker_cmd: the podman/docker executable to use
|
47
64
|
:param container_name: name of the container
|
65
|
+
:param timeout: seconds to wait for container to stop before killing the container
|
48
66
|
:param fail_on_error: if True then show error message on failure to stop else ignore
|
49
67
|
"""
|
50
68
|
if check_active_ybox(docker_cmd, container_name):
|
51
69
|
print_color(f"Stopping ybox container '{container_name}'", fg=fgcolor.cyan)
|
52
|
-
run_command([docker_cmd, "container", "stop", container_name],
|
53
|
-
|
70
|
+
run_command([docker_cmd, "container", "stop", "-t", str(timeout), container_name],
|
71
|
+
error_msg="container stop")
|
72
|
+
for _ in range(timeout * 2):
|
54
73
|
time.sleep(0.5)
|
55
74
|
if get_ybox_state(docker_cmd, container_name, ("exited", "stopped"),
|
56
75
|
exit_on_error=False, state_msg=" stopped"):
|
@@ -63,6 +82,42 @@ def stop_container(docker_cmd: str, container_name: str, fail_on_error: bool):
|
|
63
82
|
print_color(f"No active ybox container '{container_name}' found", fg=fgcolor.cyan)
|
64
83
|
|
65
84
|
|
85
|
+
def restart_container(docker_cmd: str, args: argparse.Namespace):
|
86
|
+
"""
|
87
|
+
Restart an existing ybox container.
|
88
|
+
|
89
|
+
:param docker_cmd: the podman/docker executable to use
|
90
|
+
:param args: arguments having all attributes passed by the user
|
91
|
+
"""
|
92
|
+
_stop_container(docker_cmd, args.container, timeout=int(args.timeout / 2), fail_on_error=False)
|
93
|
+
start_container(docker_cmd, args)
|
94
|
+
|
95
|
+
|
96
|
+
def show_container_status(docker_cmd: str, args: argparse.Namespace) -> None:
|
97
|
+
"""
|
98
|
+
Show container status which will be a string like running/exited.
|
99
|
+
|
100
|
+
:param docker_cmd: the podman/docker executable to use
|
101
|
+
:param args: arguments having all attributes passed by the user
|
102
|
+
"""
|
103
|
+
container_name = args.container
|
104
|
+
if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
|
105
|
+
print(status[0])
|
106
|
+
else:
|
107
|
+
print_error(f"No ybox container '{container_name}' found")
|
108
|
+
|
109
|
+
|
110
|
+
def wait_for_container_stop(docker_cmd: str, args: argparse.Namespace) -> None:
|
111
|
+
"""
|
112
|
+
Wait for an active container to stop.
|
113
|
+
|
114
|
+
:param docker_cmd: the podman/docker executable to use
|
115
|
+
:param args: arguments having all attributes passed by the user
|
116
|
+
"""
|
117
|
+
while check_active_ybox(docker_cmd, args.container):
|
118
|
+
time.sleep(2)
|
119
|
+
|
120
|
+
|
66
121
|
def main_argv(argv: list[str]) -> None:
|
67
122
|
"""
|
68
123
|
Main entrypoint of `ybox-control` that takes a list of arguments which are usually the
|
@@ -73,19 +128,7 @@ def main_argv(argv: list[str]) -> None:
|
|
73
128
|
"""
|
74
129
|
args = parse_args(argv)
|
75
130
|
docker_cmd = get_docker_command()
|
76
|
-
|
77
|
-
if args.action == "start":
|
78
|
-
start_container(docker_cmd, container_name)
|
79
|
-
elif args.action == "stop":
|
80
|
-
stop_container(docker_cmd, container_name, fail_on_error=True)
|
81
|
-
elif args.action == "restart":
|
82
|
-
stop_container(docker_cmd, container_name, fail_on_error=False)
|
83
|
-
start_container(docker_cmd, container_name)
|
84
|
-
elif args.action == "status":
|
85
|
-
if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
|
86
|
-
print(status[0])
|
87
|
-
else:
|
88
|
-
print_error(f"No ybox container '{container_name}' found")
|
131
|
+
args.func(docker_cmd, args)
|
89
132
|
|
90
133
|
|
91
134
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
@@ -96,7 +139,46 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
96
139
|
:return: the result of parsing using the `argparse` library as a :class:`argparse.Namespace`
|
97
140
|
"""
|
98
141
|
parser = argparse.ArgumentParser(description="control ybox containers")
|
99
|
-
parser.
|
100
|
-
|
101
|
-
|
142
|
+
operations = parser.add_subparsers(title="Operations", required=True, metavar="OPERATION",
|
143
|
+
help="DESCRIPTION")
|
144
|
+
|
145
|
+
start = operations.add_parser("start", help="start a ybox container")
|
146
|
+
_add_subparser_args(start, 60, "time in seconds to wait for a container to start")
|
147
|
+
start.set_defaults(func=start_container)
|
148
|
+
|
149
|
+
stop = operations.add_parser("stop", help="stop a ybox container")
|
150
|
+
_add_subparser_args(stop, 10,
|
151
|
+
"time in seconds to wait for a container to stop before killing it")
|
152
|
+
stop.add_argument("-I", "--ignore-stopped", action="store_true",
|
153
|
+
help="don't fail on an already stopped container")
|
154
|
+
stop.set_defaults(func=stop_container)
|
155
|
+
|
156
|
+
restart = operations.add_parser("restart", help="restart a ybox container")
|
157
|
+
_add_subparser_args(restart, 60, "time in seconds to wait for a container to restart")
|
158
|
+
restart.set_defaults(func=restart_container)
|
159
|
+
|
160
|
+
status = operations.add_parser("status", help="show status of a ybox container")
|
161
|
+
_add_subparser_args(status, 0, "")
|
162
|
+
status.set_defaults(func=show_container_status)
|
163
|
+
|
164
|
+
wait = operations.add_parser("wait", help="wait for an active ybox container to stop")
|
165
|
+
_add_subparser_args(wait, 0, "")
|
166
|
+
wait.set_defaults(func=wait_for_container_stop)
|
167
|
+
|
168
|
+
parser_version_check(parser, argv)
|
102
169
|
return parser.parse_args(argv)
|
170
|
+
|
171
|
+
|
172
|
+
def _add_subparser_args(subparser: argparse.ArgumentParser, timeout_default: int,
|
173
|
+
timeout_help: str) -> None:
|
174
|
+
"""
|
175
|
+
Add arguments for the sub-operation of the ybox-control command.
|
176
|
+
|
177
|
+
:param subparser: the :class:`argparse.ArgumentParser` object for the sub-command
|
178
|
+
:param timeout_default: default value for the -t/--timeout argument, or 0 to skip the argument
|
179
|
+
:param timeout_help: help string for the -t/--timeout argument
|
180
|
+
"""
|
181
|
+
if timeout_default != 0:
|
182
|
+
subparser.add_argument("-t", "--timeout", type=int, default=timeout_default,
|
183
|
+
help=timeout_help)
|
184
|
+
subparser.add_argument("container", help="name of the ybox")
|